Thursday, January 29, 2009

Event Handler chains in Shoes

One of the beautiful things about Shoes is how simple and easy it is to set up event handlers on objects. As I talked about in a previous post creating a handler on a button is as simple as

b = button "Click Me"
b.click { alert "You clicked the button" }

This is great for simple things, but I hadn't been hacking on Shoes for very long before I wanted to do more than this. In particular, since one of my projects is creating an application to preview Shoes applications, I wanted to be able to add handlers that call back into my main application without worrying about their effect on the code being previewed.

To do this, you need to be able to have multiple handlers on an element at once, so that you can add new event handlers without disturbing the existing ones. I spent a while trying to understand the c-based implementation of event handlers, and figuring out all of the things I'd have to do to change it to a chain based implementation, but then realized that given the ruby nature of Shoes, this was a prime opportunity for monkeypatching.

Monkeypatching may be a little less performant, but this is for a debugger-style thing anyway (I'm thinking I want to get to something a lot like Firebug is for web development...), so who cares if its slow?

So in I dove. I'm calling the module ShoeShine, because its cleaning up my Shoes without changing the inside at all, and it turned out to be pretty simple to implement. First, I define a few methods to add, remove, and call handlers:


def add_handler(type, &block)
@_handlers ||= {}
@_handlers[type] ||= []
@_handlers[type].push block
# set up callbacks from old handlers invocation into
# new handler chain
self.send "#{type}_without_chains".to_sym do |*args|
self.call_handlers(type, *args)
end
end

def clear_handlers(type)
if @_handlers
if @_handlers.delete(type)
_handler_deleted(type)
end
end
end

def call_handlers(type, *args)
return unless @_handlers[type
@_handlers[type].each do |block|
block.call(*args)
end
end


Then I do some metaprogramming and monkeypatching to plug this in between handler registration and handler callback:


HANDLERS.each do |handler|
eval %(
def #{handler}_with_chains(&block)
# need to clear handlers to maintain old semantics
clear_handlers(:#{handler})
add_handler(:#{handler}, &block)
end
)
end

def self.included(klass)
klass.instance_eval do
ShoeShine::HANDLERS.each do |handler|
if klass.instance_methods.include? handler.to_s
# Replace old handlers with new handlers
alias_method "#{handler}_without_chains".to_sym, handler
alias_method handler, "#{handler}_with_chains".to_sym
end
end
end
end


Finally, I include this module into all interesting Shoes classes:


Shoes.constants.each do |c|
k = Shoes.const_get(c)
if k.is_a? Class
k.send :include, ShoeShine
end
end


Voila! Suddenly, as well as having the traditional Shoes interface to handlers, you can also add additional handlers onto existing objects without disturbing the originals. This will open the door to all sorts of interesting things. I've already mentioned my intent to use this for debugging, but I'm also exploring using this as a way to get around the current Shoes limitations on click, hover, leave, etc handlers of only working on slots, allowing them to trickle down to paragraphs, widgets, etc.

I've got some early versions of this working, but its definitely not ready for prime time yet. For any who are interested, you can find both the ShoeShine code and my early trickle down work at github in the shoes-preview tree.

2 comments: