Swing and Roundabouts 2: Inside Action
Prequels
"To err is human, but to really foul things up you need a computer." Paul Ehrlich
See Event DTs,
Turn Tables (formerly known as
Bean Curd 1), and Panel Beater.
align=left hspace=16 />
Introduction
"The best accelerator available for a slow computer is gravity outside the window."
Swing actions are used for buttons, menu items and keystrokes.
For example, a "Save" action event might be initiated by the user
using a menu item, button or keystroke eg. Ctrl-S.
We need to configure actions centrally, so that consistent labels, icons,
mnemonics and keystrokes are associated with a given action, such as "Save."
And also so that we can enable and disable an action, without having to attend
to every associated button, menu item and keystroke.
So we build an "action framework." Why? Because it's fun, it's useful, and it's legal, so it's a must!
Our action framework is copied from greenscreen, but
we will try to improve upon that design here, in particular simplify it, so that
anyone can reimplement it from scratch in a few days, or paste and hack it
into their project.
Since i redesigned and reimplemented greenscreen
some months ago, it's about time to redo that again. So i'm writing this article to present and improve upon that design.
So this is an exercise in "design by explanation." Because if it's not easy to explain, then it's not simple enough.
Uncle Vinnie's Application
"Programming today is a race between software engineers striving to build bigger and
better idiot-proof programs, and the Universe trying to produce bigger and better idiots."
Rich Cook.
align=right hspace=16 />
Our boss, Uncle Vinnie (from Chicago) wants to modernise our operation. Cos we going
legit. And being a legit business, you gotta have these computers and
photocopies and fax machines and shredders and stuff like that, you know,
to make it look legit.
So we're in the olive oil business. Like everyone else in the neighbourhood.
We got different products, like Extra virgin, and some other types and brands.
We gonna build a database front-end application to save our products, and
stock quantities and such. Vinnie said we'll leave the supplier and customer
records for Phase N. As in, No time soon, probably Never. In the meantime
Vinnie uses that little black notebook in his pocket for those. Old habits die hard.
For our application, we prefix class names with Vinnie.
<b>public</b> <b>class</b> VinnieProductWorksheet <b>implements</b> ActionListener {
<b>protected</b> VinnieWorksheetContext context = VinnieWorksheetContext.createContext(<b>this</b>);
@ActionAnnotation(tooltip = "New product")
GAction newAction = context.createAction();
@ActionAnnotation(tooltip = "Find product", label = "Find",
keystroke = "control F", mnemonic = 'F', icon = "search.png")
GAction findByNameAction = context.createAction();
@ActionAnnotation(tooltip = "Delete product")
GAction deleteAction = context.createAction();
@ActionAnnotation(tooltip = "Save product")
GAction saveAction = context.createAction();
@ActionAnnotation(tooltip = "Exit worksheet")
GAction exitAction = context.createAction();
...
<b>public</b> VinnieProductWorksheet() {
context.configure();
configure();
setEnabled();
addToolBar(newAction, findByNameAction, deleteAction, saveAction, exitAction);
...
}
<b>protected</b> <b>void</b> configure() {
exitAction.setToolTip("Exit worksheet"); // override default
deleteAction.setKeyStoke(<b>null</b>); // remove default keystroke
...
}
<b>public</b> <b>void</b> actionPerformed(ActionEvent event) {
context.eventLogger.entering(event);
<b>if</b> (newAction.isSource(event)) {
...
}
...
setEnabled();
}
<b>protected</b> <b>void</b> setEnabled() {
saveAction.setEnabled(productModel.isChanged());
deleteAction.setEnabled(!productModel.isEmpty());
}
<b>protected</b> <b>void</b> addToolBar(GAction ... actions) {
<b>for</b> (GAction action : actions) {
JButton button = <b>new</b> JButton();
button.setAction(action);
toolBar.add(button);
}
}
...
}
align=right hspace=16 />
Rather than using "dependency injection," we use an approach of exposing all our
dependencies via a context object. If you don't like that, then think of it as a
shorthand notation to indicate where dependency injection is required. Capice?
In the above code, we have used annotations to configure our actions, and also
more direct methods, eg. invoking setToolTip() et al in the configure() method.
If you don't like this trick with using annotations for configuration, think of it
as a shorthand notation to indicate which setters you gonna invoke in the configure() method.
Of course, these settings should be externalised, eg. in a resource bundle, for translation
and customisation. But Uncle Vinnie says its OK to hardcode defaults here for rapid prototyping.
Action Configuration Object
"No computer has ever been designed that is ever aware of what it's doing;
but most of the time, users aren't either."
Firstly, let's introduce an action configuration object as follows.
@XmlRootElement(name="action")
<b>public</b> <b>class</b> ActionConfiguration {
@XmlAttribute <b>protected</b> Class worksheetClass;
// eg. com.mafia.chicago.vinnie.productworksheet.VinnieProductWorksheet
@XmlAttribute <b>protected</b> String actionCommand; // eg. "save"
@XmlAttribute <b>protected</b> String keyStroke; // eg. "control S"
@XmlAttribute <b>protected</b> Character mnemonic; // eg. 'S'
@XmlAttribute <b>protected</b> String iconName; // eg. disk.png
@XmlAttribute <b>protected</b> String label; // "Save"
@XmlAttribute <b>protected</b> String toolTip;
@XmlAttribute <b>protected</b> Integer ordial;
... // getters and setters
... // configure(ActionConfiguration), to overwrite with non-null properties from another instance
... // cloneActionConfiguration()
}
We will configure common actions centrally, eg. label, tooltip, keystroke et al,
using this class. Also, we will externalise our action configuration by serialising
a list of instances of this class. We might use JAXB to persist our action configuration
to an XML file. In which case, we can just add JAXB2 annotations into the above
class, and Vinnie's your Uncle, woohoo!
Common actions like saveAction would have an null worksheetClass.
If a property is not specified, then it will be null, which indicates
we should use the default for that action, eg. from the common action configuration.
In the case of common actions, our worksheet's actions typically only override the toolTip
eg. "Save product" for the saveAction of VinnieProductWorksheet.
Our Own Actions
"The most reliable components are the ones you leave out." Gordon Bell
Swing has an AbstractAction class with an abstract actionPerformed() method.
So we gonna extend this class.
AbstractAction has a Map for its configuration, with keys Action.ACTION_COMMAND_KEY,
NAME and SHORT_DESCRIPTION. I was tryna
explain to Uncle Vinnie that it might look better to use fields rather than this map. He said, no problem.
<b>public</b> <b>class</b> GAction <b>extends</b> AbstractAction {
<b>protected</b> GContext context;
<b>protected</b> String actionCommand;
<b>protected</b> String keyStroke;
<b>protected</b> char mnemonic;
<b>protected</b> String iconName;
<b>protected</b> String label;
<b>protected</b> String toolTip;
<b>public</b> GAction(GContext context) {
<b>super</b>();
<b>this</b>.context = context;
}
<b>public</b> <b>void</b> setActionCommand(String actionCommand) {
<b>this</b>.actionCommand = actionCommand;
<b>super</b>.putValue(Action.ACTION_COMMAND_KEY, actionCommand);
}
<b>public</b> <b>void</b> setLabel(String label) {
<b>this</b>.label = label;
<b>super</b>.putValue(Action.NAME, label);
}
<b>public</b> String getLabel() {
<b>return</b> label;
}
<b>public</b> <b>void</b> setToolTip(String toolTip) {
<b>this</b>.toolTip = toolTip;
<b>super</b>.putValue(Action.SHORT_DESCRIPTION, toolTip);
}
... // other getters and setters
<b>public</b> <b>void</b> configure(ActionConfiguration configuration) {
setActionCommand(configuration.getActionCommand());
setLabel(configuration.getLabel());
setKeyStroke(configuration.getKeyStroke());
... // other properties
}
<b>public</b> <b>void</b> putKeyStrokeAncestor(JComponent component) {
putKeyStroke(component, JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
}
<b>public</b> <b>void</b> putKeyStrokeInWindow(JComponent component) {
putKeyStroke(component, JComponent.WHEN_IN_FOCUSED_WINDOW);
}
<b>public</b> <b>void</b> putKeyStroke(JComponent component) {
putKeyStroke(component, JComponent.WHEN_FOCUSED);
}
/**
* @see JComponent#WHEN_FOCUSED
* @see JComponent#WHEN_IN_FOCUSED_WINDOW
* @see JComponent#WHEN_ANCESTOR_OF_FOCUSED_COMPONENT
*/
<b>public</b> <b>void</b> putKeyStroke(JComponent component, int inputMapId) {
component.getActionMap().put(actionCommand, <b>this</b>);
component.getInputMap(inputMapId).put(KeyStroke.getKeyStroke(keyStroke), actionCommand);
}
<b>public</b> <b>void</b> actionPerformed(ActionEvent event) {
context.eventHelper.actionPerformed(event);
}
}
align=left hspace=16 vspace="8" />
The actionPerformed() method delegates to an event helper dependency.
Also we introduce some methods for handling keystrokes, because i can never
remember the InputMap and ActionMap stuff.
We implement a configure() method to configure an action using an ActionConfiguration.
Event helper
"The price of reliability is the pursuit of the utmost simplicity.
It is a price which the very rich find most hard to pay." Edsger Dijkstra
Our event helper is just an event dispatcher to our controller. In effect, it
does "automatic event listener registration" by inspecting the controller,
In particular it checks if the controller implements the relevant listener interface
eg. ActionListener, ie. handles a given event. If so, it forwards the
event to the controller, eg. invokes actionPerformed(ActionEvent).
<b>public</b> <b>class</b> GEventHelper <b>implements</b> FocusListener, ActionListener, WindowListener {
<b>protected</b> GContext context;
<b>protected</b> Object controller = <b>null</b>;
<b>public</b> GEventHelper(GContext context) {
<b>this</b>.context = context;
}
<b>public</b> <b>void</b> setController(Object controller) {
<b>this</b>.controller = controller;
}
<b>public</b> <b>void</b> actionPerformed(ActionEvent event) {
beforeEvent();
context.actionLogger.entering(event.getSource());
<b>try</b> {
<b>if</b> (controller instanceof ActionListener) {
ActionListener actionListener = (ActionListener) controller;
actionListener.actionPerformed(event);
}
} <b>catch</b> (Throwable e) {
context.exceptionHelper.severe(e);
}
afterEvent();
}
... // other listeners
... // beforeEvent(), afterEvent() overridable aspects
}
We might extend this class as VinnieEventHelper to override methods eg.
the beforeEvent() amd afterEvent() aspects. Then we need to configure
our context to use this customised event helper.
align=left hspace=16 />
Basic context
"The first 90% of the code accounts for the first 90% of the development time.
The remaining 10% of the code accounts for the other 90% of the development time." Tom Cargill
As mentioned earlier, we get into an awful mess by using "customised cloned contexts"
rather than IoC dependency injection.
Firstly we have our "framework context" class, which
exposes all dependencies required by our framework.
<b>public</b> <b>void</b> GContext {
<b>public</b> <b>static</b> GContext frameworkContext = <b>new</b> GContext();
<b>public</b> <b>static</b> GLogger eventLogger = <b>new</b> GLogger(<b>this</b>, "eventLogger");
// logger verbosity set via system properties eg. -DeventLogger.level=FINEST
// eg. setLevel(Level.parse(System.getProperty(loggerName + ".level", "INFO")));
<b>public</b> <b>static</b> GFormatter formatter = <b>new</b> GFormatter(<b>this</b>);
<b>public</b> GEventHelper eventHelper = <b>new</b> GEventHelper(<b>this</b>);
...
<b>protected</b> GContext() {
}
<b>public</b> <b>static</b> GContext getInstance() {
<b>return</b> frameworkContext;
}
...
}
align=right hspace=16 vspace="32 "/>
We don't want null pointer exceptions, so we instantiate references such as eventHelper,
with default implementations.
Unfortunately we have to take great care when constructing the above context object. For example, if GLogger
references something on this halfbaked context that we pass to its constructor,
eg. formatter, then we get a null pointer exception when we construct eventLogger, because
formatter is created after eventLogger.
Worksheet context
"When a programming language is created that allows programmers to program in simple English,
it will be discovered that programmers cannot speak English."
For event handling, we extend this to include a reference to our worksheet object, eg. VinnieProductWorksheet.
<b>public</b> <b>void</b> GWorksheetContext <b>extends</b> GContext {
<b>public</b> <b>static</b> GActionConfigurator actionConfigurator;
<b>protected</b> Object worksheet = <b>null</b>;
<b>public</b> GWorksheetContext() {
<b>super</b>.eventHelper = <b>new</b> GEventHelper(<b>this</b>);
}
<b>public</b> <b>void</b> setWorksheet(Object worksheet) {
<b>this</b>.worksheet = worksheet;
<b>super</b>.eventHelper.setController(worksheet);
}
<b>public</b> <b>void</b> configure() {
... // configure actions using actionConfigurator
<b>for</b> (Field field : worksheet.getFields()) {
... // configure components in the worksheet
}
}
...
}
When the worksheet is set, we pass this on to the event helper as the controller.
We will introduce our GActionConfigurator later. This is used to
configure the common actions of our application in a central, externalisable way.
We do this so that our action properties are customisable and translatable.
Custom cloned contexts
"The computer is a stupid machine with the ability to do incredibly smart things,
while computer programmers are smart people with the ability to do incredibly stupid things.
They are, in short, a perfect match." Bill Bryson
We then extend our framework worksheet context for our application, as follows.
<b>public</b> <b>void</b> VinnieWorksheetContext <b>extends</b> GWorksheetContext {
<b>public</b> <b>static</b>
VinnieWorksheetContext applicationContext = <b>new</b> VinnieWorksheetContext();
...
<b>protected</b> VinnieWorksheetContext() {
<b>super</b>();
<b>super</b>.formatter = <b>new</b> VinnieFormatter(<b>this</b>);
}
<b>public</b> <b>static</b> VinnieWorksheetContext createWorksheetContext(Object worksheet) {
VinnieWorksheetContext worksheetContext = applicationContext.cloneContext();
worksheetContext.eventHelper = <b>new</b> VinnieEventHelper(<b>this</b>);
worksheetContext.setWorksheet(worksheet);
<b>return</b> worksheetContext;
}
<b>public</b> VinnieWorksheetContext cloneContext() {
<b>try</b> {
<b>return</b> (VinnieWorksheetContext) clone();
} <b>catch</b> (Exception e) {
<b>throw</b> <b>new</b> RuntimeException();
}
}
<b>public</b> <b>static</b> <b>void</b> main(String[] args) {
GContext.frameworkContext = applicationContext;
... // launch VinnieApplicationFrame
}
}
When we create our application context, we override some of the default dependencies
in the framework context with our customised application ones, eg. VinnieFormatter,
as in the above constructor.
When we run our application, we customise the framework context instance to the
application context instance, as in the above main() method. So when
classes invoke GContext.getInstance() they will get a reference to
our customised application context.
src="http://weblogs.java.net/blog/evanx/archive/OliveOil_bottle.jpg" width="180" height="246"
align=left hspace=16 />
When a worksheet creates a context for itself, ie. in createWorksheetContext()
above, then the application
context is cloned, and the worksheet reference is set on the cloned context, which
is now customised for that worksheet.
As i said earlier, it's nasty. But dependency handling is difficult, since there are
typically loads of dependencies. Some dependencies are customised for specific classes
eg. GEventHelper is created as a peer to a specific worksheet class instance,
eg. VinnieProductWorksheet. Others are singletons,
but also customisable for our application eg. GFormatter.
Others are both customisable for our application, and for a specific instance,
eg. GEventHelper is customised as VinnieEventHelper in our application,
and is created by the framework for each worksheet instance, eg. in the
createWorksheetContext() method above.
As nasty as our "cloned customised context" mechanism is, it is relatively simple
compared to a dependency injection framework. And we can avoid XML configurations.
Vinnie absolutely insists on this, and otherwise goes flying off the handle.
So we need to pass the relevant context through to our constructors. But we pass
one dependency only ie. this context, to expose all the rest of our dependencies.
And we need not declare all our dependencies in our client classes, only the context.
So Vinnie finds it terribly convenient. Which explains why i do it this way.
Because The Boss is always right.
Even when he's wrong... Especially when he's wrong, actually.
Central Configuration Station
"Undetectable errors are infinite in variety, in contrast to detectable errors,
which by definition are limited."
Vinnie did a stint in jail once, on a trumped up charge of knocking over a cigarette truck.
On the inside, when he wasn't pushing weights in the yard, he was in the library learning computers.
He came out looking like a
jock and talking like a geek. That's when he said we was gonna go legit, and we was gonna
get all computerised to prove it.
Vinnie says we want our common actions to be
configured centrally in one place as below, just like he learnt in prison.
<b>public</b> <b>class</b> VinnieActionConfigurator <b>extends</b> GActionConfigurator {
@ActionAnnotation(tooltip = "Save", label = "Save",
keystroke = "control S", mnemonic = 'S', icon = "disk.png")
ActionConfiguration saveAction = createActionConfiguration();
... // many other common actions, eg. new, find, delete, cancel, refresh, next, previous, exit
<b>protected</b> VinnieActionConfigurator() {
setSmallIconPackage("com.everaldo.icons.crystal.16x16");
setLargeIconPackage("com.everaldo.icons.crystal.32x32");
setDefaultIconName("misc");
configure();
}
}
Where we of course to override these defaults from an externalised configuration file,
eg. in the configure() method.
Action configurator
"UNIX is basically a simple operating system, but you have to be a genius
to understand the simplicity."
Let's look at our action configuration superclass.
<b>public</b> <b>abstract</b> <b>class</b> GActionConfigurator {
<b>protected</b> Map<String, ActionConfiguration> externalisedActionConfigurationMap = <b>null</b>;
<b>protected</b> List<ActionConfiguration> commonActionConfigurationList = <b>new</b> ArrayList();
<b>protected</b> Map<String, ActionConfiguration> commonActionConfigurationMap = <b>new</b> HashMap();
<b>protected</b> GContext context = GContext.getInstance();
<b>protected</b> String smallIconPackage = "icons.16x16";
<b>protected</b> String largeIconPackage = "icons.32x32";
<b>protected</b> String defaultIconName = "misc.png";
<b>protected</b> GActionConfigurator() {
}
<b>public</b> <b>void</b> setSmallIconPackage(String iconPackage) {
<b>this</b>.smallIconPackage = iconPackage;
}
... // setter for largeIconPackage, defaultIconName
<b>public</b> ActionConfiguration createActionConfiguration() {
ActionConfiguration actionConfiguration = <b>new</b> ActionConfiguration();
actionConfigurationList.add(actionConfiguration); // configure central action
<b>return</b> actionConfiguration;
}
<b>public</b> <b>void</b> configure() {
... // load externalisedActionConfigurationMap eg. from XML file eg. using JAXB
<b>for</b> (ActionConfiguration commonActionConfiguration : commonActionConfigurationList) {
configure(field, commonActionConfiguration);
}
}
<b>protected</b> <b>void</b> configure(Field field, ActionConfiguration commonActionConfiguration) {
String actionCommand = field.getName();
commonActionConfiguration.setActionCommand(actionCommand);
... // configure defaults from ActionAnnotation
commonActionConfiguration = coalesce(commonActionConfiguration,
externalisedActionConfigurationMap.get(actionCommand));
commonActionConfigurationMap.add(commonActionConfiguration.getActionCommand(),
commonActionConfiguration);
}
<b>protected</b> ActionConfiguration coalesce(ActionConfiguration actionConfiguration,
ActionConfiguration overridingActionConfiguration) {
<b>if</b> (actionConfiguration == <b>null</b>) <b>return</b> overridingActionConfiguration;
<b>if</b> (overridingActionConfiguration == <b>null</b>) <b>return</b> actionConfiguration;
actionConfiguration = actionConfiguration.cloneActionConfiguration();
actionConfiguration.configure(overridingActionConfiguration);
<b>return</b> actionConfiguration;
}
<b>public</b> <b>void</b> configureWorksheet(Object worksheet) {
<b>for</b> (Field field : worksheet.getClass().getFields()) {
field.setAccessible(true);
<b>if</b> (field.getType() == GAction.<b>class</b>) {
configure(field, (GAction) field.get(worksheet), worksheet);
}
}
}
<b>protected</b> <b>void</b> configure(Field field, GAction action, Object worksheet) {
String actionCommand = field.getName();
action.setActionCommand(actionCommand);
... // configure defaults from ActionAnnotation
ActionConfiguration commonActionConfiguration = commonConfigurationMap.get(action.getActionCommand());
String key = worksheet.getClass().getName() + "." + action.getActionCommand();
ActionConfiguration externalisedActionConfiguration = externalisedActionConfigurationMap.get(key);
ActionConfiguration actionConfiguration =
coalesce(commonActionConfiguration, externalisedActionConfiguration);
<b>if</b> (actionConfiguration != <b>null</b>) {
action.configure(actionConfiguration);
} <b>else</b> {
// this action is not a common one, and its configuration is not externalised (yet)
... // maybe generate the externalised XML to cut and paste into our configuration file
}
configureIcon(action, smallIconPackage);
}
<b>protected</b> <b>void</b> configureIcon(GAction action, String iconPackage) {
<b>if</b> (action.getIconName() == <b>null</b> || action.getIconName().trim().length() == 0) {
action.setIconName(defaultIconName);
}
String iconName = iconPackage + "." + action.getIconName() + ".png";
URL iconImageUrl = getClass().getResource(iconName);
<b>try</b> {
ImageIcon imageIcon = <b>new</b> ImageIcon(iconImageUrl);
action.setIcon(imageIcon);
} <b>catch</b> (Exception e) {
context.fileLogger.warning(e, iconName, iconImageUrl);
}
}
}
The configure() method gets the list of ActionConfiguration's created
in our subclass eg. VinnieActionConfigurator. It configures them
using the field name as the action command, extracting the ActionAnnotation,
and finally overriding with the externalised configuration, eg. loaded from an XML file
to which the actionConfigurationList was previously persisted, eg. using JAXB2.
When we launch our application, we might set GWorksheetContext.actionConfigurator as follows.
<b>public</b> <b>static</b> <b>void</b> main(String[] args) {
GWorksheetContext.actionConfigurator = <b>new</b> VinnieActionConfigurator();
GContext.frameworkContext = VinnieWorksheetContext.applicationContext;
... // launch VinnieApplicationFrame
}
The GWorksheetContext.configure() method will delegate to the configureWorksheet()
in the above GActionConfigurator to configure the worksheet's actions appropriately.
Defaults are used where these are defined, ie. in common actions. Defaults are overridden with the
externalised configuration eg. from an XML configuration file.
Action items
"A printer consists of three main parts: the case, the jammed paper tray and the blinking red light."
Using our actions, we can create buttons and menu items.
<b>public</b> JButton createToolBarIconOnlyButton(GAction action) {
JButton button = <b>new</b> JButton();
button.setAction(action);
button.setText(<b>null</b>);
<b>return</b> button;
}
<b>public</b> JButton createTextOnlyButton(GAction action) {
JButton button = <b>new</b> JButton();
button.setAction(action);
button.setIcon(<b>null</b>);
<b>return</b> button;
}
This causes the JButton instances to invoke configurePropertiesFromAction()
(in their AbstractButton superclass) to get their label et al from the action.
When the button is pressed, the actionPerformed()
method in GAction will be invoked, which forwards the event to GEventHelper,
which in turn will forward the event to our worksheet, that is if it implements ActionListener.
Menu items are created similarly, as below.
<b>public</b> JMenuItem createMenuItem(GAction action) {
JMenuItem menuItem = <b>new</b> JMenuItem();
menuItem.setAction(action);
<b>return</b> menuItem;
}
Finally, keystrokes are activated on components' input maps,
using putKeyStroke() methods presented above in GAction.
align=right hspace=32 vspace="8" />
Conclusion
"Computers make very fast, very accurate mistakes."
We continue our Swing adventures from earlier articles
Event DTs,
Turn Tables and
Panel Beater.
We present the action framework of greenscreen,
with some simplification.
We look at Swing Actions, which are used for buttons, menu items and keystrokes.
We configure actions centrally, so that consistent labels, icons,
mnemonics and keystrokes are associated with common actions, such as "Save."
And also so that we can enable and disable actions, and this will filter through automatically
to any buttons, menu items and keystrokes associated to that action.
The configuration of actions is designed to be externalisable eg. in XML configuration files
using JAXB2.
Time for pizza... with loads of olive oil!
The next article in this series is Framewarez
which will look at building an application frame with menu and tool bars, for housing worksheets,
using the actions and worksheet context presented here.
- Login or register to post comments
- Printer-friendly version
- evanx's blog
- 462 reads





