Julen Parra is a free-lance software consultant, obsessed with efficiency in software development. A lifetime programmer and analyst, has worked with companies in Germany and Spain, with the aim of improving their productivity. Since late 2002 he also collaborates in the QuantumDB project, an open source database plugin for the Eclipse platform. He's currently based in Madrid.
We encourage you to ask the author any questions or discuss the article here.
Working sets are static filters for resources. You define a working set as a list of resources (files, for example) and then only those resources are shown. For example, if you want to work in project A, and see project B for reference, you don't want to have your Package Explorer cluttered with the rest of the alphabet. By default, Eclipse offers working sets for Resources, Breakpoints, Java Resources and Plug-ins. If you have your own plug-in, and some view in it can be cluttered with information, you may want to define your own working set type, and filter your view based on it. This article aims to describe how to add a new resource type to the working sets already defined in Eclipse. (Note: This article was developed and tested against Eclipse 3.2.)
The QuantumDB plug-in is a database plug-in for Eclipse. Connections to databases are defined as bookmarks in the Bookmark View, which looks like:

With many bookmarks, the view can become cluttered, so some kind of filtering was needed. Each view can have its own filters, but may not easily be customisable by the user, so combining it with a working set allows the user to customise what they see. You can define several working sets (for example, “Production” and “Development”) and the working sets resources can overlap (for example, “Linux” and “Windows”). A filter can then be used to display only resources from specified working sets. This article explains how it was done.
The resources in this Database Bookmarks view are BookmarkNode objects,
a simple container adequate for use in Eclipse's content providers. They are uniquely identified
by name, as no duplicates are allowed. We'll define a
new working set for this kind of resources.
The org.eclipse.ui.workingsets extension point allows us to define a new
working set for BookmarkNodes. To create this, open the project's
plugin.xml file, and from the
Extensions tab create a new extension of the org.eclipse.ui.workingsets
extension point. (You can find a description of this extension point in the Eclipse
help). You need to provide five pieces of information:
com.quantum.ui.BookmarkWorkingSetQuantum Database BookmarksEclipse can define new classes (implementing the correct interfaces) by clicking on the
pageClass and updaterClass links.
The newly generated code is placed in
a new package com.quantum.ui.workingsets. The updater class is used to
keep track of changes to the working sets, which we'll revisit towards the end of this article.
The page class (called BookmarkWorkingSetPage in this example)
can be left with default fields, and having set the extension point up, we can run
a runtime workbench to test the code. You should see that in the Working Sets view
that a new working set type (Quantum Database Bookmarks) is listed in the
available resources. Of course, it you press the Next button then nothing happens;
that's because the newly-created BookmarkWorkingSetPage doesn't
have a default constructor, and so can't be instantiated. We can add one as follows:
/**
* Default constructor needed for instantiation by the framework
*/
public BookmarkWorkingSetPage() {
super("com.quantum.ui.BookmarkWorkingSet", "Select the bookmarks you want",
ImageStore.getImageDescriptor("bookmarks32.gif"));
}
To show something in the working set page, we need to implement the contents of
the dialog. To do this, we implement the createControl() method to
return a tree which shows all bookmarks with a checkbox next to them to allow us to
select individual bookmarks in the list. The control will contain something like:
treeViewer = new CheckboxTreeViewer(composite,
SWT.BORDER | SWT.H_SCROLL | SWT.V_SCROLL);
gd = new GridData(GridData.FILL_BOTH | GridData.GRAB_VERTICAL);
treeViewer.getControl().setLayoutData(gd);
treeViewer.setContentProvider(new BookmarkContentProvider());
treeViewer.setLabelProvider(new BookmarkLabelProvider());
treeViewer.setInput(BookmarkListNode.getInstance());
For more info on what each of these calls do, see this illustrative
article. BookmarkContentProvider, BookmarkLabelProvider and
BookmarkListNode are previously-defined classes that populate the Bookmark
view and are included in the resources if you are interested.
Once the user has selected a set of the bookmarks, clicking Finish button instructs
the code to calculate an IWorkingSet which represents the selection, using a
a convenience method getCheckedList() that returns a list of the checked
elements in the tree. We can store the working set in
an instance variable and calculate it in the finish() method:
private IWorkingSet workingSet;
public IWorkingSet getSelection() {
return workingSet;
}
public void finish() {
String workingSetName = textName.getText();
List elements = getCheckedList(treeViewer.getInput());
IWorkingSetManager workingSetManager =
PlatformUI.getWorkbench().getWorkingSetManager();
workingSet = workingSetManager.createWorkingSet(workingSetName,
(IAdaptable[])elements.toArray(new IAdaptable[elements.size()]));
}
The IWorkingSetManager will automatically
take care of managing the working set, and the elements that we
have selected. The dialog can also support editing existing working sets, but
in order to achieve this the working set must be stored when the setSelection()
method is invoked:
public void setSelection(IWorkingSet workingSet) {
this.workingSet = workingSet;
}
Lastly, we can modify createControl() if we already have a working set,
and initialize the checked state of the tree viewer with the already-selected
bookmarks in the working set:
if (workingSet != null) {
textName.setText(workingSet.getName());
}
setTreeChecks();
The setTreeChecks() convenience method sets the checkboxes for
the elements in the tree viewer. We put it outside the if because we'll also
check, for new working sets, the selected bookmarks in our bookmark view. The
setTreeChecks() method is implemented as follows:
private void setTreeChecks() {
List elementList = new ArrayList();
Object[] elements;
if (workingSet == null) {
// Selected bookmarks in the bookmark view will be marked on init
StructuredSelection selection = BookmarkView.getInstance().getSelection();
for (Object element : selection.toArray()) {
if (element instanceof BookmarkNode) {
elementList.add((BookmarkNode) element);
}
}
elements = elementList.toArray();
} else {
elements = workingSet.getElements();
}
// Mark the selected bookmarks in the tree
treeViewer.setCheckedElements(elements);
}
Fortunately the TreeViewer class has the setCheckedElements() method
that does most of the work. Tihs allows us to edit the working sets, and check
that they keep their members; at least, until we exit Eclipse.
Although we can edit working sets, the contents aren't persisted between restarts of the Eclipse
workbench. The working set names are
preserved, but if you edit them, they show no selected bookmarks. That's
because we haven't persisted those selected bookmarks. The framework
can persist (save) the working sets because it knows about working sets, but
it has no idea what a bookmark is, and so no idea of how to
persist it (or how to restore it later). We have to provide that
information to the framework. We do that by implementing the IPersistableElement
interface. The IPersistableElement has only two methods: saveState()
defines what to write to the persistence file to persist our object;
getFactoryId() returns a unique string that will allow the framework,
when restoring the objects, to know which class is responsible of restoring
that particular kind of objects. We'll use a unique string called
"com.quantum.ui.bookmarkFactory", and for identification of the bookmark,
the name is enough. We'll implement this interface in the TreeNode class,
that is the parent of BookmarkNode.
public String getFactoryId() {
return "com.quantum.ui.bookmarkFactory";
}
public void saveState(IMemento memento) {
memento.putString("elementID", getName());
}
That has the effect of adding a line for each bookmark that we have selected
in our working set, to an XML file where the working sets are persisted. (The
file is called workingsets.xml which can be found in .metadata\.plugins\org.eclipse.ui.workbench if you're interested.) It looks like:
<?xml version="1.0" encoding="UTF-8"?>
<workingSetManager>
<workingSet editPageId="com.quantum.ui.BookmarkWorkingSet"
factoryID="org.eclipse.ui.internal.WorkingSetFactory" label="BookmarkWS" name="BookmarkWS">
<item factoryID="com.quantum.ui.bookmarkFactory" elementID="AccessTest" />
</workingSet>
<workingSet editPageId="org.eclipse.jdt.ui.JavaWorkingSetPage"
factoryID="org.eclipse.ui.internal.WorkingSetFactory" label="JavaWS" name="JavaWS">
<item elementID="=Prueba/src" factoryID="org.eclipse.jdt.ui.PersistableJavaElementFactory"/>
</workingSet>
<workingSet aggregate="true" factoryID="org.eclipse.ui.internal.WorkingSetFactory"
label="Window Working Set" name="Aggregate for window 1169719504828" />
</workingSetManager>
... or at least, that's how it should look like. The problem is that the line in italics
isn't there. That is the line that should persist the one bookmark selected
by our working set ("AccessTest"). Although the bookmark implements the IPersistableElement
interface, the framework hasn't recognized the fact that the BookmarkNode class (via
its parent TreeNode) implements IPersistableElement.
Instead, an adapter has to be provided via the more generic IAdaptable interface.
The IAdaptable interface is a very generic and rather ubiquitous (in Eclipse)
interface that basically allows us not to define too many interfaces for our
classes. As one can see, the more services that get implemented via interface
compliance, like this working set persistence, the longer our list of interfaces
will be. Long lists of interfaces are difficult to manage properly, and may lead
to unintended conflicts. So, for many services, Eclipse does not ask if the
class implements interface xxx, but rather if it implements
IAdaptable, and then asks the IAdaptable interface if it can provide an object
that implements the interface xxx. That allow us to define an
external class to provide the interface, instead of piling them all up in ours.
For a more detailled discussion on the IAdaptable interface, and of how you
can define an external class to handle it, see this EclipseZone
article.
In this case, TreeNode only implements three interfaces (one of
them already being IAdaptable), so a fourth one is not too much. Then we simply
instruct the IAdaptable interface part of TreeNode (by implementing
getAdapter()), to return this when asked for an IPersistableElement:
public Object getAdapter(Class adapter) {
if (adapter == IPersistableElement.class) {
return this;
}
return null;
}
Now it will work, which we can verify by looking at the
workingsets.xml file, but not because we
notice any change in our application. The working sets are empty,
because we still haven't told the framework how to restore them.
The framework, when reading the XML file, will see the factoryID="com.quantum.ui.bookmarkFactory"
and will search for a factory with that id. The factory
is an Eclipse extension defined in org.eclipse.ui.elementFactories.
This extension contains an id (that matches the factoryID string "com.quantum.ui.bookmarkFactory"),
and a class name. The class must implement the IElementFactory
interface, with an implementation of createElement() that
returns an IAdaptable object:
public IAdaptable createElement(IMemento memento) {
String identifier = memento.getString("elementID");
if (identifier != null) {
// If the bookmark node is not in the list, no object will be returned
return BookmarkListNode.getInstance().find(identifier);
}
return null;
}
There is no provision in this code for creating bookmark nodes that don't exist
in the BookmarkListNode, so you cannot restore a working set element if it has
been deleted from the bookmarks list.
Now to the most intricate part. We have to filter the bookmarks displayed to match only those in the selected working set(s). To do this, we need to know which working set, if any, is selected for our view. Usually views will implement a selection of working sets in the toolbar menu, but for this purpose it is enough to default always to the window working set. The Window Working Set is the one you select when you open the working sets dialog. To obtain it, we call:
IWorkingSet workingSet = getSite().getPage().getAggregateWorkingSet();
IWorkingSet is the interface for working sets. It gives the label, the
name, the contents, etc. We are mostly interested in the contents; that is,
the bookmarks that we want to be shown in the bookmark view. We get that with:
IAdaptable[] workingSetElements = workingSet.getElements();
This returns all the selected elements
of all the selected working sets in the working set dialog. That includes our
bookmark working sets, the resource working sets, the Java working sets, etc.
That's because the working sets can be aggregate, in which case they
are composed of other working sets. Fortunately, that's all transparent to us,
as we just ask for the elements and get them, regardless of if our working set
is a simple working set, or an aggregate. Of course we'll be only interested
in the elements of type BookmarkNode.
Now that we have the elements, we can create a filter. In the BookmarkView,
when creating the tree viewer, we add:
BookmarkWorkingSetFilter workingSetFilter = new BookmarkWorkingSetFilter();
...
treeViewer.setInput(input);
treeViewer.addFilter(workingSetFilter);
For some reason, the filter has to be set after the input, or the bookmark
working sets won't be recreated correctly. The filter is a relatively simple
class, that only has to know which elements (from working sets) of the BookmarkNode
type are selected, and filter all those that aren't. The following is somewhat inefficient;
each time that the filter is called, it checks all the working
sets. That way, when a working set is changed (by selecting one new element,
for example), you only need to refresh the tree to reflect all changes to the
working sets. The filter method is:
public Object[] filter(Viewer viewer, Object parent, Object[] elements) {
if (workingSet != null) {
boolean isAnyBookmarkElement = false;
// Check if there is at least an element of type BookmarkNode
for(IAdaptable element : workingSet.getElements()) {
if (element instanceof BookmarkNode || element.getAdapter( BookmarkNode.class ) != null) {
isAnyBookmarkElement = true;
break;
}
}
// If no element of type BookmarkNode, we return all items
if (!isAnyBookmarkElement) {
return elements;
}
}
return super.filter(viewer, parent, elements);
}
In the filter we store the window working set into the workingSet
variable. Then, every time the filter is used, we get all the working set
elements, and see if any one is a BookmarkNode. If any is, we filter based on
these elements. If none is , we return all the received elements (the BookmarkNodes
to be displayed). The super.filter() method will call our select()
function if we have one defined, for each BookmarkNode to be displayed. So we
filter like this:
public boolean select(Viewer viewer, Object parentElement, Object element) {
BookmarkNode bookmarkNode = null;
if (workingSet == null) {
return true;
}
if (element instanceof BookmarkNode) {
bookmarkNode = (BookmarkNode) element;
} else if (element instanceof IAdaptable) {
IAdaptable adaptable = (IAdaptable) element;
bookmarkNode = (BookmarkNode) adaptable.getAdapter(IResource.class);
}
if (bookmarkNode != null) {
return isInWorkingSet(bookmarkNode);
}
return true;
}
Where the isInWorkingSet() method will return true if the bookmarkNode
is in the filter's working set. It's better to listen for changes to the working set, make a list of the elements
when a change is processed, and filter based on that list; but this works for the purposes of this example.
Our implementation so far misses a couple of situations. The most important
one is what happens when the user changes a working set, for example adding
a BookmarkNode to the active working set. The user would expect (not altogether
unreasonably) to see the added bookmark in the bookmark view. But the view is
currently unaware of the changes in the working set. We have to listen
to those changes; the object that will give us that service of telling us about
changes is the working set manager, which implements the IWorkingSetManager
interface:
IWorkingSetManager workingSetManager = QuantumPlugin.getDefault().getWorkbench().getWorkingSetManager();
This gives you access to query the existing working sets, but in this case we are just interested in changes so that we can re-filter the view:
workingSetManager.addPropertyChangeListener(propertyChangeListener);
You can get notifications of all kind of events related to working sets, such as if one has changed name, or content, or being deleted. You should add code for each situation, if you are selecting your working set at the view level. As we are using always the "window working set", we are just interested in the fact that something has changed that may affect us, and so we just need to refresh the viewer. As the viewer's filter reloads all working sets elements, no further processing is needed:
private IPropertyChangeListener propertyChangeListener = new IPropertyChangeListener() {
public void propertyChange(PropertyChangeEvent event) {
treeViewer.refresh();
}
};
There is still one last scenario to consider. We have just processed the changes to our filters if the working set changes. What happens when we change something in the resource view, in our case the bookmark view? Suppose we change the name of one of the bookmarks. We should change it in every working set where it's selected. Fortunately Eclipse will take care of that (and also when you delete a resource). But if you add a resource, and you'd like to add it to the active working set, you need to use the updater class. (The updater class is the second class in the original working set extension point.)
The updater class must implement the IWorkingSetUpdater interface, so that
it will be informed of changes to the working sets. It consists of four methods:
add(IWorkingSet)remove(IWorkingSet)contains(IWorkingSet)dispose()
The working set manager calls the add() method on Eclipse startup
with every working set of matching id that it has, allowing your updater class
to build a list of the working sets that relate to it. Then, for each addition
or deletion of a related working set, the working set manager will call
add() or remove() as needed. The dispose() method will
be called when the manager is disposed (usually at the closing of the Eclipse framework),
to allow you to dispose allocated resources. You have also to ensure that the
contains() method will return true if it receives a working set that
your updater object contains, and false otherwise. The idea is to build a list
of the working sets of your type (in our case BookmarkWorkingSet objects), to
know what to update. When a change occurs, you can update the working set as follows:
workingSet.setElements((IAdaptable[]) elements.toArray(new IAdaptable[elements.size()]));
Where elements would be a list of the elements (BookmarkNodes) that
will comprise that working set.
You can find the complete sources of the Quantum plug-in at http://quantum.sourceforge.net and working sets are implemented in the last release, 3.0.4.
I would like to thank Alex Blewitt for his invaluable input into this document. However, all opinions expressed are mine, as are all errors and omissions.
We encourage you to ask the author any questions or discuss the article here.