Friday, May 26, 2006

Context-sensitive Edit Menu

Most applications have an "Edit" menu where our friends "Cut", "Copy", and "Paste" hange out. In any non-trivial application, there are certainly more than one thing to which those operations might apply. How can we hook up these friendly edit operations without having to know beforehand everything that might be Cut or every component that might accept a Paste?


Permission is required in order to access the system clipboard.

Let's take a simple example, which displays two JLists. The JList supports a copy action (via its LAF TransferHandler, if you're curious). You can retrieve the action from its ActionMap. The ActionMap is supported by all JComponents, and is basically a pool of all Actions that the component supports. How best to make Edit->Copy (or any other menu item, for that matter) invoke the corresponding action from the appropriate JList? Note that handling a keyboard shortcut is already taken care of by the JList's InputMap, which maps keystrokes to actions in the ActionMap.

First, let's define context to be what component has focus. This may not always be true, but given that keyboard input goes where the focus is, it's reasonable to assume that focus indicates the proper current context.

If all we wanted to do was support mapping a keyboard shortcut to an action, we'd have nothing to do, but we'd like to actually make Edit->Copy do the same thing as that keyboard shortcut. A quick and dirty hack might be to have the menu item generate a fake keystroke sent to the currently focused component, but that only works if the target action has a keystroke associated with it. But that's kind of indirect and doesn't smell very good.

What if the Edit->Copy menu item reflected a sort of delegating action, whose actionPerformed logic simply called some other action. Whenever the context (i.e. focus) changes, we check the focused component's ActionMap for a copy action. Whatever we find, we set as the delegate of the delegating action. If there is no copy action, then the delegating action sets itself disabled.

public class DelegatingAction {
public DelegatingAction(String name) { super(name); }
public void setDelegate(Action a) {
delegate = a;
setEnabled(delegate != null);
}
public void actionPerformed(ActionEvent e) {
if (delegate != null) { delegate.actionPerformed(e); }
}
}


To actually look up the delegates, we can store one or more keys which can be used to identify the actions in the target components. For custom-defined actions, you can set your action keys to whatever is appropriate. In order to handle Swing built-in edit actions, we have to account for a few variations. TransferHandler uses hard-coded strings "cut", "copy" and "paste", while text components use a few string constants defined in DefaultEditorKit, which are different from the TransferHandler strings (e.g. "copy-to-clipboard"). Since these variants exist, we need to check for all of them, since they all correspond to the same Edit menu items.

To know when to update the delegate's mapping, we can install a property change listener to the keyboard focus manager (available in 1.4+ JREs). We only want to look at permanent focus changes, so listen to "permanentFocusOwner".

There's an unexpected glitch caused by the fact that the TransferHandler edit actions are singletons shared across all transfer handlers. As such, they expect to deduce the target of the edit operatoin from the ActionEvent source. In order to work around this, the delegating action must also keep track of the current component context so that it can reformat the ActionEvent to have the proper source.

The singleton implementation also means that the actions' enabled state doesn't reflect whether the target component can actually process the action. Copy should be disabled if nothing is selected, and paste disabled if the component is not editable. Leave that to a future article...

In practice I use a system that's a little more general (it can auto-populate dynamic menus and submenus, and listens for changes on a per-menubar basis instead of per-action, which makes GC a little easier).

Source is here.

2 comments:

Alex said...

This seems to be exactly what I am looking for... but the source link gives me a jarfile with .class files and no source. Would you be willing to post the source code of the demo app?

Thank you so much.

Alex

technomage said...

The source is available at the furbelow project on sourceforge, http://furbelow.sf.net.