The small details in life: the focus restore

This blog should show you a small, often overlooked, problem within Eclipse e4 and it should also provide you with a possible solution.

The Problem

Eclipse e4 has a lot of useful annotations and one of these is the @Focus. This annotation helps you to set the focus on a control in your view, but have you ever realized that in some cases it also leads to unwanted focus jumps? Sounds weird, but the annotated method is called whenever your MPart is activated or the parent component gets focused, which means if you click (give focus) on one of the elements marked in the following screenshot.

SC_Focus_ExplainationA default implementation of an @Focus method will look like this.

@Focus
public void onFocus() {
  // The following assumes that you have a Text field
  // called summary
  summary.setFocus();
}

This implementation gives the summary input element the focus whenever the method is invoked. This leads to an annoying focus switch if you activate another part to get some additional information.

To sketch it in an example: The summary is already entered and you start to enter the description. During this process you recognize that you need some information from another view. You open the required view, copy the needed information, and switch back to the “Details” view where you want to paste the copied information into the description. Your brain remembers that the last cursor position in the “Details” view was the description field and you start to paste the value with CTRL+V, but the focus has already jumped back to the summary field and appends the value into the summary.

The Solution

Sometimes you want exactly this behavior and sometimes you don’t. One potential solution is to add a FocusListener to all controls on your part just to keep a reference of the last focused element and restore the focus in the @Focus method. While this solution works, it is not scalable and easy to transfer to other parts in your application. Think about other parts from other developers which want this behavior too. And what if you (or somebody else) add(s) some controls to the view and forget to add this listener?

Eclipse 4 and the application model come to rescue if you let them help you. All that needs to be done is to write a small add-on which remembers the last focused control per part and in the @Focus annotated method you retrieve this control and re-assign the focus to it.

The Example

In the following example I reuse the “RCP Todo” application from Lars Vogel which is created by executing the examples in his book “Eclipse RCP application development – Based on Eclipse 4.3“. To reuse the add-on in other projects a new plug-in is created which provides the functionality.

  1. create a new bundle/plug-in named “com.example.e4.rcp.swt.focus.restore” (just to have the same naming convention as in the book but you can use any name you want)
  2. add the following dependencies to it
    • javax.annotation
    • org.eclipse.swt
    • org.eclipse.e4.ui.model.workbench (version must be at least: 0.12.1)
  3. create a new package called “com.example.e4.rcp.swt.focus.restore” and a Java-Class inside the package named “SWTLastFocusedControlAddon
  4. export the package “com.example.e4.rcp.swt.focus.restore"
  5. add the following code to the SWTLastFocusedControlAddon class:
    private static final String SWT_MODEL_ELEMENT = "modelElement";
    
    /** MUIElement's transient data key which holds the last focused widget. */
    public static final String LAST_FOCUSED_CONTROL = "lastFocusedControl";
    
    @PostConstruct
    public void registerFilter(Display d) {
      final Listener l = new Listener() {
        @Override
        public void handleEvent(Event event) {
          /*
           * The following Widget#getData() call is the bi-directional link
           * between SWT widget and the ApplciationModel Element, which is
           * created during:
           * org.eclipse.e4.ui.workbench.renderers.swt.SWTPartRenderer#bindWidget(MUIElement, Object)
           */
          Object modelElement = event.widget.getData(SWT_MODEL_ELEMENT);
          MUIElement view = (modelElement instanceof MUIElement) ? (MUIElement) modelElement : null;
    
          if (view == null && event.widget instanceof Control) {
            // search the hierarchy upwards to find the MPart
            for (Composite p = ((Control) event.widget).getParent(); p != null; p = p.getParent()) {
              modelElement = p.getData(SWT_MODEL_ELEMENT);
    
              if (modelElement instanceof MUIElement) {
                view = (MUIElement) modelElement;
                break; // leave the loop, we've found the view/part
              }
            }
          }
    
          if (view != null) {
            // keep the reference so it can be accessed in the @Focus method
            view.getTransientData().put(LAST_FOCUSED_CONTROL, event.widget);
          }
        }
      };
    
      // install the global focus lost handler
      d.addFilter(SWT.FocusOut, l);
    
      // manual clean-up because @PreDestory would be too late (Display already
      // disposed); not really necessary because the display will be disposed
      // but clean-up after you have made a mess!
      d.addListener(SWT.Dispose, new Listener() {
        @Override
        public void handleEvent(Event event) {
          event.display.removeFilter(SWT.FocusOut, l);
          event.display.removeListener(SWT.Dispose, this);
        }
      });
    }
  6. register the add-on in your application model (don’t forget to add the new “com.example.e4.rcp.swt.focus.restore” plug-in as a dependency in your MANIFEST.MF file)
    AddonRegistration
  7. change your @Focus method in the “com.example.e4.rcp.todo.ui.parts.TodoDetailsPart” to
    @Focus
    public void onFocus(MPart part) {
      Control c = (Control) part.getTransientData().get(SWTLastFocusedControlAddon.LAST_FOCUSED_CONTROL);
      if( c != null ){
        c.setFocus();
      } else {
        // The following assumes that you have a Text field
        // called summary
        summary.setFocus();
      }
    }
  8. add the new “com.example.e4.rcp.swt.focus.restore” bundle to your product configuration (either directly or via feature)
  9. run the application and check the above scenario with the “Details” view again

Final Words

All of the following code can be found at GitHub: https://github.com/RenBrand/eclipse4book

Many thanks to Lars Vogel who invited and motivated me to write this blog entry.

This entry was posted in Eclipse, Other and tagged , , . Bookmark the permalink.