Key Bindings in Eclipse/RCP Applications

June 7, 2008 | Tags:

THE CONTEXT

I very recently started to write my first application based on the eclipse Rich Client Platform.  RCP is a wicked neato platform that automagically implements all kinds of UI features that you would otherwise have to either program manually or leave out of your app.  RCP FTW!

My app is dalewriter, a tool to help me write and organize stories, articles, and books.  (I’m a geek, which means that instead of writing stories, articles, and books, I spend my time creating tools to help me write stories articles and books.)  dalewriter will allow me to edit text, store it a tree form (chunks of text in a tree of folders), shuffle and reorder the bits, and build a manuscript from the tree or a subtree.

Six days ago I didn’t know nothin’ ‘bout no RCP, so I’ve been using  McAffer and Lemieux’s RCP book, following along with their running example and adjusting what I learned to fit my app instead of theirs.

Development was going swimmingly until I tried to establish my first key binding–the connection that makes a given keystroke cause a given action.  I wanted CTRL-N to create a new text item in the currently selected folder and open it for editing.  I’d already written and tested the action itself, and now I wanted to connect CTRL-N to the action.

THE PROBLEM

I followed the example in the book, and it didn’t work right.  I wish I could tell you the exact failure, but I wasn’t expecting that I would (have to) learn so danged much that I’d want to write about it.  I spent a great deal of the next few days (and long, long nights) tracking down everything I could find on the Interwebs about key bindings.

Part of the problem is that eclipse’s key-binding mechanism has changed over the past few releases, and much of the information I found referred to old-style bindings.  I tried each idea I found.  Each one led to “hey, dood, cut that out, that’s deprecated” warnings.  (I’m paraphrasing slightly.)

And not only that, they didn’t work.  I observed three general “not what I wanted it to do” responses from my app when I typed CTRL-N:

Finally, I somehow pieced together disparate examples and descriptions of bindings and related mechanisms, and found a solution.

Given how many other people have stumbled over eclipse/RCP key-binding quirks, I thought I’d describe the solution I found, my understanding of why this solution works, and ideas for what to do if you’re having trouble.  You’re welcome.

THE SOLUTION

Before I describe what worked, A CAVEAT:  I still don’t know hardly nothin’ ‘bout no RCP.  As I said, I’ve been working with RCP for less than six days, so take everything I say here with a bag of salt.  I’ve done my best to explain my understanding of how and why this works, but I’m no authority.  Yet.

The key ingredients of the whole enchilada are:

  1. An action to invoke.
  2. A command that invokes the action.
  3. A key binding that invokes the command (that invokes the action).
  4. A plug-in configuration file that enables your key bindings.

Action.  First, create an action that the keystroke will invoke.  I’ll assume that you know how to create an Action in RCP.  If you need help with this, see the RCP book.

The secret sauce here is that you have to make the action available for invocation by commands.  To do that:

  1. Make up a unique identifier string that will represent the command.  I chose com.dhemery.dalewriter.command.AddItem.
  2. In the constructor of your action, call the setActionDefinitionId() method, passing it the identifier you created.  When the command is invoked, RCP will look for a registered action that has this action definition ID, and invoke it.
  3. In ApplicationActionBarAdvisor.makeActions(), register your action in the usual way (see the RCP book).

Command.  There is no way to connect a key binding directly to an action.  Apparently there used to be, but the old mechanism either gone or deprecated.  To connect a key binding to an action, you create an intermediary called a command.  You do this not with Java code, but by declaring a command extension:

  1. Add the org.eclipse.ui.commands extension point to your plugin.xml file (on the Extensions tab of your plugin.xml file).
  2. Add a new command:  Right click on the commands extension point and select New > command.
    • In the id* field of the new command, enter the command identifier that you used to register your action (e.g. com.dhemery.dalewriter.command.AddItem).  This connects the command to the action.  When the command is invoked--such as in response to a user keystroke--RCP will invoke the action that you have registered with this ID.  The id* that you enter into this field must be identical to the one with which you registered the action.
    • Use whatever name* and description you like.  Those fields don't affect key bindings.

Key Binding.  Next, you need a key binding and a key binding scheme.  The key binding connects a specified keystroke to your command.  A key binding scheme is a named bundle of key bindings that can be enabled all at once.

  1. Add the org.eclipse.ui.bindings extension point to your plugin.xml file.
  2. Create a key binding scheme:  Right click on the bindings extension point and select New > scheme.
    • Give your scheme a unique identifier in the id* field.  I used com.dhemery.dalewriter.bindings.
    • Use whatever name* and description you like.  These fields don't affect key bindings.
    • You can leave the parentId field blank.  Or if you want to make a ten-pound bag of default actions available to your app, see the "Bonus: Default Bindings and Actions" section below.
  3. Create a key binding:  Right click on the bindings extension point and select New > key.
    • In the sequence* field, enter the key (or key chord) that will invoke your action (e.g. M1+N or CTRL-N).
    • In the schemeId* field, enter the identifier you assigned to your new binding scheme.  This tells RCP to active this binding whenever the scheme is active.
    • In the commandId field, enter the command identifier that you made up way back in step one.  This is the command id that you assigned to the command extension, and that you used to register the action.  For me, the commandid was com.dhemery.dalewriter.command.AddItem.
    • For the love of all that is good and holy, don't mess with contextId, platform, or locale.  If you're indifferent to all that is good and holy, make up your own reason not to mess with those fields.

Plug-In Configuration File.  To activate your binding scheme (and thus all of its bindings), you use a property in a plug-in configuration file:

  1. Create a new plug-in configuration file in your project.  This is just a plain text file.  I added mine directly under the project and called it plugin_customization.ini (which seems to be the standard name).
  2. Add a KEY_CONFIGURATION_ID property in the file to tell RCP to activate your binding scheme whenever your plug-in is active.  To do that, add a line like this:
    • org.eclipse.ui/KEY_CONFIGURATION_ID=com.dhemery.dalewriter.bindings
  3. Add a preferenceCustomization property to your product extension.  This tells RCP to load your configuration file and apply its preferences when it activates your plug-in.
    • Find your product extension under the org.eclipse.core.runtime.products extension point in the plugin.xml file.  If your application doesn't yet have a product extension, see the RCP book for instructions on how to create one.
    • Right click your product extension and select New > property.
    • In the name* field, enter preferenceCustomization.  When RCP starts up your application, it will look up this property to identify the plug-in configuration file to apply.
    • In the value* field, enter the name (and path, if appropriate) of your plug-in configuration file.

Yep, it’s as simple as that.  (Yep, that was irony–a subtle form of we Mainers call hyoomah.)

If you know what you’re doing, you can define the command, the binding scheme, the key binding, and the customization property directly in raw XML in the plugin.xml file.  If you don’t know what you’re doing, your plugin.xml file will become contumaciously hosed (hosed being a technical term that means (roughly):  hosed).  I will leave it to the reader to infer how I know this.

Before I move on to troubleshooting, here’s one more bit of useful information.

Bonus:  Default Bindings and Actions.  eclipse/RCP includes a default key binding scheme the defines key bindings for a passel of groovy built-in actions (e.g. CTRL-S to save).  Now, it turns out that the default scheme is the bastard that eructs that annoying “Select a wizard” dialog when I press CTRL-N in dalewriter.  Mumble grumble.

But wait, here’s the wickedest awesomest part:  There’s a way for your key binding scheme to inherit all of the cool defaults, use the ones you want, override the ones you want to override, and defenestrate the ones you don’t want.  Here’s the straight dope:

TROUBLESHOOTING RCP KEY BINDING PROBLEMS

Here are the primary failures I’ve observed, and possible solutions.

The “Dreaded Ding” Problem:  Your app emits a warning ding instead of executing your action.  This is a sign that your app has no active key binding for that keystroke.  In order for a keystroke to find the right action, there’s a long chain of links that must be established correctly.

Solution.  Here are some things to check:

The “Multiple Choice” Problem: Your app displays a little yellow table (like the picture below) that displays a number of actions that you can choose to execute. This is a sign that the active binding scheme has two or more bindings for the same keystroke. I stumbled over this when (in my willingness to try anything) I assigned my key binding to eclipse/RCP’s default binding scheme (org.eclipse.ui.defaultAcceleratorConfiguration). This made all of the cool default bindings available to my app, but it created a conflict between my CTRL-N binding and the “Select a wizard” CTRL-N binding already in the default scheme. When you execute a keystroke for which the active scheme has two bindings, eclipse/RCP requires the user to take the extra step of choosing between the several possible actions:

(Note: The image is no longer available.)

Solution.  Assign your key binding to your own binding scheme, and not to the default one.  If you also want your app to inherit all of the other key bindings from the default scheme, follow the instructions in the “Bonus:  Default Bindings and Actions” section above.

The “Wrong Action” Problem:  Your app executes some other action (e.g. the default “Select a wizard” dialog) instead of your action.  This primary trouble here is the same as in The “Dreaded Ding” Problem:  Your key binding is not active.  The difference here is that some other scheme has activated a key binding for the relevant keystroke.  Your key binding is not active, but some other key binding is active.

Solution.  First activate your key binding by working through the checklist for The “Dreaded Ding” Problem.  If this causes you to lose all of the useful actions from the default scheme, make sure to declare the default scheme as a parent of your scheme, as described in the “Bonus:  Default Bindings and Actions” section above.

EXPLORING ON YOUR OWN

If none of my ideas solve the problem for you, well, that’s all the ideas I have right now, so you’ll have to gather more information either from your program or from the innertoobs.

To help you explore your program, I’ll toss you one last cookie.  Here’s a programmatic way to print the active binding scheme and a list of all active key bindings:

IWorkbench workbench = PlatformUI.getWorkbench();
IBindingService bindingService =
  (IBindingService)workbench.getAdapter(IBindingService.class);
System.out.println(bindingService.getActiveScheme());
for(Binding binding : bindingService.getBindings()) {
    System.out.println(binding);
}