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, we also build in default functionality to treat “most”
java events as generic IEvent
s, 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!).