blob: 9301439fdfae65fa49a5605c86554c1c16572a6f [file] [log] [blame]
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 3.2//EN">
<html>
<head>
<title>How to use the JFace Tree Viewer</title>
<link rel="stylesheet" href="default_style.css">
</head>
<body LINK="#0000ff" VLINK="#800080">
<div align="right">&nbsp; <font face="Times New Roman, Times, serif" size="2">Copyright
&copy; 2002 Chris Grindstaff.</font>
<table border=0 cellspacing=0 cellpadding=2 width="100%">
<tr>
<td align=LEFT valign=TOP colspan="2" bgcolor="#0080C0"><b><font face="Arial,Helvetica"><font color="#FFFFFF">&nbsp;Eclipse
Corner Article</font></font></b></td>
</tr>
</table>
</div>
<div align="left">
<h1><img src="images/Idea.jpg" height=86 width=120></h1>
</div>
<p>&nbsp;</p>
<h1 align="CENTER">How to use the JFace Tree Viewer</h1>
<blockquote>
<b>Summary</b>
<br>
The goal of this article is to teach you how to use TreeViewers in your Eclipse
plug-ins or stand-alone JFace/SWT applications. We&#8217;ll start with a simple
example and progressively add functionality.
<p><b> By Chris Grindstaff, Applied Reasoning</b> (<font face="arial,helvetica,geneva" size=-1>chrisg
at appliedReasoning.com)</font><br>
<font size="-1">May 5, 2002</font> </p>
</blockquote>
<hr width="100%">
<h2>Introduction</h2>
<p>
The TreeViewer is used throughout Eclipse. In fact,
trees are pervasive in most applications.
This article will explain the following information about tree viewers:
</p>
<ul>
<li>how the tree viewer fits into the JFace UI toolkit</li>
<li>how to populate a tree viewer</li>
<li>how to listen for events generated by the tree viewer</li>
<li>how to select items in the tree viewer</li>
<li>how to filter out items</li>
<li>how to control the order of items in the tree viewer.</li>
</ul>
<p>
The tree viewer can be used in Eclipse plug-ins or in a stand alone
JFace/SWT application. We&#8217;ve chosen to implement a plug-in that
demonstrates the tree viewer functionality.
</p>
<p>
<a name="mainView"></a>
Here&#8217;s an example of what the plugin provides:
</p>
<p>
<p align="center"><img alt="main.gif (4K)" src="images/main.gif" height="350" width="230" align="middle"></p>
<p></p>
<p>
As you work through the article you may notice the
occasional <img alt="tryit.gif (1K)" src="images/tryit.gif" height="13" width="61">. These
icons indicate points where you should start up Eclipse and try the example.
</p>
<h2>
Source Code
</h2>
<p>
To run the examples or view the source for this article,
unzip <a href="cbg.article.treeviewer.zip">cbg.article.treeviewer.zip</a>
into your <i>eclipse_root</i> directory and restart Eclipse. In Windows
the <i>eclipse_root</i> directory will look something like <i>d:\apps\eclipse\</i>.
Once Eclipse is restarted, select the Perspective | Show View |
Other&#8230; menu option. In the &#8220;Other&#8221;
category select &#8220;Moving Box.&#8221;
</p>
<p>
<p align="center"><img alt="perspectiveMenu.jpg (21K)" src="images/perspectiveMenu.jpg" height="272" width="383"></p>
<p></p>
<p>
<img alt="tryit.gif (1K)" src="images/tryit.gif" height="13" width="61">
</p>
<p>
This article will be most effective if you are able to run the plugin we&#8217;ve
provided as you read it.
</p>
<h2>
Big Picture
</h2>
<p>
To understand how to use the TreeViewer, it is important
to understand where the TreeViewer fits into JFace as a
whole, and how JFace fits into Eclipse.
</p>
<p>
JFace is a UI toolkit that helps solve
common UI programming tasks. JFace also acts as a bridge
between low-level SWT widgets and your domain objects.
SWT widgets interact with the host operating system and
as such have no knowledge of your domain objects.
</p>
<p>
One of the ways that JFace bridges the gap between SWT
widgets and domain models is through viewers. JFace
viewers consist of an SWT widget (e.g. Tree, Table, etc), plus
your domain objects. You provide the viewers with the
information they need in order to populate the underlying
SWT widget. The viewer is able to sort and filter your domain objects,
as well as update the widget when your domain objects change.
</p>
<p>
More information about JFace and SWT can be found in the
Javadoc for org.eclipse.jface.viewers,
org.eclipse.swt.widgets, org.eclipse.swt.custom and
<a href="http://www.eclipse.org/documentation/html/plugins/org.eclipse.platform.doc.isv/doc/guide/jface_viewers.htm">JFace viewers</a>.
</p>
<h2>
Setting the Stage
</h2>
<p>
Because the TreeViewer uses domain objects to populate its Tree,
we will create a simple model to be used throughout
this article.
</p>
<p>
Suppose you've bought a new house and it's time to pack up your
stuff for the move. To keep all of your books and board
games organized you decide to employ a TreeViewer to help
you maintain order. (Yes, you're a geek.)
</p>
<p>
Here are our domain objects:
</p>
<ul type="disc">
<li>
MovingBox
</li>
</ul>
<p>
A moving box can contain other moving boxes, books, and
board games. A moving box may or may not have a name.
</p>
<ul type="disc">
<li>Book</li>
<li>Board game</li>
</ul>
<p>
Each book and board game has both a title and an author.
</p>
<h2>
Build a Simple TreeViewer
</h2>
<p>
We&#8217;ll start with a simple TreeViewer example and
build on it throughout the article.
</p>
<p>
Here&#8217;s the code that creates this <a href="#mainView">view</a> depicted earlier.
</p>
<blockquote>
<a name="sampleCode"></a><pre>treeViewer = new TreeViewer(parent);
treeViewer.setContentProvider(new MovingBoxContentProvider());
treeViewer.setLabelProvider(new MovingBoxLabelProvider());
treeViewer.setInput(getInitalInput());
treeViewer.expandAll();
</pre></blockquote>
<h2>
TreeViewer Domain Model Interactions
</h2>
<p>
It is crucial to understand the model/view relationship as utilized by
JFace viewers. Conceptually, all viewers perform two primary tasks:
</p>
<ul>
<li>they help adapt your domain objects into viewable entities</li>
<li>they provide notifications when the viewable entities are selected or
changed through the UI</li>
</ul>
<p>
More specifically, when working with a tree viewer, you
use your domain objects as arguments in the API methods.
For example, you can
add a book to a moving box by calling the
TreeViewer.add(aMovingBox, aBook) method. You don&#8217;t
need to translate your domain objects into UI elements;
the tree viewer does that for you. You make your "root" domain object
avaliable to the tree viewer by invoking the TreeViewer&#8217;s <b>setInput</b>
method. Your domain object then becomes the tree viewer&#8217;s input.
</p>
<p>
Likewise, when you ask the tree viewer for the selected
objects, it will answer with the domain objects - not the underlying
UI resources.
</p>
<h2>
Content Provider
</h2>
<p>
When working with a tree viewer, you need to provide the
viewer with information on how to <i>transform</i> your
domain object into an item in the UI tree. That is the
purpose of an <code>ITreeContentProvider</code>. One of your domain
classes could implement this interface. Instead of "polluting" your
domain objects with user interface code, you may want to create
another object to fulfill the tree content provider requirements.<br><br>
Let&#8217;s take a look at some of this interface&#8217;s
methods.
</p>
<p>
<ul type="disc">
<li>
<code>public Object[] getElements(Object inputElement)</code>
</li>
</ul>
This is the method invoked by calling the <b>setInput</b> method on
the tree viewer. In fact, the <b>getElements</b> method is called only in
response to the tree viewer's <b>setInput</b> method and should answer with
the appropriate domain objects of the inputElement.
The <b>getElements</b> and <b>getChildren</b> methods operate in a similar way.
Depending on your domain objects, you may have the <b>getElements</b> simply
return the result of calling <b>getChildren</b>. The two methods are kept
distinct because it provides a clean way to differentiate between the root
domain object and all other domain objects.
<p></p>
<ul type="disc">
<li>
<code>public Object[] getChildren(Object parent)</code>
</li>
</ul>
<p>
The tree viewer calls its content provider&#8217;s
getChildren method when it needs to create or display the
child elements of the domain object, <b>parent</b>. This method
should answer an array of domain objects that represent
the unfiltered children of <b>parent</b> (more on filtering later).
</p>
<ul type="disc">
<li>
<code>public Object getParent(Object element)</code>
</li>
</ul>
<p>
The tree viewer calls its content provider&#8217;s
getParent method when it needs to reveal collapsed domain objects programmatically
and to set the expanded state of domain objects. This method should answer the parent of the
domain object element.
</p>
<p>
<ul type="disc">
<li><code>public boolean hasChildren(Object element)</code></li>
</ul>
<p></p>
<p>
The tree viewer asks its content provider if the domain
object represented by <b>element</b> has any children. This
method is used by the tree viewer to determine whether or
not a plus or minus should appear on the tree widget.
</p>
<ul type="disc">
<li>
<code>public void
inputChanged</code><code>(Viewer viewer, Object
oldInput, Object newInput)</code>
</li>
</ul>
<p>
This method really serves two slightly different purposes.
This method is invoked when you set the tree viewer's input. In other
words, anytime you change the tree viewer's input via the setInput
method, the inputChanged method will be called. This will often be
used to register the content provider as a recipient of domain changes
and to de-register itself from the old domain object.<br><br>
In big picture terms, input objects for the content provider are managed
through the viewer.<br><br>
The method is also called when the tree viewer wants
to inform the content provider that it's no longer the tree viewer's
content provider. This happens when you set the tree viewer's content
provider.
</p>
<p>
For example, our moving box notifies its listeners
whenever a book, board game or box is added to or removed
from it. The content provider will register as a listener
for these domain model changes so it can update its UI
when the domain changes. Likewise, it will want to remove
itself as a listener from domain objects that are no
longer being viewed.
</p>
<p>
Enough discussion about the abstract APIs. Let&#8217;s
take a look at the actual code used in this example. In
the code provided <a href="#sampleCode">above</a>
the tree viewer&#8217;s content provider was set to an
instance of <code>MovingBoxContentProvider</code>.
Let&#8217;s take a look at that class in detail.
</p>
<p>
<blockquote>
<pre>
public Object[] getChildren(Object parentElement) {
if(parentElement instanceof MovingBox) {
MovingBox box = (MovingBox)parentElement;
return concat(box.getBoxes().toArray(),
box.getBooks().toArray(), box.getGames().toArray());
}
return EMPTY_ARRAY;
}</pre></blockquote>
<p></p>
<p>
Only MovingBox domain objects can have children, so an
empty array is returned for any other object. A moving
box&#8217;s children are a concatenation of its moving
boxes, plus its books, plus its board games.
</p>
<p>
<blockquote>
<pre>
public Object[] getElements(Object inputElement) {
return getChildren(inputElement);
}</pre></blockquote>
<p></p>
<p>
The getElements method is used to obtain the root
elements for the tree viewer. In our case,
the tree viewer's root object is a moving box so we can simply call
the getChildren method since it handles MovingBoxes. If the root
domain object was <i>special</i> in some way, the getElement method
would likely not delegate to the getChildren method.
</p>
<p>
<blockquote>
<pre>
public Object getParent(Object element) {
if(element instanceof Model) {
return ((Model)element).getParent();
}
return null;
}</pre></blockquote>
<p></p>
<p>
The getParent method is used to obtain the parent of the
given element. In our example, the Model class is the common superclass
of moving boxes, books and board games and defines a
child/parent relationship.
</p>
<p>
<blockquote>
<pre>
public boolean hasChildren(Object element) {
return getChildren(element).length > 0;
}</pre></blockquote>
<p></p>
<p>
The hasChildren method is invoked by the tree viewer when
it needs to know whether a given domain object has
children.
</p>
<a name="inputChanged"></a>
<p>
<blockquote>
<pre>
public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
this.viewer = (TreeViewer)viewer;
if(oldInput != null) {
removeListenerFrom((MovingBox)oldInput);
}
if(newInput != null) {
addListenerTo((MovingBox)newInput);
}
}</pre></blockquote>
<p></p>
<p>
The <b>input changed</b> method caches the viewer argument for later
use when responding to events. Recall that the <b>inputChanged</b>
method will be called when the tree viewer's setInput method is
invoked. When the tree viewer's input is changed, we need to make
sure that we remove any listeners we have associated with the
old input so that we no longer receive updates from the stale input.
Likewise, we need to listen for changes in the new input. Typically
the content provider adds itself as a listener to domain object
changes. This way when the domain object changes the content provider
is notified and can in turn, notify the tree viewer.
Here is the addListenerTo method. (The removeListenerFrom method is
almost exactly the same so we'll ignore it.)
</p>
<blockquote>
<pre>
/** Because the domain model does not have a richer
* listener model, recursively add this listener
* to each child box of the given box. */
protected void addListenerTo(MovingBox box) {
box.addListener(this);
for (Iterator iterator = box.getBoxes().iterator(); iterator.hasNext();) {
MovingBox aBox = (MovingBox) iterator.next();
addListenerTo(aBox);
}
}
</pre></blockquote>
<p>
This method simply finds all the moving boxes and adds the content viewer
as a listener; therefore, if any of the moving boxes change, the content
viewer will be notified. Later in the article we'll show how the content
viewer responds to these domain changes.
</p>
<h2>
Label Provider
</h2>
<p>
The label provider is responsible for providing an image
and text for each item contained in the tree viewer. As
with the content provider, the label provider accepts
domain objects as its arguments. It is important that
instances of label providers are not shared between tree viewers
because the label provider will be disposed when the
viewer is disposed.
</p>
<p>
The tree viewer makes use of the ILabelProvider interface
to provide these services. Let&#8217;s take a look
at the interface&#8217;s two methods.
</p>
<p>
<code>public Image getImage(Object element)</code>
</p>
<p>
This method answers an SWT <code>Image</code> to be used
when displaying the domain object, <b>element</b>. If the domain
object does not have a corresponding image, you can
answer null. Because images use OS resources you need to
be careful to dispose of them when you are no longer using them.
This is often accomplished by caching
the images in the label provider or the plug-in class and
disposing of them when the viewer is disposed.
</p>
<p> Another important point to keep in mind is that the tree viewer will scale
your images if they are different sizes. The first image returned from this
method will be the "standard" tree viewer image size. All other images will
be scaled up or down to match the size of this first image. These plus and minus
images used in the tree viewer to show expanded and collapsed items are also
scaled to the "standard" size. <br>
<br>
A safe size to use for your images is 16x16.
</p>
<p>
<code>public String getText(Object element)</code>
</p>
<p>
The getText method answers a string that represents the
label for the domain object, element. If there is no
label for the element, answer null.
</p>
<p>
<code>public void dispose()</code>
</p>
<p>
The dispose method is called when the tree viewer that
contains the label provider is disposed. This method is often used
to dispose of the cached images managed by the
receiver.
</p>
<p>
In
the code listed <a href="#sampleCode">above</a>,
the tree viewer&#8217;s label provider was set to an
instance of <code>MovingBoxLabelProvider</code>.
Let&#8217;s take a look at this class in detail.
</p>
<p>
<blockquote>
<pre>
public Image getImage(Object element) {
ImageDescriptor descriptor = null;
if (element instanceof MovingBox) {
descriptor = TreeViewerPlugin.getImageDescriptor("movingBox.gif");
} else if (element instanceof Book) {
descriptor = TreeViewerPlugin.getImageDescriptor("book.gif");
} else if (element instanceof BoardGame) {
descriptor = TreeViewerPlugin.getImageDescriptor("gameboard.gif");
} else {
throw unknownElement(element);
}
//obtain the cached image corresponding to the descriptor
Image image = (Image)imageCache.get(descriptor);
if (image == null) {
image = descriptor.createImage();
imageCache.put(descriptor, image);
}
return image;
}
</pre></blockquote>
<p></p>
<p>
This method answers the correct image for the given
domain object, <b>element</b>. An ImageDescriptor is used to
load the image for the corresponding domain
object. If an image has not yet been
loaded, the image descriptor is asked to create the image. The 
<code>getImageDescriptor</code><code>(String)</code>
method is shown below. This method will often be
implemented as a static method on the plug-in class since
it is a generic utility method and makes use of the
plug-in&#8217;s installURL.
</p>
<p>
<blockquote>
<pre>
public static ImageDescriptor getImageDescriptor(String name) {
String iconPath = "icons/";
try {
URL installURL = getDefault().getDescriptor().getInstallURL();
URL url = new URL(installURL, iconPath + name);
return ImageDescriptor.createFromURL(url);
} catch (MalformedURLException e) {
// should not happen
return ImageDescriptor.getMissingImageDescriptor();
}
}</pre></blockquote>
<p></p>
<p><blockquote>
<pre>
public void dispose() {
for (Iterator i = imageCache.values().iterator(); i.hasNext();) {
((Image) i.next()).dispose();
}
imageCache.clear();
}</pre></blockquote>
<p></p>
<p>
This dispose method makes sure the label provider correctly
disposes of the images it has created. This is very
important to insure that operating system resources are
not &#8220;leaked.&#8221;
</p>
<h2>
Setting the Initial Input
</h2>
<p>
Now that we&#8217;ve discussed the content provider and the
label provider, we are nearly finished with the code
required to create the tree viewer. The one step missing from the example code
is setting the initial input for the tree viewer. As it stands now, our
tree viewer does not contain any domain objects.
Here&#8217;s the code used to set the initial input of
the tree viewer.
</p>
<p>
<code>treeViewer.setInput(getInitalInput());</code>
</p>
<p>
When you set the tree viewer&#8217;s input, the tree
viewer works in conjunction with the content and label
providers to display the tree. Also, remember our content
provider&#8217;s <code><a href=
"#inputChanged">inputChanged(&#8230;)</a></code> method
will be invoked whenever the tree viewer&#8217;s setInput
method is called.
</p>
<p>
The getInitalInput() method simply creates our sample set
of domain objects.
</p>
<p>
<blockquote>
<pre>
public MovingBox getInitalInput() {
MovingBox root = new MovingBox();
MovingBox books = new MovingBox("Books");
MovingBox games = new MovingBox("Games");
MovingBox books2 = new MovingBox("More books");
MovingBox games2 = new MovingBox("More games");
root.addBox(books);
root.addBox(games);
root.addBox(new MovingBox());
books.addBox(books2);
games.addBox(games2);
books.addBook(new Book("The Lord of the Rings", "J.R.R.", "Tolkien"));
books.addBoardGame(new BoardGame("Taj Mahal", "Reiner", "Knizia"));
books.addBook(new Book("Cryptonomicon", "Neal", "Stephenson"));
books.addBook(new Book("Smalltalk, Objects, and Design", "Chamond", "Liu"));
books.addBook(new Book("A Game of Thrones", "George R. R.", " Martin"));
books.addBook(new Book("The Hacker Ethic", "Pekka", "Himanen"));
//books.addBox(new MovingBox());
books2.addBook(new Book("The Code Book", "Simon", "Singh"));
books2.addBook(new Book("The Chronicles of Narnia", "C. S.", "Lewis"));
books2.addBook(new Book("The Screwtape Letters", "C. S.", "Lewis"));
books2.addBook(new Book("Mere Christianity ", "C. S.", "Lewis"));
games.addBoardGame(new BoardGame("Tigris & Euphrates", "Reiner", "Knizia"));
games.addBoardGame(new BoardGame("La Citta", "Gerd", "Fenchel"));
games.addBoardGame(new BoardGame("El Grande", "Wolfgang", "Kramer"));
games.addBoardGame(new BoardGame("The Princes of Florence", "Richard", "Ulrich"));
games.addBoardGame(new BoardGame("The Traders of Genoa", "Rudiger", "Dorn"));
games2.addBoardGame(new BoardGame("Tikal", "M.", "Kiesling"));
games2.addBoardGame(new BoardGame("Modern Art", "Reiner", "Knizia"));
return root;
}</pre></blockquote>
<p></p>
<h2>
Selection
</h2>
<p>
In most applications, the user selects an item from the tree viewer in
order to perform some specific action. You can be notified of these
selections by adding a selection change listener
to the tree viewer. When a selection occurs in the tree
viewer, it will notify each of its selection change
listeners, passing along a selection event describing
what has been selected. Note that these events flow in the opposite
direction of the model-generated events we discussed earlier.<br><br>
Here&#8217;s an example of handling selection in the tree viewer.
</p>
<p> <a name=
"selectionChanged"></a> </p>
<blockquote>
<pre>
treeViewer.addSelectionChangedListener(new ISelectionChangedListener() {
public void selectionChanged(SelectionChangedEvent event) {
// if the selection is empty clear the label
if(event.getSelection().isEmpty()) {
text.setText("");
return;
}
if(event.getSelection() instanceof IStructuredSelection) {
IStructuredSelection selection = (IStructuredSelection)event.getSelection();
StringBuffer toShow = new StringBuffer();
for (Iterator iterator = selection.iterator(); iterator.hasNext();) {
Object domain = (Model) iterator.next();
String value = labelProvider.getText(domain);
toShow.append(value);
toShow.append(", ");
}
// remove the trailing comma space pair
if(toShow.length() > 0) {
toShow.setLength(toShow.length() - 2);
}
text.setText(toShow.toString());
}
}
});
</pre>
</blockquote>
<p></p>
<p>
Here an anonymous class selection changed listener is
created. The selection changed method is implemented to
display the currently selected items in a label or to
clear the label if no items are selected. A few points to
note:
</p>
<ul type="disc">
<li>
Because the selection changed listener method is
defined in a very generic way, some casting is required.
For example, a tree viewer&#8217;s selection will
always be an <code>IStructuredSelection</code> even
though the <code>SelectionChangedEvent</code>&#8217;s
getSelection method will answer the more generic
<code>ISelection</code>.
</li>
<li>
We are using the label provider&#8217;s getText()
method to assist us in converting the domain objects to
a textual representation to display in the label.
</li>
<li>
In most UI frameworks, the selection-changed listener
will be called by the application&#8217;s UI thread.
SWT is no exception; this means you should be careful
to return from listener methods in a timely fashion.
</li>
</ul>
<p>
<img alt="tryit.gif (1K)" src="images/tryit.gif" height="13" width="61">
</p>
<p>
Open the &#8220;Moving Box&#8221; view and select one or
more of the items in the tree. You should see the status
label update with the names of the selected items like
this:
</p>
<p>
<p align="center"><img alt="selection.jpg (8K)" src="images/selection.jpg" height="120" width="214"></p>
<p></p>
<h2>
Responding to Change
</h2>
<p>
Applications aren&#8217;t very useful if they don&#8217;t
handle change. Conceptually, change can be described in a couple
of ways. When domain objects are changed, the UI
usually reflects these changes. Likewise, user actions in the UI may
require updates in the domain objects.
</p>
<h3>
Responding to domain model changes
</h3>
<p>
When a change occurs in the domain model, the UI needs
to reflect that change. For example, if a new
book is added to one of our moving boxes
programmatically, we want the UI to show the newly added book.
</p>
<p>
While we want the domain to notify the UI through some
means, we do not want to &#8220;pollute&#8221; the domain
objects with knowledge about the UI. If the model and the
view are too strongly coupled, each becomes brittle and
fragile to change. We employ an Observer or
Event-Notification style pattern to break this strong
coupling.
</p>
<p>
In our example, we achieve this by creating a listener
interface that our domain objects notify when an
interesting change occurs. Now we need to provide
an object to listen for the changes.
</p>
<p>
Typically that object will be the tree viewer&#8217;s
content provider. Remember the <a href=
"#inputChanged">inputChanged</a>() method? Our
inputChanged method will register itself as a listener to
the domain object changes so it can notify the tree
viewer of any changes.
</p>
<p>
Typically the tree viewer will be notified of domain
object changes by calling one of the update methods.
These are important methods, so let&#8217;s take a look at each
of them in more detail.
</p>
<h4>
<a name="refeshAndUpdateDifference"></a>What&#8217;s the
difference between refresh and update?
</h4>
<p>
The tree viewer provides both a refresh and an update
method. What is the difference between these two and when
should you use one or the other?
</p>
<h5>
Refresh method
</h5>
<p>
The refresh method refreshes the domain object and all of
the domain object&#8217;s children. The tree viewer
updates the label for the object passed to the refresh
method. The tree viewer also asks its content viewer for
the children of the object passed to the refresh method
and recursively updates each of them by collaborating
with the label and content providers.
</p>
<p>
In addition to this version of refresh, there is also a
version that allows you to specify whether or not the
labels of existing elements should be updated.
</p>
<h5>
Update method<a name="update"></a>
</h5>
<p>
The update method simply updates the given domain object&#8217;s
label. In other words, if the domain object passed to
the update method contained new children, those new
children would not appear in the tree viewer by invoking
the update method. Invoking update will only update the
domain object&#8217;s label or image.
</p>
<h6>
Update properties
</h6>
<p>
In addition to this behavior, the update method also
provides a means to specify which &#8220;parts&#8221; of
the domain object changed. If you choose to specify these
sub-parts, the tree viewer may be able to optimize the
update. If no sub-properties are specified, a full update
of the element is performed.
</p>
<p>
In particular, when you call the update method the
following decisions are made:
</p>
<ul type="disc">
<li>
Does the updated domain object need a new label and
image?
</li>
</ul>
<p>
The tree viewer collaborates
with your label provider in order to answer this question.
By default, all label providers
answer that they should be updated when a given property
of the domain object changes. You can override the
<code>isLabelProperty(Object element, String
property)</code> method to specify otherwise.
</p>
<ul type="disc">
<li>
If the tree viewer contains any filters, it asks each
filter if the updated domain object needs to be filtered
out of the tree.
</li>
</ul>
<p>
The same rules apply here. Each viewer filter can specify
whether or not it is affected by a change in the given
property. By default, viewer filters answer false but you
can override the <code>isFilterProperty(Object element,
String property)</code> method to specify otherwise.
</p>
<ul type="disc">
<li>
If the tree viewer contains a sorter, it asks the
sorter if the updated domain object needs to be
re-sorted.
</li>
</ul>
<p>
The same rules apply again. The sorter can specify whether or
not it is affected by a change in the given property. By
default, viewer sorters answer false but you can override
the <code>isSorterProperty(Object element, String
property)</code> method to specify otherwise.
</p>
<h4>Example of Responding to Domain Object Additions and Removals</h4>
<p>
Let's take a look at how to respond to domain object changes. Let's
add a new book to one of the moving boxes. We will add this book in
response to a button push. It is
important to understand, however, that this domain change could come
from "anywhere" - a database trigger, a background thread or some other
program. We use a button press to keep it simple.
</p>
<img alt="tryit.gif (1K)" src="images/tryit.gif" height="13" width="61">
<p>
When you press the "Add New Book" button on the view's toolbar, a
book will be added to the selected moving box. What happens when the
button is pressed? When the SWT button is presed the OS generates an event
that is forwarded to the button's selection listener. The selection listener
method adds a new book to the selected moving box. When a new book is
added, the moving box will notify its listeners that a domain change has
occurred. Since we added the content provider as a domain listener, it will
be notified of the change. The content provider then updates the tree viewer.
</p>
<img alt="addBook.jpg (5K)" src="images/addBook.jpg" height="55" width="415">
<p>
If nothing is selected, the book will be added to the topmost moving
box.
</p>
<p>Let's take a look at the code.
</p>
<blockquote>
<pre>
protected void addNewBook() {
MovingBox receivingBox;
if (treeViewer.getSelection().isEmpty()) {
receivingBox = root;
} else {
IStructuredSelection selection = (IStructuredSelection) treeViewer.getSelection();
Model selectedDomainObject = (Model) selection.getFirstElement();
if (!(selectedDomainObject instanceof MovingBox)) {
receivingBox = selectedDomainObject.getParent();
} else {
receivingBox = (MovingBox) selectedDomainObject;
}
}
receivingBox.add(Book.newBook());
}
</pre></blockquote>
<p>
The addNewBook method is called when the "Add a New Book" toolbar button
is pressed. Notice that if multiple items are selected, we ignore all of them
except for the first one. Also notice that if the selected item is not a moving box,
we ask the selected item for its parent, which will be a moving box. Once
we have the moving box domain object, we tell it to add a new book.
</p>
<p>
The moving box's add method is not very interesting. It simply adds the
book and notifies its listeners that a book has been added. When this
notification occurs, the content viewer will be informed since it
<a href="#inputChanged">registered</a> itself as a listener for these
domain changes. Let's take a look at how the content provider services
the addition event notification. Here's the content provider's listener
method that is called on additions.
</p>
<blockquote>
<pre>
public void add(DeltaEvent event) {
Object movingBox = ((Model)event.receiver()).getParent();
viewer.refresh(movingBox, false);
}
</pre></blockquote>
<p>
Pretty simple, right? The content provider asks the event for the receiver,
which is the newly added book. Next, the content provider asks the
tree viewer to refresh the moving box that contains the newly added
book. Remember that refresh will cause the tree viewer to consult with
its content provider to supply a list of children for the refresh
object. The false argument is passed along to the refresh method
to let it know that we do not need the labels of the other objects in
the tree refreshed.
</p>
<p>Removing something from the tree works in exactly the same way. We
remove the domain object from the moving box and ask the tree viewer
to acquire its model objects from the content provider again.
</p>
<h3>
Responding to UI changes
</h3>
<p>
We also need to respond to changes made in the UI. Often
these UI changes cause a domain object to change. For
example, if we edit the author of one of the books in the
tree viewer, we need to make sure this change is
correctly propagated to the book domain object.
</p>
<p>
Typically, you will create listeners and add them to the
tree viewer. Your listener methods will retrieve the necessary
domain objects from the tree viewer or passed event
object.
</p>
<p>
An example of this technique was shown in the
selectionChanged method. Here's part of that method again.
</p>
<p>
<blockquote>
<pre>
public void selectionChanged(SelectionChangedEvent event) {
// if the selection is empty clear the label
if(event.getSelection().isEmpty()) {
text.setText("");
return;
}
if(event.getSelection() instanceof IStructuredSelection) {
IStructuredSelection selection = (IStructuredSelection)event.getSelection();
StringBuffer toShow = new StringBuffer();
for (Iterator iterator = selection.iterator(); iterator.hasNext();) {
<img SRC="images/tag_1.gif" BORDER=0 height=13 width=24>Object domain = (Model) iterator.next();
String value = labelProvider.getText(domain);
toShow.append(value);
toShow.append(", ");
}
// remove the trailing comma space pair
if(toShow.length() > 0) {
toShow.setLength(toShow.length() - 2);
}
text.setText(toShow.toString());
}
</pre></blockquote>
<p></p>
<p>
<img SRC="images/tag_1.gif" BORDER=0 height=13 width=24> In this example, we're retrieving the
domain object from the event object's selection. We could have just as easily asked the tree viewer
for the currently selected objects.
</p>
<p>
Here is a list of listeners you can add to a tree viewer
along with a description of each of them.
</p>
<table border="1" cellspacing="3" cellpadding="0" width="595">
<tr>
<td valign="top">
<p align="center">
<b>Listener Type</b>
</p>
</td>
<td valign="top">
<p align="center">
<b>Description</b>
</p>
</td>
</tr>
<tr>
<td valign="top">
<p>
<code>addHelpListener(HelpListener)</code>
</p>
</td>
<td valign="top">
<p>
Responds to help requests
</p>
</td>
</tr>
<tr>
<td valign="top">
<p>
<code>addSelectionChangedListener</code><code>(ISelectionChangedListener)</code>
</p>
</td>
<td valign="top">
<p>
Responds to selection changes
</p>
</td>
</tr>
<tr>
<td valign="top">
<p>
<code>addDoubleClickListener</code><code>(IdoubleClickListener)</code>
</p>
</td>
<td valign="top">
<p>
Responds to double clicks
</p>
</td>
</tr>
<tr>
<td valign="top">
<p>
<code>addDragSupport</code><code>(int, Transfer[],
DragSourceListener)</code>
</p>
</td>
<td valign="top">
<p>
Sets up a listener to support dragging items out of
the tree viewer
</p>
</td>
</tr>
<tr>
<td valign="top">
<p>
<code>addDropSupport</code><code>(int, Transfer[],
DropTargetListener)</code>
</p>
</td>
<td valign="top">
<p>
Sets up a listener to support dropping objects in
tree viewer
</p>
</td>
</tr>
<tr>
<td valign="top">
<p>
<code>addTreeListener</code><code>(ItreeViewerListener)</code>
</p>
</td>
<td valign="top">
<p>
Responds to expand and collapse events
</p>
</td>
</tr>
</table>
<h2>
<a name="viewFilters"></a><a name=
"viewerFilters"></a>Viewer Filters
</h2>
<p>
Viewer filters are used by a tree viewer to extract a
subset of the domain objects provided by the content
provider. You add and remove viewer filters from the tree
viewer.
</p>
<p>
Viewer filters are additive, which means that the output of
one filter is passed in as the input of the next filter.
The final result has been filtered through each view
filter.
</p>
<p>
ViewerFilter is an abstract class. In order to implement
a viewer filter you subclass this class and override a
single method, <code>select(Viewer, Object,
Object)</code>. The method should answer true if
the domain object makes it through the filter. The select
method also passes the domain object&#8217;s
parent.
</p>
<p>
If you need more sophisticated filtering, you may override
other methods in ViewFilter to refine how the filtering
is accomplished. In particular the <code>isFilterProperty(Object
domain, String property)</code> method can be used to
answer whether or not the particular filter is interested in a
<b>property</b> change to the <b>domain</b> object.
</p>
<p>
The <code>isFilterProperty</code> method is used in
conjunction with the tree viewer&#8217;s update method.
More information about the update method can be found <a
href="#update">here</a>.
</p>
<p>
Let&#8217;s build a view filter that shows only the board
games contained in our moving boxes.
</p>
<p>
When building filters, it&#8217;s important to keep in
mind the content providers and the input element for the viewer. For
example, if we use the following filter, we&#8217;ll have
problems.
</p>
<p>
<blockquote>
<pre>
public boolean select(Viewer viewer, Object parentElement, Object element) {
return element instanceof BoardGame;
}
</pre></blockquote>
<p></p>
<p>
The problem with this example is that we will filter out our parent.
In other words, this filter rejects moving boxes, but
remember that moving boxes are what contain board games. So if
we filter out moving boxes, the content provider will not
have anything left to ask for content.
</p>
<p>
We can solve this by changing our filter to
this:
</p>
<p>
<blockquote>
<pre>
public boolean select(Viewer viewer, Object parentElement, Object element) {
return element instanceof BoardGame || element instanceof MovingBox;
}
</pre></blockquote>
<p></p>
<p>
This filter works as intended, leaving only board games
visible in the tree viewer.
</p>
<p>
Let&#8217;s build another filter that both augments and
works independently from the board game filter. The second
filter will show only the contents of moving boxes that
contain at least 3 items. Here&#8217;s the code for our
ThreeItemFilter:
</p>
<p>
<blockquote>
<pre>
public boolean select(Viewer viewer, Object parentElement, Object element) {
return parentElement instanceof MovingBox && ((MovingBox)parentElement).size() >= 3;
}
</pre></blockquote>
<p></p>
<p>
Coupled with the first filter, this filter can be used
to show all of the board games contained in moving boxes
with three or more items in them.
</p>
<p>
Another important point to remember with filters is that
they also apply to the root of the tree viewer. In other words, if the
root does not contain at least three
items, then you will never get around to filtering the
children.
</p>
<p>
<img alt="tryit.gif (1K)" src="images/tryit.gif" height="13" width="61">
</p>
<p>
Open the &#8220;Moving Box&#8221; view and select the
triangle near the edge of the window. Then choose the
filters and verify that they are working.
</p>
<p>
<p align="center"><img alt="filters.jpg (13K)" src="images/filters.jpg" height="117" width="482"></p>
<p></p>
<h2>
<a name="viewerSorters"></a>Viewer Sorters
</h2>
<p>
The tree viewer collaborates with a viewer sorter to order
the domain objects provided by the content provider.
</p>
<p>
In contrast to our discussion of viewer filters, a tree viewer may
utilize only one viewer sorter at a given time. You can always set a different view
sorter whenever you like, but at any specific moment, there is either
exactly one viewer sorter or none at all.
</p>
<p>
ViewerSorter is an abstract class that defines a default
sorting behavior. Conceptually, the only method that needs
to be implemented is the <code>public int compare(Viewer
viewer, Object e1, Object e2)</code>method. The
ViewerSorter class provides a default implementation of
this method that defines a default sorting behavior.
</p>
<p>
This default sorting behavior consists of grouping
each domain object into categories [by
invoking the public int category(Object
element) method] and then sorting each domain
object within a category by using the tree viewer&#8217;s
label content provider&#8217;s getText method.
</p>
<p>
By default the viewer sorter considers all domain
objects to be in the same category.
</p>
<p>
If this two-phase default sorting behavior makes
sense for your application, then you likely only need to
override the category method in order to place your domain objects
into their respective categories. Otherwise you&#8217;re
probably better off implementing the compare
method to sort in the appropriate manner for your
application. We&#8217;ll take a look at both
approaches.
</p>
<p>
As with the view filter, the view sorter contains an
<code>isSorterProperty</code> method that can be used to
answer whether or not the particular sorter would be
affected by a change to the given property for the given
domain object.
</p>
<p>
The <code>isSorterProperty</code> method is used in
conjunction with the tree viewer&#8217;s update method.
More information about the update method can be found <a
href="#update">here</a>.
</p>
<p>
Our first sorter will rely on the default sorting
behavior and implement a sorter that orders the items in
such a way that books appear before moving boxes, which
appear before board games. To achieve this we can simply
override the category method like this:
</p>
<p>
<blockquote>
<pre>
/** Orders the items in such a way that books appear
* before moving boxes, which appear before board games. */
public int category(Object element) {
if(element instanceof Book) return 1;
if(element instanceof MovingBox) return 2;
return 3;
}
</pre></blockquote>
<p></p>
<p>
The default sorting behavior uses the category method to
place objects into bins, and the bins are arranged in
ascending order.
</p>
<p>
Our second sorter will use the text labels to order the items in an article
independent fashion. This means that the literals (e.g.
&#8220;the&#8221;, &#8220;a&#8221;, &#8220;El&#8221; and
&#8220;La&#8221; will be ignored in terms of sorting. For
example &#8220;A Zoo&#8221; will sort after &#8220;The
Car.&#8221; This was not true for the first sorter.
</p>
<p>
The default sort method uses a simple case insensitive
sort for items within the same category. We will
override the compare method to implement an
article-ignoring sort.
</p>
<p>
Here&#8217;s our compare method (which is mostly a copy
of ViewerSorter&#8217;s method).
</p>
<p><blockquote>
<pre>
public int compare(Viewer viewer, Object e1, Object e2) {
int cat1 = category(e1);
int cat2 = category(e2);
if (cat1 != cat2) return cat1 - cat2;
String name1, name2;
if (viewer == null || !(viewer instanceof ContentViewer)) {
name1 = e1.toString();
name2 = e2.toString();
} else {
IBaseLabelProvider prov = ((ContentViewer)viewer).getLabelProvider();
if (prov instanceof ILabelProvider) {
ILabelProvider lprov = (ILabelProvider)prov;
name1 = lprov.getText(e1);
name2 = lprov.getText(e2);
} else {
name1 = e1.toString();
name2 = e2.toString();
}
}
if(name1 == null) name1 = "";
if(name2 == null) name2 = "";
name1 = stripArticles(name1);
name2 = stripArticles(name2);
return collator.compare(name1, name2);
}
</pre></blockquote>
<p></p>
<p>
This is a verbatim copy of the superclass&#8217;s method,
except for the addition of stripArticles near the bottom
of the method. The stripArticles method looks like this:
</p>
<p>
<blockquote>
<pre>
protected String stripArticles(String name) {
String test = name.toLowerCase();
if(test.startsWith("the ") && test.length() > 3) {
return name.substring(4);
} else if(test.startsWith("a ") && test.length() > 1) {
return name.substring(2);
} else if(test.startsWith("el ") && test.length() > 2) {
return name.substring(3);
} else if(test.startsWith("la ") && test.length() > 2) {
return name.substring(3);
}
return name;
}
</pre></blockquote>
<p></p>
<p>
This sorter does exactly what we want and shows an
example of overriding the compare method to implement the search.
</p>
<p>
<img alt="tryit.gif (1K)" src="images/tryit.gif" height="13" width="61">
</p>
<p>
Open the &#8220;Moving Box&#8221; view and select the
triangle near the edge of the window. Then choose the
sorters and verify that they work as you expected.
</p>
<p>
 
</p>
<p>
<p align="center"><img alt="sorters.jpg (15K)" src="images/sorters.jpg" height="144" width="501"></p>
<p></p>
<h2>
Conclusion
</h2>
<p>
This article has demonstrated the basic usage of tree
viewers. Hopefully, you now understand how to use the tree
viewer and understand where the tree viewer fits within
the larger JFace framework.
</p>
<p>
There are a number of topics planned for an
&#8220;advanced&#8221; tree viewer article, including
drag and drop, auto expand nodes
within the tree, in-place editing and making use of domain
object properties.
</p>
<h2>
Frequently Asked Questions
</h2>
<p>
Here is a list of frequently asked questions about using
the tree viewer.
</p>
<h3>
How do I add or remove an item from the tree?
</h3>
<p>
Addition and removal of items is essentially the same.
</p>
<p>
To remove an item from the tree viewer you should remove
the domain object from your model. Then ask
the tree viewer to refresh the parent of the domain
model. When the update method is called on the tree
viewer, it will ask its content provider for
the children of the parent of the domain model. However, since you
removed it from the parent, it will not be there and
subsequently will be removed from the view.
</p>
<p>
If your domain objects trigger events, you will add
the content provider as a listener so that whenever removals
occur in your domain object, the content provider can be
notified and perform the steps above.
</p>
<p>
What about the tree viewer's remove methods?
If you read the Javadoc for these
methods, they will tell you the remove methods should be
called by the <b>content provider</b>. This makes sense
after all, because the content provider is responsible
for supplying the content. If you removed the item from
the tree viewer without &#8220;going through&#8221; the
content provider, the item would likely reappear the next
time the parent of the item is refreshed.
</p>
<h3>
How do I programmatically select an item in the tree?
</h3>
<p>
Call the tree viewer&#8217;s setSelection(new
StructuredSelection(&lt;domain model&gt;), true).
</p>
<h3>
What is the difference between the tree viewer&#8217;s
update and refresh methods?
</h3>
<p>
See this <a href=
"#refeshAndUpdateDifference">discussion</a>.
</p>
<h3>
How do I reveal a newly added model object that is hidden?
</h3>
<p>
Let&#8217;s assume you&#8217;ve added the domain object
book to your moving box. The moving box is
already in the tree but isn&#8217;t expanded when you add
the book to it. You want the book to be revealed when
you add it to the moving box. You would do this:
</p>
<p>
<blockquote><pre>
treeViewer.setExpandedState(movingBox, true);
treeViewer.refresh(movingBox, false);
</pre></blockquote>
<p></p>
<h3>
My tree viewer hangs or isn&#8217;t very responsive
because I&#8217;m hitting a database in my content
provider. What can I do?
</h3>
<p>
In most UI frameworks, the listeners are called by the
application&#8217;s UI thread. SWT is no exception. This
means you should be careful to return from listener or
provider methods in a timely fashion. If you don&#8217;t,
the GUI will not receive events, which makes for a
sluggish and potentially useless UI.
</p>
<p>
If you have a long running operation in your listener or
provider methods, you should consider forking another
thread to do the actual work. For example, if you are
populating your tree from a database, you can&#8217;t have
your content provider waiting for the database to return
the domain objects. Instead you could have the content
provider return a stub object that shows something like
&#8220;Loading Data&#8230;&#8221; while the background
thread loads the real data.
</p>
<p>
Once the background thread has loaded the data, it would
need to refresh the parent of the stub object so that the
parent would request its domain objects again. Now
when the parent requests the domain objects, they already
exist since they&#8217;ve been loaded from the database.
</p>
<h3>
How do I add a viewer filter to a tree viewer?
</h3>
<p>
See this <a href="#viewerFilters">discussion</a>.
</p>
<h3>
How do I change the order of the items in a tree viewer?
</h3>
<p>
See this <a href="#viewerSorters">discussion</a>.
</p>
<h3>
What does the setUseHashlookup() method do?
</h3>
<p>
The setUseHashlookup(boolean) method informs the tree
viewer that it should maintain a hash mapping of your
domain object to the corresponding SWT TreeItem. Whenever
the tree viewer needs to manipulate the SWT tree items,
it uses the hash lookup or performs a recursive search
starting at the root looking for the item.
</p>
<p>
The tree viewer manipulates SWT tree items whenever you
add, remove, collapse, expand, reveal, refresh or update
a domain object. In other words, the tree viewer is
manipulating tree items all the time.
</p>
<p>
You should probably always setUseHashlookup to true.
The only downside to doing so is that the tree viewer will
require more memory since it&#8217;s maintaining the
mapping, but it should perform much faster with a large
set of domain objects.
</p>
<p>
Another important point about tree viewers - in general, they don&#8217;t
handle domain objects that
map to the same value. This is because the tree viewer
uses the equals() method and potentially the hashCode()
method if you use the setUseHashlookup method.
</p>
</body>
</html>