# # application.rb # vienna # # Created by Adam Beynon. # Copyright 2010 Adam Beynon. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # require 'foundation/core/responder' module CherryKit # Main Application class to control the lifetime and control flow of events # through a cherry kit application. Custom behaviour can be setup through # the application delegate, which by default is the custom AppController # class. # # Notifications # ------------- # # On setting the delegate, the delegate is automatically registered to receive # all of the following notifications on the presumtion that it has a method # named the same as the notification name. Any methods that are not # implemented will simply not receive the relevant notification. All # notifications and methods are optional, but application_did_finish_launching # is highly recomended as it is the best/required place to setup the # application GUI. # # :application_will_finish_launching - sent when the application is nearly # ready to run, but before any global events are registered. This is a # suitable place to perform any additional tasks before the DOM is likely to # be ready. # # :application_did_finish_launching - sent once the DOM has been setup and all # relevant events are in place. This is a suitable time to load the main # interface for the application by loading some windows, or using the # Builder class and its related methods. # class Application < Responder # The application delegate. # attr_reader :delegate def initialize # an array of all our windows @windows = [] # hash of our touch identifiers to the touches themselves. @touches = {} # hash of views to an array of touch identifiers @touches_for_views = {} end # Register the window for the application def register_window(window) @windows << window end # Set the application delegate. This method will register the delegate for # all notification messages that it has a defined method for. # # @param {Object} delegate to set # @returns delegate # def delegate=(delegate) @delegate = delegate center = NotificationCenter.default_center notifications = [ :application_will_finish_launching, :application_did_finish_launching ] notifications.each do |notification| if delegate.respond_to? notification center.add_observer delegate, notification, notification, self end end delegate end # Run the application. def run RunLoop.run do # body class names for css etc setup_body_class_names # global application Object.const_set('CKApp', self) # our main application window that we create ourselves and give (for # free to AppController) window = CherryKit::Window.build({}) window.show @delegate.window = window # puts "need to add window to #{@delegate}" # initial notification that we will finish launching (before events) notify :application_will_finish_launching # setup/create all event handlers setup_event_handlers # we can post our did finish launching once we have all our events setup notify :application_did_finish_launching end end def setup_body_class_names body = Browser::Element.body if Browser.safari? body.class_name = 'safari' elsif Browser.opera? body.class_name = 'opera' elsif Browser.msie? # also set ie7, ie6, ie8 respectively? body.class_name = 'msie' end end def setup_event_handlers # touch based events.. listen_for Browser.document, :touchstart, :touchmove, :touchend, :touchcancel # standard mouse events listen_for Browser.document, :mousedown, :mouseup, :mousemove listen_for Browser.document, :keydown, :keyup, :keypress # browser window resizing listen_for Browser.window, :resize # browser dependant focus/blur events if Browser.msie? # event_handler Browser.document, :focusin # event_handler Browser.document, :focusout else # event_handler Browser.window, :focus # event_handler Browser.window, :blur end end def listen_for(target, *events) events.each do |event| Browser::Event.listen target, event, __send__("on_#{event}") end end # Dispatch the event # # @param {CherryKit::Event} event to send # @returns nil # def send_event(event) # keep track of our current event @current_event = event # if we have an event handler registered, delegate events to that if @event_handler event.view = @event_handler_view RunLoop.run do `#{@event_handler}.apply(#{@event_handler}.__self__, [#{event}]);` end else # send event within a runloop block so that all action calls etc are # caught, as well as view display requests, so we can run them on # completion of sending the event RunLoop.run do # puts "============= sending event on within runloop!" # puts event.type # return # # `console.log(#{event});` # puts "view for event is: #{event.view}" # puts "window for event is: #{event.window}" res = event.window.send_event event # puts "res is #{res}" res end end end # Set a block delegate for dealing with all events matching the given event # array. Any other event will simply be ignored until the removal of this # block. # # Note # ---- # This method will also pass the current event to the block, so in the # following example, the mouse_down event will be passed as well. # # Usage # ----- # # app.handle_events([:mouse_down, :mouse_dragged]) do |event| # puts "got new event #{event}" # end # # @param {Proc} block # def handle_events(events, &block) @event_handler = block @event_handler_events = events @event_handler_view = @current_event.view # resend current event `#{@event_handler}.apply(#{@event_handler}.__self__, [#{@current_event}]);` end # Discard event capture. # def finish_handling_events @event_handler = nil @event_handler_events = nil @event_handler_view = nil end # Get the array for the view, or make it if it does not exist def touches_for_view(view) if @touches_for_views[view] @touches_for_views[view] else @touches_for_views[view] = [] end end def setup_touch_began(touch, event) touch_hierarchy = [] view = touch.view # get all views in hierarchy `while(#{view}.r) { #{touch_hierarchy}.unshift(#{view}); #{view} = #{view.superview}; }` capturing_view = nil to_try = nil # find first view that will capture_touches? .. if any `for (var i = 0; i < #{touch_hierarchy}.length; i++) { #{to_try} = #{touch_hierarchy}[i]; if (#{to_try.capture_touches?}.r) { #{capturing_view} = #{to_try}; break; } }` if capturing_view touch.view = capturing_view else touch.view = event.view end # puts "touch hierarchy is #{touch_hierarchy.inspect}" # puts "capturing view is #{capturing_view}" end # When the user touches their finger on the document. def on_touchstart proc do |event| RunLoop.run do # puts "touchstart!" touches = event.changed_touches touches.each do |touch| # keep track of this touch (with identifier) @touches[touch.identifier] = touch # assign the event for the touch touch.event = event # touch.view = event.view # puts "touch began #{touch.identifier}" setup_touch_began touch, event # we have our right view, so make sure that we can send events to it # i.e. is it multi touch enabled? cannot send more than one touch # to non multi touch views view = touch.view view_touches = touches_for_view(view) if view.multiple_touch_enabled? puts "can send event!" view_touches << touch.identifier touch.view.touches_began(touches, event) else # puts "#{view} is not multi touch friendly" if view_touches.length > 0 # puts "need to drop touch: multitouch not enabled for view" puts "touch_start: #{touch.identifier} being dropped" else view_touches << touch.identifier puts "touch_start: #{touch.identifier} sending!" touch.view.touches_began(touches, event) end end end # puts event.changed_touches # puts event.view false end end end def on_touchend proc do |event| RunLoop.run do touches = event.changed_touches touches.each do |touch| entry = @touches[touch.identifier] if @touches_for_views[entry.view].include? touch.identifier puts "sending touchend for #{entry.identifier}" entry.event = event entry.view.touches_ended(touches, event) @touches_for_views[entry.view].delete touch.identifier else # drop event otherwise # puts "dropping touch end for #{entry.identifier}" end end end false end end def on_touchmove proc do |event| RunLoop.run do # hash of views => touches for that view view_touches = {} touches = event.changed_touches touches.each do |touch| entry = @touches[touch.identifier] unless entry raise "Application: touchmove: unknown touch #{touch.identifier}" end if @touches_for_views[entry.view].include? touch.identifier entry.event = event view_array = view_touches[entry.view] unless view_array view_array = view_touches[entry.view] = [] end view_array << touch entry.view.touches_moved(touches, event) else # puts "dropping move event for touch #{touch.identifier}" end end end false end end def on_touchcancel proc do |event| puts "touchcancel!" true end end # Handles the window's 'resize' event. This method simply posts out # notifications to alert receivers that the browser window, i.e. screen # space is being adjusted. By default, every window will listen for these # notifications so that they can resize and /or move to suit # # @param {Browser::Event} event from browser # @returns self # def on_resize proc do |event| end end def on_keydown proc do |event| event.type = :key_down # puts "sending keydown event for #{event.key_code} for #{event.key}" res = send_event event # puts "on_keydown handler result is #{res}" res end end def on_keyup proc do |event| event.type = :key_up # puts "sending keyup event for #{event.key_code} for #{event.key}" send_event event end end def on_keypress proc do |event| event.type = :key_down # puts "sending keypress event for #{event.key_code} for #{event.key}" return true end end def on_mousemove proc do |event| # must make ruby friendly name - should check if mouse_dragged event.type = :mouse_moved # puts "mouse moved!" send_event event true end end # Handles raw events from the browser for mousedown # # @param [Browser::Event] event received # @returns self # def on_mousedown proc do |event| # begin # first we rename the event to our ruby friendly name event.type = :mouse_down @mouse_down_view = view = event.view window = view.window # get current first responder first_responder = window.first_responder # make the view the first responder (if it accepts it) if view != first_responder && view.accepts_first_responder? window.make_first_responder? view end send_event event # rescue Exception => e # puts "exception occured within Application#on_mousedown" # raise e # end true end end # Handles raw mouseup events from browser # # @para, [Browser::Event] event received # @returns self # def on_mouseup proc do |event| event.type = :mouse_up send_event event true end end end end