Sunday, February 12, 2006

Swing: Context Sensitive Actions

Swing Components manage the list of actions and their key bindings using ActionMap and InputMap.

In simple terms,
ActionMap can be thought of Map between actionName and action.
InputMap can be thought of Map between KeyStroke and actionName.

The ActionMap and InputMap of a component is actually created by its LookAndFeel class and UI Delegate. Most of the actions of swing components are singleton instances. I mean, a single instance of action is enough to handle actions from multiple instances of swing components.Such action can't be reused in creating Action based components such as JButton.

For example let us see the following snippet:

  JList list = new JList(listData);
JPanel listPanel = new JPanel(new BorderLayout());
listPanel.add(new JScrollPane(list), BorderLayout.CENTER);
Action action = list.getActionMap().get("selectAll");
listPanel.add(new JButton(action), BorderLayout.SOUTH)

In the above snippet, we fetched the "selectAll" action from a JList's actionMap and created a JButton with that action. But when you click that button, it won't work as expected. It throws ClassCastException.

java.lang.ClassCastException
at javax.swing.plaf.basic.BasicListUI$SelectAllAction.actionPerformed(BasicListUI.java:2125)
at javax.swing.AbstractButton.fireActionPerformed(AbstractButton.java:1786)
at javax.swing.AbstractButton$ForwardActionEvents.actionPerformed(AbstractButton.java:1839)

The reason is, such actions assume that ActionEvent.getSource() is the JList on which the action to be performed. But here the ActionEvent.getSource() returns the JButton that we created. That is why ClassCastException is thrown.

Then question arises: does it mean, there is no way to reuse the action without writing your own SelectAllAction implementation ?

There is a solution. We should somehow make the selectAll action context sensitive to this particular JList. We create a wrapper to the original action and add the context sensitive information via this.

// @author Santhosh Kumar T - santhosh@in.fiorano.com 
public class ContextSensitiveAction implements Action{
protected Action delegate;
protected Object source;

public ContextSensitiveAction(Action delegate, Object source){
this.delegate = delegate;
this.source = source;
}

public boolean isEnabled(){
return delegate.isEnabled();
}

public void setEnabled(boolean enabled){
delegate.setEnabled(enabled);
}

public void addPropertyChangeListener(PropertyChangeListener listener){
delegate.addPropertyChangeListener(listener);
}

public void removePropertyChangeListener(PropertyChangeListener listener){
delegate.removePropertyChangeListener(listener);
}

public Object getValue(String key){
return delegate.getValue(key);
}

public void putValue(String key, Object value){
delegate.putValue(key, value);
}

public void actionPerformed(ActionEvent ae){
delegate.actionPerformed(new ActionEvent(source, ae.getID()
, ae.getActionCommand(), ae.getWhen()
, ae.getModifiers()));
}
}


Now we modify the original snippet to use this wrapper action:

  JList list = new JList(listData);
JPanel listPanel = new JPanel(new BorderLayout());
listPanel.add(new JScrollPane(list), BorderLayout.CENTER);
Action action = new ContextSensitiveAction(
list.getActionMap().get("selectAll"), list);
listPanel.add(new JButton(action), BorderLayout.SOUTH);




This is the screenshot of the webstart demo. Here the first [SelectAll] button uses action from ActionMap. and the second [SelectAll] button uses the action wrapped with ContextSensitiveAction.

No comments: