require 'jimpanzee/exceptions' module Jimpanzee module EventHandlerRegistrationAndDispatchMixin def self.included(target) target.extend ClassMethods end module ClassMethods @@event_handler_procs ||= {} def event_handler_procs @@event_handler_procs[self] ||= Hash.new {|hash, key| hash[key] = []} end @@handlers ||= {} def handlers @@handlers[self] ||= [] end # Declares which components you want events to be generated for. add_listener # takes a hash of the form :type => type, :components => [components for events] # All AWT and Swing listener types are supported. See Jimpanzee::Handlers for # the full list. # # The array of components should be strings or symbols with the exact naming of the # component in the Java class declared in the view. As an example, if you have a JFrame # with a text area named infoTextField that you wanted to receive key events for, perhaps # to filter certain key input or to enable an auto-completion feature you could use: # # add_listener :type => :key, :components => [:infoTextField] # # To handle the event you would then need to implement a method named # _ which in this case would be info_text_field_key_pressed, # info_text_field_key_released or info_text_field_key_typed. # # If you have a single component you can omit the array and pass a single string or symbol. # # add_listener :type => :key, :components => :infoTextField # # You will run into errors if your component is a nested name, for example # # add_listener :type => :document, :components => "infoTextField.document" # # because when the event is generated and a handler is attempted to be located, # the name infoTextField.document doesn't map well to a method. To resolve this, # the component name can be a hash, the key being the component name and the value # being the desired callback name. # # add_listener :type => :document, :components => {"infoTextField.document" => "info_text_field"} # # This will cause the info_text_field_action_performed method to be called when # the action performed event is generated by infoTextField.document. # # If you want to add a listener to the view itself (JFrame, JDialog, etc.) # then you can use :java_window as the component # # add_listener :type => :window, :components => [:java_window] # # If it is not possible to declare a method, or it is desirable to do so dynamically # (even from outside the class), you can use the define_handler method. # # If you wish to override the default event handling behavior, override handle_event def add_listener(details) handlers << details end # define_handler takes a component/event name and a block to be called when that # event is generated for that component. This can be used in place of a method # declaration for that component/event pair. # # So, if you have declared: # # add_listener :type => :action, :components => [:ok_button] # # you could implement the handler using: # # define_handler(:ok_button_action_performed) do |event| # # handle the event here # end # # Note that handlers defined using this method will create implicit listener # registrations the same as a declared method would. # # define_handler also accepts multiple event names # # define_handler(:ok_button_action_performed, :cancel_button_action_performed) do # # handle event(s) here # end def define_handler(*actions, &block) actions.each {|action| event_handler_procs[action.to_sym] << block} end end # This method should be called from the initialize method of the class using # this mixin to set up the needed instance variables and both declared # and implicit handlers def setup_implicit_and_explicit_event_handlers # :nodoc: @__event_handler_view_target = if self.class.ancestors.member?(Jimpanzee::Controller) @__view else self end @__registered_handlers = Hash.new{|h,k| h[k] = []} @__event_handler_procs = Hash.new{|h,k| h[k] = []} unless self.class.handlers.empty? if @__event_handler_view_target.nil? raise "A view must be declared in order to add event listeners" end self.class.handlers.each do |handler| handler[:components].each do |component| if component.kind_of? Array component = component.first end begin resolved_component = @__event_handler_view_target.instance_eval(component.to_s, __FILE__, __LINE__) rescue NoMethodError => e raise InvalidHandlerError, "Could not find component: #{component} on view #{@__event_handler_view_target}\nOriginal exception: #{e.message}" end add_handler_for handler[:type], handler[:components], resolved_component end end end jimpanzee_base_class = self.class.ancestors.find {|klass| /^Jimpanzee::(Controller|View)$/ =~ klass.name } (methods.grep(/_/) - (EventHandlerRegistrationAndDispatchMixin.instance_methods + jimpanzee_base_class.instance_methods)).each {|method| add_implicit_handler_for_method(method) } self.class.event_handler_procs.each {|method, proc| add_implicit_handler_for_method(method)} end # Instance-level version of Jimpanzee::Controller.define_handler. It follows the same # syntax as the class-level version but applies the callback block as a listener to events # generated by this instance of the controller class' view. This callback is # useful when the application has nested controllers and event handling needs to be different # for each instance of a controller class. # # define_handler can accept either a single event or a list of events to apply the block to: # # define_handler :ok_button_action_performed { puts "action performed on 'ok button'" } # # define_handler :ok_button_action_performed, :cancel_button_action_performed { puts "action performed on a button" } # # If you are defining a handler that requires aliasing, define handler can also be passed a hash of method => component mappings # mixed in with the methods to apply the handler to. # # define_handler :text_field_insert_update => "text_field.document" { puts "you typed something" } # # define_handler :text_field_insert_update => "text_field.document", :text_field_remove_update => "text_field.document" { puts "you typed or deleted something" } # # These mappings can also be mixed in with regular methods. It is suggested that you put # all of your hash items at the end of the argument list so they are wrapped up into an # implicit Hash object although this is not strictly necessary. # # define_handler :ok_button_action_performed, :text_field_insert_update => "text_field.document" { puts "you did ... something" } # def define_handler(*actions, &block) # define_handler :foo_action_performed => :foo_document_action_performed, { handle event here } actions.each do |action| if action.kind_of? Hash # handle a hash with multiple mappings, e.g. # define_handler :text_field_insert_update => "text_field.document", :text_field2_insert_update => "text_field2.document { ... handler code here ... } action.each do |method, component| @__event_handler_procs[method.to_sym] << block add_implicit_handler_for_method(method, component) end else @__event_handler_procs[action.to_sym] << block add_implicit_handler_for_method(action) end end end # Specific handlers get precedence over general handlers, that is button_mouse_released # gets called before mouse_released. def handle_event(component_name, event_name, event) #:nodoc: return if event.nil? callbacks = get_callbacks("#{component_name}_#{event_name}".to_sym) if callbacks.empty? callbacks = get_callbacks(event_name.to_sym) end callbacks.each{ |proc| 0 == proc.arity ? proc.call : proc.call(event) } end private def get_callbacks(method) callbacks = [] begin callbacks << method(method) rescue NameError; end callbacks + self.class.event_handler_procs[method] + @__event_handler_procs[method] end def add_implicit_handler_for_method(method, component_to_alias = nil) component_match = nil Jimpanzee::Handlers::ALL_EVENT_NAMES.each do |event| component_match = Regexp.new("(.*)_(#{event})").match(method.to_s) break unless component_match.nil? end return if component_match.nil? component_name, event_name = component_match[1], component_match[2] begin if component_to_alias.nil? component = @__event_handler_view_target.instance_eval(component_name) else component = @__event_handler_view_target.instance_eval(component_to_alias) end rescue NameError => e rescue Jimpanzee::UndefinedComponentError => e # swallow, handler style methods for controls that don't exist is allowed else component.methods.each do |method| listener_match = /add(.*)Listener/.match(method) next if listener_match.nil? if Jimpanzee::Handlers::EVENT_NAMES_BY_TYPE[listener_match[1]].member? event_name if component_to_alias.nil? add_handler_for listener_match[1], component_name, component else add_handler_for listener_match[1], {component_to_alias => component_name}, component end end end end end def add_handler_for(handler_type, components, java_component) components = ["global"] if components.nil? components = [components] unless components.kind_of? Array components.each do |component| # handle aliases :components => {"text_area.document" => "text_area"} if component.kind_of? Hash component_name = component.values[0] component_field = component.keys[0] else component_name = component component_field = component end unless @__registered_handlers[java_component].member? handler_type.underscore handler = "Jimpanzee::#{handler_type.camelize}Handler".constantize.new(self, component_name.to_s) @__event_handler_view_target.add_handler(handler, component_field) @__registered_handlers[java_component] << handler_type.underscore end end end end end