Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Component

Stathis Sideris edited this page · 15 revisions
Clone this wiki locally

Clarity Basics

Rationale

You can do a lot with Swing, but achieving even the basics requires you to write some pretty tedious code. Clarity aims to simplify the syntax for achieving the common tasks of GUI programming, and to make Swing feel much more "clojury". Also, it aims to be more than just a wrapper around Swing, taking the opportunity to add extra functionality for tasks that we now take for granted, but that were perhaps uncommon when Swing was first conceived.

Features overview

  • Simplified syntax for constructing components and setting their attributes.
  • Ability to attach identifiers and categories to components for querying and styling.
  • Very easy creation of event listeners.
  • Easy and unified access to the contents of components (values), both for retrieving and setting. This applies to hierarcies of components too.
  • Easy form/dialog creation.
  • More clojury handling of colors and fonts.
  • CSS-like styling.
  • Lazy sequence for applying operations on hierarchies of components (e.g. disable all children of panel).
  • Advanced CSS-like selectors for iterating over components that match certain criteria.
  • Development tools for interactive development.

Roadmap

  • Support for models backing lists, tables and trees.
  • Form validation facilities.

Making components

The core of Clarity is the make macro from the component namespace. It provides the basis for creating GUI components. For example:

   (ns example.core
    (use [clarity.component :as c]))

   (make :button "The Button")

This creates a JButton. The :button keyword is translated to javax.swing.JButton, and "The Button" is passed to the constructor of the JButton. Instead of a keyword, you can also pass a class name to make and it will contruct an instance of the class, even if it's not part of Swing. For more details about how keywords are translated to class names, and for an important point about constructor parameters, see the documentation of the make macro.

The make macro allows you to attach extra information to Swing components, such as an identifier and categories. Both those pieces of information are useful for getting and setting the values of the components, for styling, and for queying the component hierarchy through selectors (more on this later). Categories are very similar to CSS classes. For example (assume that the clarity.component namespace has been aliased to c):

   > (def b
       (c/make :button "The Button"
               (:id :the-button)
               (:categories :important :simple-button)))
   > (c/id b)
   :the-button

   > (c/categories b)
   #{:simple-button :important}

   > (dosync (c/add-category b :test))
   #{:simple-button :test :important}

As you can see, the categories are held in a ref which contains a set. The ID is currently immutable (although this might change). The ID and the categories don't have to be keywords, but it's recommended.

The other bit of syntactic sugar that make introduces concerns setters. Basically, any forms that are not the special forms for setting the id and categories are interpreted as calls on the constructed object, as if the object and the forms were enclosed in a (doto). If the form starts with a keyword, it is translated to a call of a setter method. For example:

   (c/make :button "The Button"
       (:border nil)
       (:focus-painted false))

...is equivalent to:

   (doto (javax.swing.JButton. "The button")
     (.setBorder nil)
     (.setFocusPainted false))

...(although it does not expand to that exactly). Note that this is bound by make to the constructed component and can be used in the setter calls.

Again, the syntactic syntax for setters is optional, and it's there to make things look a bit more declarative. This would have worked as well:

   (c/make :button "The Button"
       (.setBorder nil)
       (.setFocusPainted false))

A more useful aspect of the make macro is how it helps with event handling. Try this in your REPL:

   > (use 'clarity.dev)
   > (show-comp
       (c/make :button "hello"
               (:on-mouse-entered (.setText this "over!"))
               (:on-mouse-exited (.setText this "out!"))))

Clarity uses the forms starting with :on- keywords to create listeners and attach them to the component. In this particular case, the keywords match methods in the java.awt.event.MouseListener interface. An instance of this interface is constructed, the matching methods are implemented using the code provided, and the listener is assigned to the component. Please note that only one instance of the interface is constructed, the forms that correspond to the same interface are grouped together. Again, this is available to the listener code.

Generally when the make macro is being expanded, Clarity uses the special forms that start with :on- keywords to decide what listeners to create and attach to the component. This is achieved by looking up maps in the clarity.event namespace. If there are listeners with methods that match the names of the keywords, then the listeners are created and attached to the component, and the passed forms are used as the bodies of the methods of the listeners. Listeners from the javax.swing.event and the java.awt.event packages are supported.

In this example, we also use the very useful show-comp function from the dev namespace to quickly see the button in its own frame.

The value of things

A very common and tedious GUI programming task is to collect the various bits of data from the fields, especially for windows that are used to edit a lot of data. Almost invariably, once you are done with the bit of code that collects the values, you then have to do the same amount of work again for the other direction, namely code to set all the values of all the fields in the form.

Clarity tries to unify this process by defining the HasValue protocol in the component namespace. The protocol itself is very simple, its two methods are value and set-value, one to get the value and the other to set it respectively. Implementations of this interface are offered for various standard Swing components like JTextField, JCombobox, JCheckBox etc, but it's trivial to write your own. Here are some examples of the usage:

   > (def t (c/make :text-field))
   > (c/value t)
   ""
   > (c/set-value t "TEST")
   nil
   > (c/value t)
   "TEST"
   > (.getText t)
   "TEST"

   > (def c (c/make :check-box))
   > (c/value t)
   "TEST"
   > (c/value c)
   false
   > (c/set-value c true)
   nil
   > (c/value c)
   true
   > (.isSelected c)
   true

If you are thinking that this is kind of neat but not very impressive, then you're right. The real benefit on this approach shows when you have to handle hierarchies of components. The HasValue protocol is implemented for the java.awt.Container class to facilitate this. Let's see an example (please ignore the fact that there is no layout manager in this example):

   > (def panel
         (c/make :panel
           (.add (c/make :text-field "Tom" (:id :name)))
           (.add (c/make :text-field "Waits" (:id :surname)))
           (.add (c/make :text-field "Musician" (:id :job)))))
   > (c/value panel)
   {:name "Tom", :surname "Waits", :job "Musician"}

   > (c/set-value panel (assoc (c/value panel) :job "Legend"))
   > (c/value panel)
   {:name "Tom", :surname "Waits", :job "Legend"}

The implementation of HasValue for java.awt.Container is such, so that it recursively calls value on each child element that has an ID, and then collects the returned values into a map with keys that are the same as the ID of each element. And that's why in Clarity it's preferable to use keywords as the IDs of components. Setting the value works in pretty much the same way: you pass a map to set-value while calling it on a container, and the descendants of the container that match each key are detected and their values are set to the corresponding values from the map.

This ties in very nicely with the forms system of Clarity.

There is also a HasSelection protocol which follows the same logic but this is still under development.

do-component

Most of the functionality that is available through the make macro is also covered by the do-component (in fact, make uses do-component internally). The difference is that the do-component can be used to modify an existing component. You can think of do-component a bit like Clarity's version of doto. For example:

   > (def button (c/make :button))
   > (c/do-component button
        (:border nil)
            (:focus-painted false)
            (:on-mouse-entered (.setText this "over!"))
            (:on-mouse-exited (.setText this "out!")))

The only two things that you cannot do with the do-component macro is set the ID of the component (because it is immutable -- at least for now), and set its categories.

You now know the basics

This concludes the introduction to Clarity--you now know the basics. Please make sure to check the other sections of the documentation for the rest of the Clarity features! (still being written)

Something went wrong with that request. Please try again.