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.
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:
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)
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.
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.
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
- 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)
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):