Key Bindings in Eclipse/RCP Applications
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:
- Ding at me (the standard Windows warning ding) without creating a new item. I came to interpret this as a sign that CTRL-N was not bound to any action.
- Open the "Select a wizard" dialog, a standard eclipse dialog that allows the user to select among a list of things to create. This appears to be the default eclipse action for CTRL-N.
- Open a little yellow table in the bottom right corner of the window, asking me to select which action I wanted to take. The choices were labeled "New" and "New Item". The "New" choice invoked the "Select a wizard dialog". The "New Item" choice was the one I had created. Hey! That's progress! It still wasn't right--I wanted CTRL-N to directly invoke my action instead of first asking the user (i.e. me) to choose, but still, that was progress.
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:
- An action to invoke.
- A command that invokes the action.
- A key binding that invokes the command (that invokes the action).
- 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:
- Make up a unique identifier string that will represent the command. I chose com.dhemery.dalewriter.command.AddItem.
- 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.
- In ApplicationActionBarAdvisor.makeActions(), register your action in the usual way (see the RCP book).
- Add the org.eclipse.ui.commands extension point to your plugin.xml file (on the Extensions tab of your plugin.xml file).
- 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.
- Add the org.eclipse.ui.bindings extension point to your plugin.xml file.
- 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.
- 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.
- 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).
- 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
- 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.
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:
- To make the default bindings and actions available in your own binding scheme, make the default scheme the parent of yours: In the parentId field of your scheme, enter org.eclipse.ui.defaultAcceleratorConfiguration.
- If you want a keystroke to trigger an action of your own rather than the default action, simply add a key binding of your own into your scheme. Yeah, okay, so that's not so simple: You still have to do all of the crap in the previous sections. But still.
- If you want to disconnect a default binding from your app without overriding it, simply add a binding to your scheme, enter the appropriate key chord in the sequence* field, and leave the commandId field empty. See? This time when I said simply, I really meant simply. Leaving the commandId field blank tells RCP "when the user presses these keys, do nothing."
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:
- Did you make up a command ID that is absolutely unique within your app? I don't know what happens if there are duplicate command IDs. I suspect it would destroy Western civilization.
- Did you call setActionDefinitionId() in your action's constructor? If not, your action is not available to be invoked by commands.
- Did you pass the correct command ID to setActionDefinitionId()? The string that you pass must be identical to the string you entered into your command extension's id* field. If you passed an incorrect string, RCP won't find your action (and therefore won't execute it) when the command fires.
- Did you register your action by calling register(action) within ApplicationActionBarAdvisor.makeActions()? If you omit this call, your action won't be registered with RCP, so RCP won't be able to find and execute your action when the command fires.
- Did you define a command extension? If you omit this, RCP will not know to associate your key binding with your action.
- Did you assign your command the correct id*? If the id* is incorrect, RCP will not find your action when the command fires.
- Did you define a binding scheme for your key bindings? If you omit binding scheme, RCP will not know to activate your binding.
- Did you assign the binding scheme a unique id*? I don't know what happens if there are duplicate binding scheme IDs. Again, Western civilization hangs in the balance, and even Obama won't be able to fix this.
- Did you define a key binding? If you omit the key binding, RCP won't know what to do when the user types the keystroke. (Could I say "when the user strokes the keys," or would that just be weird?)
- Did you specify the correct key sequence* in the key binding? If the sequence* is incorrect, your action may fire when the user strokes some other keys. (Hmmm. Yes, I guess that does sound weird.)
- Did you assign your key binding to your binding scheme? If you assign the key binding to an incorrect binding schemes, then even if your binding scheme is active, your key binding will not be.
- Did you create a plug-in customization file? If you omit the customization file, RCP will not know that you have a binding scheme (or other preferences) for it to load when it loads your plug-in.
- Did you include your plug-in customization file in the build? If not, RCP won't find your customizations when it looks for them.
- Did you add a preferenceCustomization property to your product? If you omit this property, RCP will not know about your preferences file, and so will not load your preferences.
- Did you assign the preferenceCustomization the right value*? This value* be the name your plug-in customization file, along with the file path if you put the file somewhere other than directly the project. If you assign an incorrect value*, RCP will load (or attempt to load) the wrong set of preferences when it loads your plug-in.
- Did you add the org.eclipse.ui/KEY_CONFIGURATION_ID property to your plug-in customization file? If you omit this property, then won't know that you want it to activate your binding scheme.
- Did you assign the KEY_CONFIGURATION_ID property the right value? This must be the unique identifier for your binding scheme. If value of this property is incorrect, RCP will activate (or try to active) a binding scheme other than the one you intend.
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);
}