Thursday, February 5, 2009

Field-level key bindings

Sometimes you need to create key bindings in Eclipse which are scoped to just a single control. I had one such case today. I have a table cell editor that’s based on the TextCellEditor, but adds a small graphical browse button. It works great as long as you are using a mouse, but I needed to make the browsing function keyboard accessible for people with disabilities.

Eclipse has a nice command framework that lets you define commands in abstract sense, place them in contexts, define key bindings and finally associate handlers to do the actual work. Finding an example that puts all of these concepts to work together in a particular way can be challenging, so I thought I would share my solution to the above problem and explain some of this API along the way.

The first step is to define the command. A command is an operation that a user can perform, but we don’t actually specify how to perform that operation when defining the command. That comes later when we add a handler. Every command must belong to a category. Here we define a category as well. You will typically want to create a category for each broad functional area to make it easier for users to find and manage your commands in the preferences.

<extension point="org.eclipse.ui.commands">
  <category
    id="my.category"
    name="My Category"/>
  <command
    id="my.browse.command"
    categoryId="my.category"
    name="Browse"/>
</extension>

The next step is to define the context. The context controls what commands are available via key bindings based on where in the workbench the user is working. Typically views and editors define contexts, but there is nothing stopping you from defining one that is more focused. In this example, we will create a context for fields with browsing capability.

<extension point="org.eclipse.ui.contexts">
  <context
    id="my.browseable.field.context"
    parentId="org.eclipse.ui.contexts.window"
    name="In Browseable Field"/>
</extension>

The final declarative step is to define the key binding. The following assigns Ctrl+L to the browse command in the browseable field context.

<extension point="org.eclipse.ui.bindings">
  <key
    sequence="M1+L"
    contextId="my.browseable.field.context"
    commandId="my.browse.command"
    schemeId="org.eclipse.ui.defaultAcceleratorConfiguration"/>
</extension>

And now for the final bit of magic… The following function brings it all together by enabling the browseable field context and associating a handler with the browse command when the specified text field gains focus. When the focus is lost, the context and the handler are deactivated.

public static void addBrowseKeyBinding( final Text textField,
                                        final Runnable browseOperation )
{
    final IHandler browseCommandHandler = new AbstractHandler() 
    {
        public Object execute( final ExecutionEvent event )
        {
            browseOperation.run();
            return null;
        }
    };
        
    final IWorkbench workbench = PlatformUI.getWorkbench();
    
    final IHandlerService handlerService 
        = (IHandlerService) workbench.getService( IHandlerService.class );

    final IContextService contextService 
        = (IContextService) workbench.getService( IContextService.class );
        
    final IHandlerActivation[] handlerActivationRef = new IHandlerActivation[ 1 ];
    final IContextActivation[] contextActivationRef = new IContextActivation[ 1 ];
        
    textField.addFocusListener
    (
        new FocusListener()
        {
            public void focusGained( final FocusEvent event )
            {
                final IHandlerActivation handlerActivation
                    = handlerService.activateHandler( "my.browse.command", browseCommandHandler );
                    
                handlerActivationRef[ 0 ] = handlerActivation;
                    
                final IContextActivation contextActivation
                    = contextService.activateContext( "my.browseable.field.context" );
                    
                contextActivationRef[ 0 ] = contextActivation;
            }

            public void focusLost( final FocusEvent event )
            {
                handlerService.deactivateHandler( handlerActivationRef[ 0 ] );
                contextService.deactivateContext( contextActivationRef[ 0 ] );
            }
        }
    );
}

No comments: