Showing posts with label shoequery. Show all posts
Showing posts with label shoequery. Show all posts

Saturday, January 31, 2009

More Shoes Structure

When I first started using Shoes, it took me a while to get the various pieces set in my head. There's a couple of different levels to understand. There are a number of tutorials about layout, the most fundamental being Stacks and Flows. This is critical to understanding to design your GUI, but it wasn't enough for me to really understand what was going on.

After hacking around, reading through the source code, and writing some addons I think I'm starting to get my head around it, so here's an attempt to explain some more of the structure of Shoes.

The most basic piece is Shoes.app. This is the method you use to open a window for a new Shoes application; hello world in shoes is something like.

Shoes.app do
para "Hello World"
end

What this does is twofold... it instantiates a new Shoes::App object, and sets self inside the block to point to that object. The Shoes::App object provides all of the helper methods for creating different components, such as para, button, stack, flow, etc, and is responsible for actually rendering them. The objects themselves don't render, they just define a structure that the Shoes::App object will then render. This is why, when trying to do something in a module or class outside of the block, you need to pass in the app object, or access it through the app method on some already rendered object (all shoes objects have an app method to get back to the app they are rendered in).

How does the structure that Shoes::App builds up look? It is a tree format, that actually looks remarkably similar to the DOM in the web world.

At the root of the tree is the slot associated with the original Shoes::App object. This slot behaves like a flow, and can be accessed via the slot method on the Shoes::App object; within the Shoes.app block that would be self.slot. (This is important; after reading that the app object was a flow, I originally thought I'd be able to use all of the flow methods immediately on it, but you actually need to call them on self.slot.)

Every node in the tree has an accessor called parent and one called children. Lets examine what these look like in the hello world example above, by adding some debug output:

app = Shoes.app do
para "Hello World"
end
Shoes.debug "Parent is #{app.slot.parent.inspect}"
Shoes.debug "Children are #{app.slot.children.inspect}"

The console now shows:



which is pretty much what we'd expect. The root should have no parent, and the paragraph is pretty much the only element, so it should be the first child. What does that paragraph look like? Lets try

Shoes.app do
p = para "Hello World"
Shoes.debug "Para Parent is #{p.parent.inspect}"
Shoes.debug "Para Children are #{p.children.inspect}"
end

Now we see:



The parent is pointing back to the app.slot (which we now see is not actually a flow, but an instance of the Shoes class, which flows inherit from.) And the children of a Para object actually contain the text of the paragraph.

Understanding this tree structure was one of the biggest breakthroughs for me, because it means that many of the things I know from manipulating the DOM with javascript in the web world are suddenly applicable to Shoes. I'm no javascript genius, but I have already climbed some of the learning curve there, and being able to apply that to Shoes was eye opening.

One of the things the DOM does really well (and the Shoes model should as well) is allow you to navigate, find, and manipulate things that have been generated someplace else. This is where I got the idea for ShoeQuery, and will prove extremely useful in my efforts to build a Shoes debugger.

I think this similarity could make Shoes the toolkit of choice for web developers coming to the desktop.

Sunday, January 25, 2009

Can I just say how much I love method_missing?

I wanted to follow up on my previous explorations with ShoeQuery to add some helper methods to access pieces of shoes functionality... thinking I'd have to add pieces one by one. However, it turns out that due to the magic of method_missing, I was able to add access to basically all of them with a single 22-line method. It looks like:


def method_missing(method, *args, &block)
if args.empty? && !block_given?
# if you have no arguments and no block, think of it as a query
# and return the answer for the first element.
if first.respond_to? method
return first.send(method)
else
return elem.style(method)
end
elsif block_given?
each {|elem| elem.send(method, *args, &block)}
else
# Otherwise, think of it as a setter
setter = "#{method}=".to_sym
if first.respond_to? setter
each {|elem| elem.send(setter, *args, &block)}
else
each {|elem| elem.style(method => args[0], &block)}
end
end
self
end


And now, thanks to this I can write lines in shoes as follows:


# find all paragraphs, change their text to 'all ps', and color
# them red
shoe_query('para').text("all ps").stroke(red)



# replace the click handlers on all buttons
shoe_query('button').click do
para "All your button are belong to us now
end

Introducing ShoeQuery

ShoeQuery (better name needed) was born out of a desire to be able to find and manipulate elements in Shoes without having stashed away their locations ahead of time. It was inspired by jQuery's mechanisms for finding and manipulating DOM objects in Javascript.

The fundamental object (a ShoeQuery object) is pretty much an array of Shoes Elements, augmented with some extra functions. ShoeQuery objects are created via a helper (unsurprisingly named shoe_query) that lives in the Shoes::App object, and so should be accessible anywhere you want it. You can pass into that helper a selector, and it will return a ShoeQuery object containing the elements that match. You can also pass in an array of elements you already have handy, or an existing element, and it will also return a ShoeQuery object containing those elements. This form should be familiar for those who've used jQuery.

Right now, the things it lets you do are pretty limited, though a lot of the functionality jQuery specially provides already exists in Ruby. The one big feature is the ability to find elements by type, including css-like hierarchy. You can do this either by invoking shoe_query directly, or calling find on an existing shoe_query object (to search within its children). For example, given the demo app:


load 'shoe_query.rb'
Shoes.app do
app = Shoes.app do
para "foo"
f = flow do
stack do
para "bar"
para 'blah'
end
end
b = button "click me to change all paragraphs"
b.click do
shoe_query('para').each do |p|
p.text = "all ps"
end
end
b2 = button "click me to just change the paragraphs inside the flow"
b2.click do
shoe_query('flow').find('para').each do |p|
p.text = "flow ps"
end
end
b3 = button "click me to change the button inside the stack"
b3.click do
shoe_query('stack para').each do |p|
p.text = "stack p"
end
end

end


In this case, the first button modifies all three paragraph elements, while the latter two use two different mechanisms to scope their changes to within the flow and within the stack respectively

As I play with this more, I'll be extending it to include more functionality. Let me know if there's anything you want!

You can get ShoeQuery at github at http://github.com/kball/shoequery/tree/master