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.
- Right-click on the
com.packtpub.e4.clock.ui
project and navigate to Plug-in Tools | Open Manifest if it's not open already. - 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:- ID:
com.packtpub.e4.clock.ui.views.TimeZoneTreeView
- Name:
Time Zone Tree View
- Class:
com.packtpub.e4.clock.ui.views.TimeZoneTreeView
- Category:
com.packtpub.e4.clock.ui
- Icon:
icons/sample.gif
- ID:
- 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>
- 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 theorg.eclipse.ui
bundle. - Create a class called
TimeZoneTreeView
in thecom.packtpub.e4.clock.ui.views
package. - Add a
create
method taking aComposite
parent and annotate it with@PostConstruct
. Inside the method, create an instance of aTreeViewer
, with theH_SCROLL
,V_SCROLL
, andMULTI
flags set, and store it in a fieldtreeViewer
: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 ); } }
- Run the Eclipse application, and show the view by navigating to Window | Show View | Timekeeping | Time Zone Tree View:
- 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).
- Create a new class called
TimeZoneLabelProvider
, which extendsLabelProvider
(from theorg.eclipse.jface.viewers
package). This has a method calledgetText
, which is passed an object and translates that into a textual representation. Instead of atoString
call here, return an appropriate value for aMap.Entry
or aZoneId
: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(); } } }
- Since a
TreeViewer
can have multiple roots, theinstanceof
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.
- Create a new class
TimeZoneContentProvider
, which implements theITreeContentProvider
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 childrengetChildren
: Provides the children of a given nodegetElements
: Provides the top-level roots
- The
hasChildren
method will returntrue
if passed a non-emptyMap
orCollection
; otherwise, recurse into theMap.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; } }
- The
getChildren
implementation recurses into aMap
,Map.Entry
, orCollection
following the same pattern. Since the return of this function is anObject[]
, theentrySet
method in theMap
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 thegetChildren
andhasChildren
methods in sync. One way of doing this is to implement thehasChildren
method as testing whethergetChildren
returns an empty array, but this may not be performant ifgetChildren
is an expensive operation. - 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 thegetElements
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]; } }
- Now that the provider methods are implemented, connect the providers to the viewer and set the input data object in the
create
method of theTimeZoneTreeView
class:treeViewer.setLabelProvider(new TimeZoneLabelProvider()); treeViewer.setContentProvider(new TimeZoneContentProvider()); treeViewer.setInput(new Object[] {TimeZoneComparator.getTimeZones()});
- Run the Eclipse instance, open the view by navigating to Window | Show View | Timekeeping | Time Zone Tree View, and see the results:
- Optionally, create a
focus
method with a@Focus
annotation, which sets the focus on the viewer'scontrol
. The@Focus
annotation needs to be imported from theorg.eclipse.e4.ui.di
package:import org.eclipse.e4.ui.di.Focus; ... @Focus public void focus() { treeViewer.getControl().setFocus(); }
- 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.