Skip to main content

Swing and Roundabouts 2: Inside Action

Posted by evanx on June 22, 2006 at 6:06 AM PDT

Introduction

"To err is human, but to really foul things up you need a computer." Paul Ehrlich

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 e.g. 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

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

For our application, we prefix class names with Vinnie. Of coz.

public class VinnieProductWorksheet implements ActionListener {

   protected VinnieWorksheetContext context = VinnieWorksheetContext.createContext(this);

   @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();     
   ...  
  
   public VinnieProductWorksheet() {
      context.configure();     
      configure();
      setEnabled();
      addToolBar(newAction, findByNameAction, deleteAction, saveAction, exitAction);
      ...
   }

   protected void configure() {
      exitAction.setToolTip("Exit worksheet"); // override default
      deleteAction.setKeyStoke(null); // remove default keystroke
      ...
   }
  
   public void actionPerformed(ActionEvent event) {
      context.eventLogger.entering(event);
      if (newAction.isSource(event)) {
         ...
      }
      ...
      setEnabled();
   }
  
   protected void setEnabled() {
      saveAction.setEnabled(productModel.isChanged());
      deleteAction.setEnabled(!productModel.isEmpty());
   }

   protected void addToolBar(GAction ... actions) {
      for (GAction action : actions) {
         JButton button = new JButton();
         button.setAction(action);
         toolBar.add(button);
      }
   }  
   ...      
}

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, e.g. 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, e.g. 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")
public class ActionConfiguration {   
    @XmlAttribute protected Class worksheetClass;
        // e.g. com.mafia.chicago.vinnie.productworksheet.VinnieProductWorksheet
    @XmlAttribute protected String actionCommand; // e.g. "save"
    @XmlAttribute protected String keyStroke; // e.g. "control S"
    @XmlAttribute protected Character mnemonic; // e.g. 'S'
    @XmlAttribute protected String iconName; // e.g. disk.png
    @XmlAttribute protected String label; // "Save"
    @XmlAttribute protected String toolTip;
    @XmlAttribute protected Integer ordial;
   
    ... // getters and setters
    ... // configure(ActionConfiguration), to overwrite with non-null properties from another instance
    ... // cloneActionConfiguration()
}

We will configure common actions centrally, e.g. 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, e.g. from the common action configuration.

In the case of common actions, our worksheet's actions typically only override the toolTip e.g. "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.

public class GAction extends AbstractAction {
   
    protected GContext context;

    protected String actionCommand;
    protected String keyStroke;
    protected char mnemonic;
    protected String iconName;
    protected String label;
    protected String toolTip;

    public GAction(GContext context) {
        super();
        this.context = context;
    }
   
    public void setActionCommand(String actionCommand) {
        this.actionCommand = actionCommand;
        super.putValue(Action.ACTION_COMMAND_KEY, actionCommand);
    }
   
    public void setLabel(String label) {
        this.label = label;
        super.putValue(Action.NAME, label);
    }
   
    public String getLabel() {
        return label;
    }
   
    public void setToolTip(String toolTip) {
        this.toolTip = toolTip;
        super.putValue(Action.SHORT_DESCRIPTION, toolTip);
    }

    ... // other getters and setters

    public void configure(ActionConfiguration configuration) {
       setActionCommand(configuration.getActionCommand());
       setLabel(configuration.getLabel());
       setKeyStroke(configuration.getKeyStroke());
       ... // other properties
    }

    public void putKeyStrokeAncestor(JComponent component) {
        putKeyStroke(component, JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
    }
   
    public void putKeyStrokeInWindow(JComponent component) {
        putKeyStroke(component, JComponent.WHEN_IN_FOCUSED_WINDOW);
    }
   
    public void 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
     */   
    public void putKeyStroke(JComponent component, int inputMapId) {
        component.getActionMap().put(actionCommand, this);
        component.getInputMap(inputMapId).put(KeyStroke.getKeyStroke(keyStroke), actionCommand);
    }
  
    public void actionPerformed(ActionEvent event) {
        context.eventHelper.actionPerformed(event);
    }
       
}   

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 e.g. ActionListener, i.e. handles a given event. If so, it forwards the event to the controller, e.g. invokes actionPerformed(ActionEvent).

public class GEventHelper implements FocusListener, ActionListener, WindowListener {

    protected GContext context;
    protected Object controller = null;
   
    public GEventHelper(GContext context) {
        this.context = context;
    }

    public void setController(Object controller) {
        this.controller = controller;
    }
   
    public void actionPerformed(ActionEvent event) {
        beforeEvent();
        context.actionLogger.entering(event.getSource());
        try {
            if (controller instanceof ActionListener) {
                ActionListener actionListener = (ActionListener) controller;
                actionListener.actionPerformed(event);
            }
        } catch (Throwable e) {
            context.exceptionHelper.severe(e);
        }
        afterEvent();
    }

    ... // other listeners
    ... // beforeEvent(), afterEvent() overridable aspects
}

We might extend this class as VinnieEventHelper to override methods e.g. the beforeEvent() amd afterEvent() aspects. Then we need to configure our context to use this customised event helper.

Basic context

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.

public void GContext {
   public static GContext frameworkContext = new GContext();
   public static GLogger eventLogger = new GLogger(this, "eventLogger");
  
   public static GFormatter formatter = new GFormatter(this);

   public GEventHelper eventHelper = new GEventHelper(this);
   ...
   protected GContext() {
   }
  
   public static GContext getInstance() {
      return frameworkContext; 
   }
   ...
}       

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 half-baked context that we pass to its constructor, e.g. formatter, then we get a null pointer exception when we construct eventLogger, because formatter is created after eventLogger.

Worksheet context

For event handling, we extend this to include a reference to our worksheet object, e.g. VinnieProductWorksheet.

public void GWorksheetContext extends GContext {

   public static GActionConfigurator actionConfigurator;

   protected Object worksheet = null;
  
   public GWorksheetContext() {
      super.eventHelper = new GEventHelper(this);
   }
  
   public void setWorksheet(Object worksheet) {
      this.worksheet = worksheet;
      super.eventHelper.setController(worksheet);
   }
  
   public void configure() {
      ... // configure actions using actionConfigurator
      for (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.

public void VinnieWorksheetContext extends GWorksheetContext {

   public static
   VinnieWorksheetContext applicationContext = new VinnieWorksheetContext();
   ...
  
   protected VinnieWorksheetContext() {
      super();
      super.formatter = new VinnieFormatter(this);
   }

   public static VinnieWorksheetContext createWorksheetContext(Object worksheet) {
      VinnieWorksheetContext worksheetContext = applicationContext.cloneContext();
      worksheetContext.eventHelper = new VinnieEventHelper(this);
      worksheetContext.setWorksheet(worksheet);
      return worksheetContext;
   }

   public VinnieWorksheetContext cloneContext() {
      try {
         return (VinnieWorksheetContext) clone();
      } catch (Exception e) {
         throw new RuntimeException();
      }    
   }
  
   public static void 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, e.g. 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.

When a worksheet creates a context for itself, i.e. 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 e.g. GEventHelper is created as a peer to a specific worksheet class instance, e.g. VinnieProductWorksheet. Others are singletons, but also customisable for our application e.g. GFormatter.

Others are both customisable for our application, and for a specific instance, e.g. GEventHelper is customised as VinnieEventHelper in our application, and is created by the framework for each worksheet instance, e.g. 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 i.e. 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

Lemme tell ya, 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.

So when Vinnie says we want our common actions to be configured centrally in one place as below, just like he learnt in prison, that's how we gonna do them.

public class VinnieActionConfigurator extends GActionConfigurator {

   @ActionAnnotation(
      tooltip = "Save",
      label = "Save",
      keystroke = "control S",
      mnemonic = 'S',
      icon = "disk.png"
   )
   ActionConfiguration saveAction = createActionConfiguration();
 
   ... // many other common actions, e.g. new, find, delete, cancel, refresh, next, previous, exit
  
   protected 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, e.g. in the configure() method.

Action configurator

Let's look at our action configuration superclass.

public abstract class GActionConfigurator {

   protected Map<String, ActionConfiguration> externalisedActionConfigurationMap = null;
   protected List<ActionConfiguration> commonActionConfigurationList = new ArrayList();
   protected Map<String, ActionConfiguration> commonActionConfigurationMap = new HashMap();

   protected GContext context = GContext.getInstance();
  
   protected String smallIconPackage = "icons.16x16";
   protected String largeIconPackage = "icons.32x32";
   protected String defaultIconName = "misc.png";
  
   protected GActionConfigurator() {
   }

   public void setSmallIconPackage(String iconPackage) {
      this.smallIconPackage = iconPackage;
   }

   ... // setter for largeIconPackage, defaultIconName
  
   public ActionConfiguration createActionConfiguration() {
      ActionConfiguration actionConfiguration = new ActionConfiguration();
      actionConfigurationList.add(actionConfiguration); // configure central action
      return actionConfiguration;
   }

   public void configure() {
      ... // load externalisedActionConfigurationMap e.g. from XML file e.g. using JAXB
      for (ActionConfiguration commonActionConfiguration : commonActionConfigurationList) {
         configure(field, commonActionConfiguration);
      }
   }

   protected void 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);
   }

   protected ActionConfiguration coalesce(ActionConfiguration actionConfiguration,
          ActionConfiguration overridingActionConfiguration) {
      if (actionConfiguration == null) return overridingActionConfiguration;
      if (overridingActionConfiguration == null) return actionConfiguration;
      actionConfiguration = actionConfiguration.cloneActionConfiguration();
      actionConfiguration.configure(overridingActionConfiguration);
      return actionConfiguration;
   }  
  
   public void configureWorksheet(Object worksheet) {
      for (Field field : worksheet.getClass().getFields()) {
         field.setAccessible(true);
         if (field.getType() == GAction.class) {
            configure(field, (GAction) field.get(worksheet), worksheet);
         }
      }
   }

   protected void 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);
      if (actionConfiguration != null) {
         action.configure(actionConfiguration);
      } else {
         // not a common action, and its configuration is not externalised (yet)
         ... // maybe generate the XML to cut and paste into our configuration file
      }
      configureIcon(action, smallIconPackage);
   }
     
   protected void configureIcon(GAction action, String iconPackage) {
      if (action.getIconName() == null || action.getIconName().trim().length() == 0) {
         action.setIconName(defaultIconName);
      }
      String iconName = iconPackage + "." + action.getIconName() + ".png";
      URL iconImageUrl = getClass().getResource(iconName);
      try {
         ImageIcon imageIcon = new ImageIcon(iconImageUrl);
         action.setIcon(imageIcon);
      } catch (Exception e) {
         context.fileLogger.warning(e, iconName, iconImageUrl);
      }
   }  
}  

The configure() method gets the list of ActionConfiguration's created in our subclass e.g. VinnieActionConfigurator. It configures them using the field name as the action command, extracting the ActionAnnotation, and finally overriding with the externalised configuration, e.g. loaded from an XML file to which the actionConfigurationList was previously persisted, e.g. using JAXB2.

When we launch our application, we might set GWorksheetContext.actionConfigurator as follows.

   public static void main(String[] args) {
      GWorksheetContext.actionConfigurator = new 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, i.e. in common actions. Defaults are overridden with the externalised configuration e.g. 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.

    public JButton createToolBarIconOnlyButton(GAction action) {
        JButton button = new JButton();
        button.setAction(action);
        button.setText(null);
        return button;
    }

    public JButton createTextOnlyButton(GAction action) {
        JButton button = new JButton();
        button.setAction(action);
        button.setIcon(null);
        return 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.

    public JMenuItem createMenuItem(GAction action) {
        JMenuItem menuItem = new JMenuItem();
        menuItem.setAction(action);
        return menuItem;
    }   

Finally, keystrokes are activated on components' input maps, using putKeyStroke() methods presented above in GAction.

Conclusion

"Computers make very fast, very accurate mistakes."

We present the action framework of http://java.net/projects/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 e.g. in XML configuration files using JAXB2.

Time for pizza... with loads of olive oil!

Sequel

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.

Resources

https://code.google.com/p/vellum/ - where i will collate these articles and their code.

Related Topics >>