Friday, August 21, 2020

How To Write a Glimmer Timer Desktop App in One Hour

I needed a simple Timer desktop app on my Mac today, so I wrote one in Glimmer. The initial working prototype took me about one hour, mostly spending my time outside of Glimmer to figure out how to leverage the Java Sound libraries (since Glimmer works through JRuby). Glimmer GUI code might have taken 10 minutes only or less. Otherwise, I spent 2-3 hours afterwards on fit and finish (e.g. logo and fonts) and testing/packaging for the Mac and Windows.

Here are screenshots of it running on Mac, Windows, and Linux.

Glimmer Scaffolding

The structure of the app was generated automatically by scaffolding as a Custom Shell Gem (i.e. custom reusable window) via this command:

glimmer scaffold:gem:cs[timer]

If you do not need a gem, then simply scaffold an app with this command instead:

glimmer scaffold[timer]
Otherwise, if you want a reusable gem too, then keep in mind that you would have to add a namespace (only Glimmer official gems do not need a namespace). For example:
glimmer scaffold:gem:cs[timer,AcmeInc]

Glimmer GUI

Below is the gist of the GUI code, which is partly generated by scaffolding and written with the Glimmer GUI DSL.

It consists of:

  • A shell (window) widget, titled "Glimmer - Timer"
  • A group widget, titled "Countdown"
  • A couple of spinner widgets for minutes and seconds separated by a label widget with text ":"
  • "Start" and "Stop" button widgets (which rely on & mnemonics to automatically provide keyboard shortcuts)
  • A couple of composite widgets with layouts to manage GUI widget organization
  • A scaffolded menu bar widget that simply displays the About dialog when requesting Preferences... (a placeholder for adding preferences in the future if needed)

Glimmer Data-Binding

The timer logic is wired to the view through Glimmer Data-Binding of self attributes (which could be refactored/extracted to a model in the future). For example:

selection bind(self, :min)
This data-binds the selection of the @min_spinner widget to the :min attribute on self (i.e. minutes).
selection bind(self, :sec)
This data-binds the selection of the @sec_spinner widget to the :sec attribute on self (i.e. seconds).
enabled bind(self, :countdown, on_read: :!)
This data-binds the enablement of the @start_button to the :countdown attribute (indicating if a countdown is in progress). The `on_read :!` option invokes the :! method (as in not) as a data-binding converter on read of the :countdown attribute to negate it and ensure the @start_button is enabled only when the countdown is NOT in progress. Notice how the @stop_button does not have the on_read data-binding converter since it needs to be enabled during countdown.

Glimmer Observers

Countdown is started and stopped through Glimmer Observers attached to the Start and Stop buttons (on_widget_selected for mouse click selection and on_key_pressed for keyboard triggered selection via the ENTER key).

Once the button observers are fired through a mouse click or hitting the ENTER key, they invoke event methods (which could be refactored/extracted to a model in the future). The last one (play_countdown_done_sound) is triggered by the timer logic itself (described next) when done with the countdown, which plays a sound using the Java Sound libraries.

Timer Logic

The timer logic code below relies on the attributes :countdown, :min, and :sec

  • countdown: indicates if a countdown has been started and is active
  • min: the countdown remaining minutes
  • sec: the countdown remaining seconds
Additionally, Glimmer Custom Widget/Shell hooks are used:
  • before_body hook: since it runs before rendering the GUI body, it is used to pre-initialize the :min and :sec attributes (in addition to initializing the GUI display, which is auto-generated code from Glimmer Scaffolding)
  • after_body hook: since it runs after rendering the GUI body, it is used to update the timer regularly via a separate OS thread (thanks to JRuby)
The timer thread logic loops indefinitely, sleeping for 1 second every loop and then checking if a @countdown is started (true or false). Based on the @countdown value, it either updates the timer minutes and seconds (by subtracting 1 second using a Ruby makeshift Time object for time arithmetic) or otherwise does nothing until it sleeps again the next loop.


One thing important to note when running multi-threaded code is you may only interact with the Glimmer GUI safely via a `sync_exec` or `async_exec` code block. That is because Glimmer GUI runs on the GUI thread via the SWT main event loop, which only processes one GUI event at a time by design. As such, if you execute code in another thread, you may only update the GUI by queuing up your code in a block to either run as soon as possible (using `sync_exec`) or asynchronously after all other queued GUI events are done processing (using `async_exec`).

Entire View Code:

To see all the pieces working together, here is the entire view code including everything mentioned (attributes for data-binding, before_body hook, after_body hook, GUI body, and event methods triggered via observers):

Happy Glimmering!


RDM said...


file_path = jar_file_path.sub(/^uri\:classloader\:/, '').sub('//', '/')

how did you know to do like this?

Andy Maleh said...

I knew from past Java experience and some troubleshooting. This is only needed when loading a file (e.g. .wav file) from inside a JAR-packaged project, not when running a raw project from the command line. I actually built similar logic automatically into Glimmer for reading image files when setting on widgets (e.g. label {image image_path}) so people don't have to worry about it with images. I have been planning to provide a utility method for properly reading files from JARs in general to avoid having to perform that logic manually (for files other than images), perhaps also contributing to JRuby, but I haven't gotten around to it. In any case, I just added it as a task to the file of the glimmer-dsl-swt project. Thanks for asking. Let me know if you have further questions, or simply post them on the Glimmer Gitter since I check it daily: