Eclipse Plug-in Development:Beginner's Guide(Second Edition)
上QQ阅读APP看书,第一时间看更新

Time for action – creating a tree viewer

As with the previous chapter, a new TimeZoneTreeView class will be created using the plugin.xml editor, as an E4 view. This will show time zones, organized hierarchically by region.

  1. Right-click on the com.packtpub.e4.clock.ui project and navigate to Plug-in Tools | Open Manifest if it's not open already.
  2. Open the Extensions tab and go to the org.eclipse.ui.views entry. Right-click on this, navigate to New | e4view, and fill in the following:
    1. ID: com.packtpub.e4.clock.ui.views.TimeZoneTreeView
    2. Name: Time Zone Tree View
    3. Class: com.packtpub.e4.clock.ui.views.TimeZoneTreeView
    4. Category: com.packtpub.e4.clock.ui
    5. Icon: icons/sample.gif
  3. An entry is created in the plugin.xml file that looks like:
    <e4view
      category="com.packtpub.e4.clock.ui"
      class="com.packtpub.e4.clock.ui.views.TimeZoneTreeView"
      icon="icons/sample.gif"
      id="com.packtpub.e4.clock.ui.views.TimeZoneTreeView"
      name="Time Zone Tree View"
      restorable="true">
    </e4view>
  4. On the Dependencies tab, click on Add in the Imported Packages section, and add the org.eclipse.e4.ui.di package, which adds the @Focus annotation used by E4 views. The @Inject and @PostConstruct annotations are re-exported from the org.eclipse.ui bundle.
  5. Create a class called TimeZoneTreeView in the com.packtpub.e4.clock.ui.views package.
  6. Add a create method taking a Composite parent and annotate it with @PostConstruct. Inside the method, create an instance of a TreeViewer, with the H_SCROLL, V_SCROLL, and MULTI flags set, and store it in a field treeViewer:
    package com.packtpub.e4.clock.ui.views;
    import javax.annotation.PostConstruct;
    public class TimeZoneTreeView {
      private TreeViewer treeViewer;
      @PostConstruct
      public void create(Composite parent) {
        treeViewer = new TreeViewer(parent,
          SWT.H_SCROLL | SWT.V_SCROLL | SWT.MULTI );
      }
    }
  7. Run the Eclipse application, and show the view by navigating to Window | Show View | Timekeeping | Time Zone Tree View:
  8. Unlike Swing, which expects data to be presented in a specific interface, the JFace viewers don't expect any specific data class. Instead, they expect an object value to display (the input), an interface that can read that data (the content provider), and an interface for displaying that data (the label provider).
  9. Create a new class called TimeZoneLabelProvider, which extends LabelProvider (from the org.eclipse.jface.viewers package). This has a method called getText, which is passed an object and translates that into a textual representation. Instead of a toString call here, return an appropriate value for a Map.Entry or a ZoneId:
    public class TimeZoneLabelProvider extends LabelProvider {
      @SuppressWarnings("rawtypes")
      public String getText(Object element) {
        if (element instanceof Map) {
          return "Time Zones";
        } else if (element instanceof Map.Entry) {
          return ((Map.Entry) element).getKey().toString();
        } else if (element instanceof ZoneId) {
          return ((ZoneId) element).getId().split("/")[1];
        } else {
          return "Unknown type: " + element.getClass();
        }
      }
    }
  10. Since a TreeViewer can have multiple roots, the instanceof Map test is used to represent the top of the tree, called Time Zones.

    Tip

    It's usually a good idea to have a default value—even if it's only an empty string—so that when an unexpected value type is seen in the list, it can be recognized and debugged.

  11. Create a new class TimeZoneContentProvider, which implements the ITreeContentProvider interface. This requires the implementation of three of the six methods as follows, leaving the other three empty:
    • hasChildren: Returns true if the node has children
    • getChildren: Provides the children of a given node
    • getElements: Provides the top-level roots
  12. The hasChildren method will return true if passed a non-empty Map or Collection; otherwise, recurse into the Map.Entry value:
    @SuppressWarnings("rawtypes")
    public boolean hasChildren(Object element) {
      if (element instanceof Map) {
        return !((Map) element).isEmpty();
      } else if (element instanceof Map.Entry) {
        return hasChildren(((Map.Entry)element).getValue());
      } else if (element instanceof Collection) {
        return !((Collection) element).isEmpty();
      } else {
        return false;
      }
    }
  13. The getChildren implementation recurses into a Map, Map.Entry, or Collection following the same pattern. Since the return of this function is an Object[], the entrySet method in the Map class can be used to convert the contents to an array:
    @SuppressWarnings("rawtypes")
    public Object[] getChildren(Object parentElement) {
      if (parentElement instanceof Map) {
        return ((Map) parentElement).entrySet().toArray();
      } else if (parentElement instanceof Map.Entry) {
          return getChildren(((Map.Entry)parentElement).getValue());
      } else if (parentElement instanceof Collection) {
          return ((Collection) parentElement).toArray();
      } else {
        return new Object[0];
      }
    }

    Tip

    The key to implementing an ITreeContentProvider is to remember to keep the implementation of the getChildren and hasChildren methods in sync. One way of doing this is to implement the hasChildren method as testing whether getChildren returns an empty array, but this may not be performant if getChildren is an expensive operation.

  14. Since a TreeViewer can have multiple roots, there is a method to get the array of roots from the input element object. A bug in the JFace framework prevents the getElements argument containing its own value; it is therefore conventional to pass in an array (containing a single element) and return it:
    public Object[] getElements(Object inputElement) {
      if (inputElement instanceof Object[]) {
        return (Object[]) inputElement;
      } else {
        return new Object[0];
      }
    }
  15. Now that the provider methods are implemented, connect the providers to the viewer and set the input data object in the create method of the TimeZoneTreeView class:
    treeViewer.setLabelProvider(new TimeZoneLabelProvider());
    treeViewer.setContentProvider(new TimeZoneContentProvider());
    treeViewer.setInput(new Object[]
      {TimeZoneComparator.getTimeZones()});
  16. Run the Eclipse instance, open the view by navigating to Window | Show View | Timekeeping | Time Zone Tree View, and see the results:
  17. Optionally, create a focus method with a @Focus annotation, which sets the focus on the viewer's control. The @Focus annotation needs to be imported from the org.eclipse.e4.ui.di package:
    import org.eclipse.e4.ui.di.Focus;
    ...
    @Focus
    public void focus() {
      treeViewer.getControl().setFocus();
    }
  18. Run the Eclipse instance again, and now when the view is opened, the tree viewer will have the focus.

What just happened?

The data for TreeViewer was provided by the setInput method, which is almost always an array of objects containing a single element.

To traverse the data structure, the ITreeContentProvider interface provides two key methods: hasChildren and getChildren. These methods allow the data structure to be interrogated on demand as the user opens and closes nodes in the tree. The rationale for having two separate methods is that the calculation for getChildren may be expensive; so the hasChildren call is used to display the expandable icon on the node, but the getChildren call is deferred until the user opens that specific node in the tree.

Note

For data structures that support it, implement the getParent method as well; this makes accessing (or revealing) the object possible. When this method is implemented, viewer.reveal(Object) will expand the nodes in the hierarchy to reveal that particular object.

To render the labels in the tree, a LabelProvider is used. This provides a label (and optional image) for each element. It is possible to present a different icon for each type of object; this is used by the Package View in the Java perspective to present a class icon for the classes, a package icon for the packages, and so on.

The LabelProvider can render the text in different ways; for example, it could append the timezone offset or only show the difference between that and GMT.