Reactive Swing and Observables Pt 5
tom, 2012-02-14

My last post discussed a library for Observers in Clojure that allowed for first-class, composable events (inspired by F#). With that basis, we set our sights on extending our first-class events to Swing, or rather, elevating the hidden events from Swing’s listener methods into first-class observable streams of data.

This provides default implementations for common events in Java (or the native system we’re operating on). Right now, I tried to pull out common methods from Swing and AWT that would be useful.

Our goal here is to provide functions that can map a Java component into one or more observables. Additionally, we want to de-encapsulate the actual event data, which is hidden by default inside of Java’s methods, so that we can treat event instances as abstract sequences. To facilitate this, wen;also build in default functionality to treat “most” java events as generic IEvents, from the cljgui.events.base namespace. We retain all of the domain specific data from each Java Event class, and package it as a map accessible via (event-data [e]) from IEvent.

To wrap Swing/AWT events in a generic fashion, I went ahead and defined an IEvent protocol in a cljgui.events.base namespace:

(ns cljgui.events.base)

(defprotocol IEvent 
  (event-source [e] "Derive the event source of event e.")
  (event-time [e] "Report the timestamp for event e.")
  (event-data [e] "Return (usually a map) of data associated with event e.")
  (event-id [e] "Return a (usually numeric) ID associated with event e.")
  (event-type [e] "Return (usually string) event-type associated with event e.")
  (event-from [e] "Return the originating source of event e, usually numeric." )
  (event-to [e] "Return the intended destination for event e, if any."))

(defrecord Event [id evtype t from to data]
  IEvent
          (event-source [e] from)
          (event-time [e] t)
          (event-data [e] data)
    (event-type [e] evtype)
          (event-id [e] id)
          (event-from [e] from)
          (event-to [e] to))

(def empty-event (Event. -1 :none -1 :anonymous :global nil))
(defn make-event 
  [id evtype & {:keys [t from to data] 
                 :or {t 0 from :anonymous to :global data nil} 
                 :as opts}]
    (Event. id evtype t from to data))

With the generic protocol in place, we need to begin integrating Swing and AWT classes, and provide a nice API in the process. Starting with imports and uses. Normally, I don’t recommend 'use, but in this case, we want direct references to the base and observe namespaces. We’re going to be doing some code-generation based off of java classes (since I don’t feel like typing…..) via macros, so we pull in Stu Halloway’s excellent reflection library, clojure.reflect.

(ns cljgui.events.native
  (:use [cljgui.events.base]
        [cljgui.events.observe])
  (require [clojure [reflect :as r]
                    [string :as s]])
  (:import  
    [java.util EventListener EventObject]
    [java.awt.event 
     ActionEvent ActionListener 
     ComponentEvent ComponentListener
     AdjustmentEvent AdjustmentListener
     FocusEvent FocusListener 
     ContainerEvent ContainerListener
     HierarchyEvent HierarchyListener
     KeyEvent KeyListener KeyAdapter
     ItemEvent ItemListener
     MouseEvent MouseListener MouseMotionListener MouseWheelListener MouseAdapter 
     TextEvent TextListener
     WindowEvent WindowFocusListener WindowListener 
     ]
     [javax.swing.event 

      ChangeEvent ChangeListener
      DocumentEvent DocumentListener 

       ]))

Wrapping

The first step in our framework is to get events into a consistent data representation, IEvent. We can do this by extending the IEvent protocol to each event class. However, there are a bunch of event classes. That means a bunch disparate domain specific data associated with each event. Rather than hard-code all of the extensions, I opted to use reflection to sort out the public properties (i.e. data) of each event, and to pack that into a flat map accessible via the IEvent/event-data protocol function. The tricky part is writing a single, unifying function to query a given event class for its data. Since it’ll be unique to each class, our extension of the IEvent protocol will be unique for each class as well. I use a combination of reflection functions and macros to look at each event class in Swing and AWT to determine its event data, build a single accessor function to wrap the public accessor methods for the event class into a corresponding Clojure map, and use macros to automatically handle all the protocol extension boilerplate for us. The end result is a simple process for wrapping event classes, extending IEvent to them, and still having access to their unique domain-specific data via the IEvent/event-data function.

We tie into the default methods guaranteed by the java Event base class.

(def default-event
  {:event-source (fn [e] (.getSource e))
   :event-time (fn [e] 0) ;no default notion of time.
   :event-data (fn [e] (.getActionCommand e))
   :event-type (fn [e] (.getID e)) :event-from (fn [e] (.getSource e))
   :event-to (fn [e] globalevent)})

(defn- get-properties
  "return a function that reflects on a class and returns all
   (getXXXX) methods and all (isXXX) methods..."
  [klass]
  (let [zero-args? (fn [r] (= 0 (count (:parameter-types r))))
        has-args? (fn [r] (contains? r :parameter-types))
        properties (filter
                      #(and 
                        (has-args? %)
                        (zero-args? %)
                        (let [nm (str (:name %))] 
                          (and (> (count nm) 3)
                             (or
                               (= (subs nm 0 2) "is")
                               (= (subs nm 0 3) "get"))))) 
                      (:members (r/reflect klass)))]
    properties)) 

(defn- cut-string [instr tobecut]
  (.replace instr tobecut ""))

(defn- get-propertymap
  "generate a list of key-val pairs, where keys are keyworded method
   names without 'get' or 'is', and vals are anonymous functions that
   apply the get or is method to an instance of the class"
  [names]
  (let [methodcall  (fn [pname] 
                      (let [n (str "." pname)]
                       (list  (symbol n) 'k)))
        get-key (fn [pname]
                  (let [cut (if (= (subs pname 0 2) "is")
                              "is" "get")]
                    (keyword (cut-string pname cut))))
        property-map (fn [pname]
                       (let [k (get-key pname)]
                         (list k (methodcall pname))))]
    (cons 'hash-map (mapcat property-map names))))

Tie together the property map into a single anonymous function. Eval it to turn the symbol list into an actual function.

(defn getmethods [klass]
  (let [ps (map (comp str :name) (get-properties klass))]
    (eval (list 'fn '[k] (get-propertymap ps)))))

Use a macro to easily define event wrappers for a Java XXXEvent class, along with replacement methods (or even additional methods) to add to the property map.

(defmacro wrap-events [awtclass methodmap]
  `(let [base# (merge default-event ~methodmap)] 
    (extend ~awtclass IEvent
      (merge base# {:event-data (getmethods ~awtclass)}))))

Extend the ability to wrap MANY java events…

(defmacro wrap-many [& [classpairs]]
  (let [cp (for [[k p] classpairs] ;(fn [[[k p] _]] 
                 (list 'wrap-events k p))]
   `(do (list 
           ~@cp))))n</pre>

Wrap an assload of event types, so we can use generic IEvent ops on them in observable combinators. I oroginally had ALL events from Swing and AWT, but I narrowed it down to these. Currently, I’m only really using/listening for Mouse/Key events, but I plan to extend as needed.

(wrap-many
         [[ActionEvent {:event-time (fn [e] (.getWhen e))}]
          [ComponentEvent {}]
          [AdjustmentEvent {}]
          [ContainerEvent {}]
          [FocusEvent {}]
          [HierarchyEvent {}] 
    [KeyEvent {}]
          [ItemEvent {}]
          [MouseEvent {}]
          [TextEvent {}]
          [WindowEvent {}]])

          ;[InputEvent {}]
          ;[PaintEvent {}]

Attaching

Now we have a way of feeding an XXXEvent class and “wrapping” it into an IEvent, x, where the underlying data is exposed as a simple map in the IEvent, accessible via (event-data x).

What about creating observables that interface with GUI components? Components are responsible for registering subscribers themselves, which is handled via the .addXXXListener method on the instance of a component. We’d like to abstract this plumbing away from client code. Ideally, we’d just have a client apply a function to a GUI component and get some identifiable event streams back, which they could compose using observable combinators.

We now define a library for easily converting GUI components into maps of observed IEvent streams.

(defn- ignore [_] nil)

(defn get-mouse
  "Return an anonymous proxy for MouseAdapter that uses function
   handlers for each event in args.  If no handlers are supplied, the
   MouseAdapter will not respond to events."
  [{:keys [clicked dragged entered exited moved pressed
                          released wheelmoved]}]
  (proxy [MouseAdapter] [] 
    (mouseClicked [e] (clicked e))
    (mouseDragged [e] (dragged e))
    (mouseEntered [e] (entered e))
    (mouseExited [e] (exited e))
    (mouseMoved [e] (moved e))
    (mousePressed [e] (pressed e))
    (mouseReleased [e] (released e))
    (mouseWheelMoved [e] (wheelmoved e))))

(defn get-keys
  "Return an anonymous proxy for KeyAdapter that uses function
   handlers for each event in args.  If no handlers are supplied, the
   KeyAdapter will not respond to events."
  [{:keys [pressed released  typed]}]
  (proxy [KeyAdapter] [] 
    (keyPressed [e] (pressed e))
    (keyReleased [e] (released e))
    (keyTyped [e] (typed e))))

(defn get-eventroute
  "Return a map of {ename [event fobservation]}, where fobservation is
   an anonymous function that routes an argument to the notify! of an
   anonymous observer.  The effect is, given an eventname, we return a
   map of the eventname and a function that wraps an observable.  The
   purpose here is to facilitate wrapping of Java events, to provide
   observables amenable to combinators in the observe library."
  [ename]
  (let [newevent (make-observable)] 
   {ename [newevent 
           (fn [e] (notify! newevent e))]}))

(defn get-events [eventroutes]
  (zipmap (keys eventroutes) (for [[event obs] (vals eventroutes)] event)))

(defn get-routes [eventroutes]
  (zipmap (keys eventroutes) (for [[event obs] (vals eventroutes)] obs)))

(defn- make-alistener [f]
  (reify ActionListener
    (^void actionPerformed [this ^ActionEvent e] (f e))))

The last three functions will probably be the primary functions used by callers. They allow us to get events for observing actions (use by timers, buttons, all sorts of things), mouse (mousemovement, clicks, wheel scrolling), and keyboard input (keypressed, released, etc.) All the caller has to do is apply the function against a component that can receive the underlying events. A hidden Listener is created as an event router, while the return value is a map of eventnames to observable events. It becomes very easy to destructure the returned map to compose events, and ultimately define GUIs.

(defn action-observer
  "Defines a simple action observer for a Java component.  The
   component must have the capacity to add an ActionListener.  We
   return the result of wrapping the ActionListner's actionPerformed
   event in an observable.  Thus, we can observe a stream of
   ActionEvents (generated by the unseen actionListener) by
   interacting with the returned observable.  This allows us to turn
   action events into generic observables, rather than hiding the
   event data behind method implementations."
  [target] 
  (let [eroute (get-eventroute :actionPerformed)
        route (first (vals (get-routes eroute)))
        observable (first (vals (get-events eroute)))]
    (do (.addActionListener target 
          (make-alistener (fn [^ActionEvent e] (notify! observable e))))
      {:actionPerformed observable})))

(defn mouse-observer
  "Defines a simple mouse observer for a Java component.  The
   component must have the capacity to add a MouseListener.  We return
   the result of wrapping the MouseListener's events in several
   observables.  Thus, we can observe a stream of MouseEvents
   (generated by the unseen MouseListener) by interacting with the
   returned observable.  This allows us to turn Mouse Events into
   generic observables."
  [target]
  (let [eventroutes 
        (reduce merge {}
                (map get-eventroute [:clicked :dragged :entered :exited :moved 
                                     :pressed :released :wheelmoved]))
        adapter (get-mouse (get-routes eventroutes))]
  (do 
    (.addMouseListener target adapter)
    (.addMouseMotionListener target adapter)
    (.addMouseWheelListener target adapter)
    (get-events eventroutes))))

(defn key-observer
  "Defines a simple key observer for a Java component.  The component
   must have the capacity to add a KeyListener.  We return the result
   of wrapping the KeyListener's events in several observables.  Thus,
   we can observe a stream of KeyEvents (generated by the unseen
   KeyListener) by interacting with the returned observable.  This
   allows us to turn Key Events into generic observeables."
  [target]
  (let [eventroutes 
        (reduce merge {}
                (map get-eventroute [:pressed :released :typed]))]
  (do 
    (.addKeyListener target (get-keys (get-routes eventroutes)))
    (get-events eventroutes))))

The next post will show an example of event composition in a simple Swing app (the first piece of our original graph-path visualization app!).