http://www.developer.com/

Back to article

The (J)Face of Eclipse


November 17, 2005

If you ever tried to build your own Eclipse plug-in, you've seen how the Plug-in Development Environment (PDE) makes the job quite easy. However, PDE won't go too far in helping you create customized user interfaces—you must do that yourself, in Java. Fortunately, Eclipse provides a set of APIs to help you along the way.

In the previous article, you used PDE to create a simple visual plug-in. This plug-in contributed a view to the workbench (you called it the Superview), that showed a list of all views available at runtime. Without going into much detail, you used the TableViewer class to display the view's contents as a single-column table. In this article, you'll examine this approach in closer detail. Specifically, you'll look at the Standard Widget Toolkit (SWT), which provides the fundamental user interface building blocks used throughout typical Eclipse applications, as well as the JFace framework, which provides a set of patterns and component abstractions that facilitate the development of user interfaces.

Introduction

Although Eclipse applications are not strictly constrained to the desktop (in fact, there are many examples of console-based Eclipse tools), the most common kind are Graphical User Interface (GUI) applications. In Eclipse, there are two closely related core technologies that support development of such applications. The Standard Widget Toolkit (SWT) provides the fundamental building blocks of the user interface in a typical Eclipse application. The JFace framework provides a set of useful user interface component abstractions. Of course, there are other Eclipse GUI frameworks, such as the Graphical Editing Framework (GEF) and other, third-party solutions (you can even use Swing), but those are usually designed for specific types of applications. SWT and JFace, on the other hand, are the two you will encounter almost everywhere—from simple SWT-based applications (that don't even use the Eclipse runtime), to workbench-based RCP applications, to IDE plug-ins. In fact, the very IDE workbench, JDT, and PDE, which are all part of your Eclipse SDK, rely on these two frameworks for their GUI. Thus, knowing the basics of SWT and JFace is essential for building any useful user interfaces in Eclipse.

I'll first introduce you to JFace rather than SWT; you will find that it is JFace, rather than SWT, that provides the constructs you will likely end up using the most when developing end-user applications. You do need the basic understanding of SWT and how it relates to JFace, but you don't really need to know the intimate details unless you plan to avoid JFace and directly use SWT in your applications, or wish to develop more advanced, low-level UI customizations.

To help you better understand the various elements of these frameworks, you will extend the Superview plug-in you developed in the previous article with the following functionality:

  • Change the view to display a tree of available views, grouped by their categories
  • Allow the user to activate the selected view by double-clicking it or by choosing an option from the context menu
  • Use a menu option to apply a filter to exclude the Superview itself from the list
  • Display properties of the selected view in a dialog box
  • Use a multi-page wizard to show the selected view in another workbench window

To follow the example, you will need a working Eclipse SDK, version 3.1 or newer. You can download it for free from the Eclipse download site.

The JFace UI Framework

In the Platform Plug-in Developer Guide, JFace is described as "a UI toolkit that provides helper classes for developing UI features that can be tedious to implement." In fact, JFace lets you focus on the end-user functionality you want to implement and avoid the complexities of managing widgets and the underlying OS resources. Many of these problems follow patterns common to most GUI applications, and there is no need to re-invent the wheel.

Viewers

Figure 1: JFace Viewer hierarchy
Figure 1: JFace Viewer hierarchy

Viewers are classes that wrap various types of widgets and manage their common aspects, such as populating them with data, updating them when the underlying model changes, as well as sorting, filtering, and controlling other visual properties specific to the widget. You've already seen the TableViewer class in action in the previous article. Other common viewers are ListViewer, TreeViewer, ComboViewer, and TextViewer. Additional classes are available in the org.eclipse.jface.viewers package. You may recall that in the previous example, you delegated the job of constructing the underlying Table widget to the viewer itself. You then configured the viewer with various helper objects that control its behavior. Lastly, you set your model object (an array of view descriptors) as the viewer's input.

The two most important helpers used by the TableViewer (and other ContentViewers) are the content provider (the TableViewer and other StructuredViewers require an IStructuredContentProvider) and the label provider (an ILabelProvider will suffice, but ITableLabelProvider should be used when multiple columns exist in the table). The content provider's job is to translate the viewer's input into something it can understand; for example, an array of rows in TableViewer's case. Implementors of IStructuredContentProvider turn the viewer's input object into an array of elements. Other viewers display different content structures; for example, the TreeViewer displays a hierarchical tree, and thus requires a content provider that can translate the model into a tree of parent/child nodes (in other words, an ITreeContentProvider). The content provider also is responsible for managing model changes—when detected (by any model-specific means), the content provider typically updates the viewer to reflect the new model state (for example, adds/removes table rows).

The label provider's job is to turn a content element into something the viewer can display. For example, an ILabelProvider can return the text label as well as an optional image to visually represent the element. ITableLabelProvider can display a different label/image pair for each table column (in the example, you only have one column; thus, you don't need to use the ITableLabelProvider).

Other helper objects used by structured viewers are the ViewerSorter, which determines the display order of the viewer's content elements, and the ViewerFilter, which can be used to filter out certain elements according to arbitrary criteria. A viewer can have multiple filters, which are applied consecutively.

Viewers also support various kinds of listeners to which they delegate events emitted by the underlying widgets. A frequently used listener is the ISelectionChangedListener, which is notified when the user changes the viewer's selection (for example, selects a table row with the click of the mouse or by using keyboard navigation).

Actions and Contributions

Actions and contributions represent user-invokable functions, accessible through menus, toolbars, and the status line. Actions encapsulate the non-UI portion of the function—you implement the run method of IAction (actually, you don't implement the IAction interface, but rather subclass the abstract Action class), which is called when the user invokes the action by means of the associated widget, such as a toolbar button. This allows you to decouple your logic from any specific UI widget and reuse it in multiple locations as needed. Actions are used extensively to open dialogs or wizards, and to modify the state of views and editors.

Contributions represent actions in shared UI components, such as menus, toolbars, and the status line. They link them to specific widgets that make the actions available to the user. Typically, you don't need to create contributions for your actions—this is done on your behalf, when you add your action to the UI component using the appropriate contribution manager, such as an IMenuManager, IToolBarManager, or the IStatusLineManager.

Dialogs and Wizards

As a user, you know that dialog boxes are the small windows commonly used in applications to conduct an interactive "dialog" with the user—ask a question, solicit input needed to perform some action, or simply present some context-specific information to the user. Typically, dialogs are modal—only one can have input focus at a time. Along with a set of standard dialogs, such as the MessageDialog, ErrorDialog, and InputDialog, JFace provides a dialog framework that you can use to create your own, application-specific dialogs. Quite predictably, at the root of this framework is the Dialog class, which can be subclassed to create a custom dialog. In addition, there are several subclasses that represent different dialog styles, such as IconAndMessageDialog, StatusDialog, or TitleAreaDialog.

To implement your own dialog, you must populate its content area with widgets in its createDialogArea(Composite) method. You are free to use any JFace components and SWT widgets available. To open a dialog (for example, in your action's run method), you must first instantiate it and then call its open() method. By default, this method does not return until the dialog is closed. The result of this method represents the dialog's return code (for example, OK, Cancel, and so forth).

A typical dialog has at least one button that can be used to close it, but more commonly it has several different buttons to present the user with a choice of responses (as in OK/Cancel, Yes/No, and the like). The application then uses this information as the user's decision with respect to the action represented by the dialog. In JFace dialogs, buttons are placed horizontally in the bottom-right corner of the dialog box.

Wizards are multi-page dialogs used to implement tasks that involve one or more steps. You can see many wizard examples in the IDE workbench; typically, any resource or object creation task is implemented as a wizard (in fact, even single-step creation tasks are usually implemented as wizards, for consistency). Strictly speaking, a wizard is a collection of wizard pages, displayed in some sequence in the wizard container dialog. This design allows you to re-use wizard pages in different wizards, as well as chain or nest multiple wizards together. The wizard dialog has the Back, Next, Finish, and Cancel buttons, enabled or disabled depending on its current state.

To create a custom wizard, subclass the Wizard class and implement its addPages() and performFinish() methods. In addPages, instantiate your wizard pages and add them to the wizard using addPage(IWizardPage). In performFinish, implement your logic to make the wizard do its task when the user clicks Finish. To create a wizard page, subclass WizardPage and implement its createControl(Composite) method to populate its content area with widgets.

To invoke your wizard, you must first instantiate it, create an instance of WizardDialog initialized with your wizard, then open it by calling open(), just like any other dialog box. This method will return when the user presses Cancel or Finish, thus closing the dialog.

As a side note, I highly recommend that you browse the source code provided with your SDK—wherever you reference a class or an interface in your code, or invoke a method, press Ctrl, move the mouse over it, and click the presented link. This will bring you to the definition of the type or method. Read the Javadoc (most Eclipse APIs are well documented). The Javadoc view makes this easier (click Window -> Show View -> Other..., select Java -> Javadoc, and then click OK).

SWT

The Standard Widget Toolkit, or SWT, provides the API for creating and managing widgets, interactive controls, graphics, and other display-related resources. It does not have dependencies on the Eclipse run-time, or any other Eclipse plug-in, which means that it is entirely possible to create SWT-only, non-Eclipse applications (in fact, there are several commercial as well as open source examples of such applications). You can download SWT packaged as a standalone library from the Eclipse downloads page, SWT section.

SWT is somewhat controversial in the Java community, partly because it is an alternative to Swing, the "official" Java UI framework (although starting with version 3.0, you can use Swing components in SWT), and partly because it is not pure Java—it uses the Java Native Interface (JNI) to interact with the OS platform's native widget toolkit. That is the most fundamental difference between SWT and Swing; whereas Swing uses emulated widgets, SWT uses window system native widgets, supplemented by custom implementations on those platforms where native versions are not available. This means that user interfaces built with SWT don't just look native, they are in fact native. This is more in line with user expectations—they get the same look and feel they see when using other applications in their OS. It also results in better application performance, although the performance gap between the two frameworks is reportedly getting narrower. The flip side is that SWT is OS-dependent (but then, so are JVM implementations). However, the set of supported platforms continues to expand.

Widget Hierarchies and Events

Figure 2: SWT Widget hierarchy
Figure 2: SWT Widget hierarchy

As you'd expect, the central theme in SWT is class Widget and its descendants. Only a handful of its direct subclasses are concrete (they are Caret, DragSource, DropTarget, Menu, ScrollBar, Tracker, and Tray). Two important Widget hierarchies are the descendants of Item and Control. The former represent "items" within multi-item controls, such as menus, tables, tabbed pages, and toolbars, that are not themselves windowed controls. The latter are the familiar elements of user interface that support interaction with the user—buttons, labels, text boxes, lists, and so on. Some of them are further abstracted as Scrollable (those that support scrolling with the help of scroll bars).

All widgets have the ability to emit events to their registered listeners to keep them informed of what is going on with the widget (most popular window-based user interface systems use this programming model). There are two types of events and listeners in SWT: typed and untyped (well, they're not really that different because the former is implemented in terms of the latter). Typed listeners are classes that handle a specific type of event; for example, the user's mouse click is delivered to MouseListeners. The events they handle are represented by specific event classes (for example, MouseEvent). Untyped events are designated by a unique constant defined in class SWT. They are delivered to implementors of org.eclipse.swt.widgets.Listener.

One important aspect of SWT programming, whether or not you know the intricate details of each widget's API, is how it processes events. To be compatible with the even-based programming model of most windowed GUI systems, SWT uses a single thread, called the UI thread, to process widget events placed into the application's event queue by the underlying window system. Thus, at the core of each SWT-based applications is an event processing loop (wait for event, dispatch it, repeat). The implication to the developer is that any interaction with widgets (in other words, creation, method invocation, and the like) must be done in the UI thread. In fact, SWT detects access from another thread and throws an exception as a result—you won't get far if you try it. If you do need to access your UI from a non-UI thread, you must ask the UI thread to do it on your behalf—obtain the singleton Display instance (responsible for managing the connection between SWT and the underlying OS platform) by calling Display.getDefault(), and use either its syncExec(Runnable) or asyncExec(Runnable) method to get your code (the Runnable) executed in the UI thread. The difference between the two methods is that the former blocks until the Runnable has been executed; the latter returns immediately after queueing it.

Composites and Layouts

A large group of controls are descendants of class Composite. As the name suggests, these controls may be composed of other controls (Composite itself is a subclass of Scrollable). You will notice that many methods throughout Eclipse UI that you're supposed to extend or implement to create your own user interface component provide an instance of Composite, which you are to use as the parent of your widgets. If you find yourself in need of drawing your widget from scratch (using SWT's graphics primitives), you'll want to subclass Canvas (itself a subclass of Composite), which is designed for that purpose.

Because composites usually contain several other widgets, they provide the ability to control their display layout. This is the responsibility of layouts (in other words, subclasses of Layout). There are several concrete layouts provided with SWT, the most commonly used one being the GridLayout (lays out controls in a cell grid). Others include FillLayout, FormLayout, and RowLayout. More advanced layouts are supported by other frameworks (such as the Eclipse UI Forms, which I do not cover here). A composite is given a layout using its setLayout(Layout) method, but many layout implementations in addition require that each control be annotated with a implementation-specific LayoutData, that the layout algorithm uses to compute the control's size and position. Each control can be assigned layout data using its setLayoutData(Object) method.

Custom Widgets and Extensibility

Even though most SWT classes are declared as public, only a few are meant to be extended; the Javadoc is usually good at indicating which ones they are (also, look in Javadoc for supported styles and events). To create custom reusable components, you can subclass Composite and populate it with child widgets in the constructor. Even though there are several "custom" widgets in package org.eclipse.swt.custom, you must subclass Canvas to create your own (to paint your widget, implement a PaintListener for it). Finally, you are free to implement your own composite layout algorithms by subclassing org.eclipse.swt.widgets.Layout. Following these guidelines will give your code the best chance to remain immune to changes in SWT.

Managing Operating System Resources

The last but nevertheless very important topic to mention is resource management. Fonts, colors, images, and even widgets are all operating system resources that are not automatically garbage collected. When you create one of these resources (usually by instantiating it, or by calling a method that creates a new resource), you must remember to dispose it—by calling its dispose() method—as soon as you're done using it. The sooner you do it, the better, because these resources are usually scarce (more so than your heap memory), and you can get a nasty, unrecoverable SWTError when you run out of them.

To simplify the management of your plug-in's or application's static resources, JFace provides several helper classes. ImageDescriptor, ColorDescriptor, and FontDescriptor allow you to define their respective resource, but delay its actual creation until needed. The corresponding registries—ImageRegistry, ColorRegistry, and FontRegistry—take the responsiblity for keeping track of resource creation and disposal; all you have to do is register your resource descriptors with the registry, and then ask it for the actual resource, which it creates on demand, as needed. Registries automatically dispose any resources they create when the Display they're associated with is disposed.

Growing the Superview

In this section, you will extend the example you developed in the previous article. To follow the steps, please download the example source code from the location listed in the Resources section.

In the previous installment, you developed a simple visual plug-in that contributed a custom view to the workbench. I already mentioned that you used a JFace viewer—the TableViewer—to display a list of views (their names and icons) in your custom view (the Superview). You will continue to treat the Superview as a convenient place for your user interface components; however, you won't go into the details of the Superview class itself (or its ViewPart superclass)—you will cover this in another installment. For now, all you need to know is that the Superview's createPartControl(Composite) method is where you construct the user interface for display in the view's content area. In this method, you are given a Composite that can act as the parent of any widgets that you create.

A brief note about your "application model": In the previous article, you used as your model an array of view descriptors, which you obtained from the view registry. This time, you want to show a tree consisting of view categories, each of which should expand into the list of views that belong to it. You can obtain these categories from the view registry as well. Thus, you will treat the view registry (IViewRegistry) as your application model.

Changing the Viewer

To switch from displaying a table to displaying a tree, the first thing you need to do is change the viewer's declared type to TreeViewer and instantiate it at the top of createPartControl. By using the constructor that only takes the parent composite, you leave it up to the viewer to create the underlying SWT Tree control (as a direct child of the given parent composite), using the default style. Alternatively, you could create the widget first, and then pass it to the viewer's constructor (with a different signature).

You then configure the viewer with a content provider, a label provider, and a sorter; however, this time you need to develop a custom content provider implementation—the TreeViewer requires its content provider to implement ITreeContentProvider (it asserts this in its setContentProvider(IContentProvider) method). Thus, you create a nested class called ViewContentProvider (for clarity, the customized label provider was also moved into a nested class named ViewLabelProvider).

The content provider is responsible for translating the viewer's input into a structure that the viewer understands. An ITreeContentProvider must support hierarchical tree navigation in both directions—downward (children) and upward (parent). The viewer uses the provider's getChildren(Object) method to obtain an element's array of children, and its getParent(Object) method to obtain an element's parent. Every child element also may act as the parent of another array of elements (the provider must indicate this for a given element by returning true from its hasChildren(Object) method). Your model doesn't have the API to allow for easy navigation upward—there is no getParent-like method in either IViewDescriptor or IViewCategory; therefore, you cache each element's parent in the getChildren(Object) method (you can do that because you know you're always asked for children first). You could choose to solve this differently; for example, you could wrap the model in a parallel hierarchy, which would provide access to each element's parent. Both approaches have some disadvantages; you should select an approach that seems most reasonable in your particular situation.

Method inputChanged(Viewer, Object, Object) notifies the provider that the given viewer changed its input (both old and new are given). This happens as a result of calling the viewer's setInput(Object) method. The content provider may use this method to add itself as a listener of the new input object, if required, and the model's API supports it, to be able to react to any changes in the model. When such changes are detected, the content provider updates the viewer accordingly (for example, by inserting new elements and/or removing deleted ones). However, in this example you assume the model is static and you do not need to monitor its changes.

The dispose() method gives the content provider a chance to clean up any resources it may have used up. You don't need to do anything here, either. Method getElements(Object) comes from IStructuredContentProvider, a supertype of ITreeContentProvider, and your implementation simply delegates to getChildren(Object).

Listing 1: Implementing ITreeContentProvider

public Object[] getChildren(Object parentElement) {
   if (parentElement instanceof IViewCategory) {
      Object[] children = ((IViewCategory) parentElement).getViews();
      mapChildrenToParent(children, parentElement);
      return children;
   }

   if (parentElement instanceof IViewRegistry)
      return ((IViewRegistry) parentElement).getCategories();

   return EMPTY_ARRAY;
}

public Object getParent(Object element) {
   if (element instanceof IViewDescriptor)
      return parentMap == null ? null : parentMap.get(element);

   if (element instanceof IViewCategory)
      return registry;

   return null;
}

public boolean hasChildren(Object element) {
   return getChildren(element).length > 0;
}

Your label provider implementation remains largely the same, except it now has to handle elements of type IViewCategory, in addition to IViewDescriptors. You will notice that you use a shared workbench image for categories (all share the same one). You obtain this image directly from the workbench registry of shared images, which means that you don't have to manage its disposal (in fact, you must not dispose it because you don't create it yourself).

You continue to use the default ViewerSorter, which sorts your elements according to their displayed labels using the default text collator. Lastly, you set the workbench view registry as the viewer's input because that is the model your content provider implementation understands.

Interacting with Superview

You want to allow the user to interact with your Superview—show the selected view, display its properties in a dialog box, and use a multi-page wizard to show the view in another window. You also want to support the optional exclusion of Superview from the displayed list of views. To accomplish this, you use a combination of viewer listeners and actions.

First, you install an IOpenListener on the viewer to be notified of its "open" events. These occur whenever the user double-clicks an entry in the viewer (or single-clicks it, depending on their workbench configuration), or hits the Enter key when the viewer has input focus and its selection is not empty. You react to these events by first obtaining the viewer's selection (you know it's an IStructuredSelection because that is what StructuredViewers work with), and determining the type of its first element (there will never be more than element because you don't explicitly configure the viewer to allow multi-element selections). If the selected element is an IViewDescriptor, you show its corresponding view in the current workbench window (the details of this operation are not relevant right now). When an IViewCategory is selected, you either expand or collapse it, depending on its current state (doing this, I believe, makes the tree viewer behave more intuitively; otherwise, the user would always have to click the +/- sign to expand/collapse categories).

Listing 2: Installing IOpenListener

viewer.addOpenListener(new IOpenListener() {]
   public void open(OpenEvent event) {]
      IStructuredSelection sel =
         (IStructuredSelection) event.getSelection();
      Object element = sel.getFirstElement();
      if (element instanceof IViewDescriptor)
         showView((IViewDescriptor) element);
      else if (element instanceof IViewCategory) {
         if (viewer.getExpandedState(element))
             viewer.collapseToLevel(element, TreeViewer.ALL_LEVELS);
      else
         viewer.expandToLevel(element, 1);
      }
   }
});

Most Eclipse users are accustomed to the power of the context menu—right-clicking anything onscreen usually brings up a menu with all possible operations relevant in the given context. Therefore, you add a context menu to the Superview as well. You do this with the help of a JFace class, the MenuManager, that handles the creation and population of the actual context menu widget. Each menu option is backed by a JFace Action, which performs its actual function.

Because your context menu options are only applicable when a view is selected (not when a category is selected), you have to enable or disable them based on the viewer's selection. Eclipse UI provides a convenience action class named BaseSelectionListenerAction (this is not strictly JFace, but that's good enough for this example), which can act as a listener to viewers' selection changes, and enable/disable itself based on current selection. You use this class to implement your context menu actions. For convenience, you put the selection-based enablement logic, as well as extraction of an IViewDescriptor from the selection in an abstract nested class named SelectedViewAction, which extends BaseSelectionListenerAction; you then subclass this one to implement each particular action.

An alternative approach to enabling/disabling context menu options based on selection would be to rebuild the menu each time it is shown. To do that, you would configure the menu manager to remove all menu items from the menu before it is shown, and install a menu listener that would add only the relevant menu options back to the menu.

Your actions must be instantiated before you can populate the context menu. You instantiate each selection-dependent action as an anonymous subclass of SelectedViewAction, passing its menu label (with the designated accelerator character prefixed with an ampersand) to its constructor. The run method of your "show view" action does the same as the IOpenListener—it shows the selected view in the current workbench window. I'll get back to the details of the other two actions—"show in window" and "view properties"—after you implement the required dialog and wizard. Lastly, you install each action as the viewer's selection change listener and manually invoke its selectionChanged method using the viewer's current selection, to initialize its enablement.

The last feature on your list, optional exclusion of the Superview from its own contents, does not depend on any selection. Therefore, it is not necessary to create its action as a subclass of SelectedViewAction; the plain Action will suffice. Note that you instantiate it with the checkbox style, so that it appears in the menu as checked/unchecked, depending on its state; in its run method, you use this "checked" state to determine whether to add or remove the filter. The filter itself is a subclass of ViewerFilter, instantiated lazily in the addSuperviewFilter method. It only allows elements other than the one that represents your Superview (in other words, an IViewDescriptor with an ID that equals the Superview's ID). Note also that filters are added, rather than set, which means that it is possible to have multiple filters installed in any given viewer.

Finally, you populate the context menu by simply adding each action to the menu manager instance—the manager creates the corresponding menu contribution item on your behalf. Lastly, you call the manager's createContextMenu(Control) method to create the actual menu widget and set it as the viewer's context menu.

Listing 3: Actions and context menu creation

SelectedViewAction showViewAction =
   new SelectedViewAction("Sh&ow") {
      protected void run(IViewDescriptor view) {
         showView(view);
      }
};

viewer.addSelectionChangedListener(showViewAction);
showViewAction.selectionChanged((IStructuredSelection)
   viewer.getSelection());

// ...

MenuManager contextMenuManager = new MenuManager();
contextMenuManager.add(showViewAction);
// ...

Menu contextMenu =
   contextMenuManager.createContextMenu(viewer.getTree());
viewer.getTree().setMenu(contextMenu);

Adding a Custom Dialog

As you already know, one of the uses of a dialog box is to present information to the user at a point in time. Your custom dialog displays additional properties of the view that is currently selected in the Superview. You could use other means to accomplish the same, but this is a reasonable example for demonstrating the creation of custom dialog boxes.

You start by creating a new class named ViewPropertiesDialog as a subclass of org.eclipse.jface.dialogs.Dialog. This is the base class of JFace's Dialog hierarchy and by default displays an empty dialog box with OK and Cancel buttons. You implement a constructor that takes the required parent Shell (in which to open the dialog window) and the view descriptor (IViewDescriptor) whose properties to display—you keep it for populating the widgets with property values.

You customize the dialog by extending method createDialogArea(Composite). Much like the view's createPartControl method, this method gives you a Composite, which you can use as the parent of each widget you create. It differs in that you are required to return the top-level composite you create, so that the dialog can use it for its own bookkeeping purposes. Here, you actually extend the method, rather than completely overriding it—you call its superclass implementation before doing anything else. The reason is that the superclass creates for you a container Composite, configured with the standard dialog layout and margins. This is the composite you actually use as the parent of your widgets and return as the result of this method.

Before you start creating any widgets, you alter the container composite's layout by changing its number of columns to two, to get a column of property labels, followed by a column of property values. You then continue to create a Label and a read-only Text for each property of interest. For values, you use a Text control, rather than another Label, because even though it is read-only, the user can still select the displayed values and copy them to the Clipboard (something they cannot do with Labels). Note that each created widget gets a GridData to control its layout within the container composite—labels are left-aligned horizontally, center-aligned vertically, and don't "grab" the excess cell space, whereas the text controls are told to fill their cells horizontally and grab any excess cell space. This causes the text controls to receive as much space as needed to display their content. For better clarity, creation of labels and text controls has each been moved into a reusable method.

A few properties do not follow this simple pattern—the icon property should display the view's icon, the description property should show its content using multiple rows, and the "allows multiple instances" property should be a read-only checkbox (just to demonstrate a different control type). To display the icon, you use the Label control instead of Text, because Labels are capable of displaying images, too. You obtain the icon's image much like you do in Superview's label provider (and must remember to dispose it accordingly).

To display description, you use both columns to get the maximum space. You accomplish this by instructing the layout to make each widget span two columns using its layout data. To make it easier to read multi-row view descriptions, you configure the text widget to wrap its content and display a vertical scrollbar (you do this by specifying the appropriate style bits in the widget's constructor). Finally, in the layout data you specify that the height of the widget's cell should be four times the height of its text line (plus two, for good measure). Note that because view descriptions are actually quite rare, and the description widget takes up a lot of screen real estate, you only create it when there actually is a description.

To indicate whether the view "allows multiple instances," you create a checkbox—a Button with the checkbox style bit. You also make it span two columns, and set its selection state (checked/unchecked) to match the property's value. Finally, you disable it because you don't want the user to be able to change its value (because there's nothing in your model they can actually change).

There are a few other tasks to complete before you can call the dialog done. For one, you should set its title to something useful (by default, it is blank). You can do this in the configureShell(Shell) method, as part of configuring the dialog's shell. You also must change the set of dialog buttons, which by default contains an OK and a Cancel button. Your dialog is purely informational, and should therefore display only a Close button. You create it in the createButtonsForButtonBar() method, and react to the user clicking it in the buttonPressed(int) method by closing the dialog. Finally, you extend the close method to dispose of the icon image.

To actually open the dialog when the user selects the appropriate context menu option, you implement the corresponding action's run method—you instantiate the dialog, pass it the view's shell and the selected view descriptor, and call its open() method. That's it!

Adding a Multi-Page Wizard

From user's perspective, wizards are essentially dialog boxes with one or more pages; the user can navigate between these pages using the Previous and Next buttons, and click Finish to complete the task. From a developer's perspective, you don't actually program against the Dialog API. The wizard pattern is abstracted into class Wizard, which is composed of one or more WizardPages, and is "hosted" in the WizardDialog (or another appropriate IWizardContainer).

To demonstrate this concept, you create a custom wizard to allow the user to select an existing (or new) workbench window in which to show the selected view. While typically only one instance of a given view may exist in a workbench window, some views may be designed to allow multiple instances. Such views may be created with a different "secondary ID"—I will discuss the details of workbench views in another installment. In your wizard, you allow the user to specify this secondary ID in another wizard page for views that support it. You are right if you think that this use case could easily be handled in a single-page wizard or dialog; however, the goal here is to demonstrate the creation of a multi-page wizard.

You start by creating a subclass of org.eclipse.jface.wizard.Wizard, named ShowInWindowWizard. You give it a constructor that takes an IViewDescriptor to represent the currently selected view, and an IWorkbenchWindow to represent the window to pre-select. You use them later when creating wizard pages. To actually add your wizard pages, you need to extend the addPages() method. But first, you must implement them.

The first page in this wizard displays a list of open workbench windows for the user to select. In addition, you want to allow the user to open a new workbench window, if they choose to. To implement this page, you create a nested class called WindowPage and make it a subclass of org.eclipse.jface.wizard.WizardPage. In its constructor, you set the page title and description. The user interface of the page is constructed in its createControl(Composite) method, where again you get a Composite to use as the parent of your widgets. In this page, you use a ListViewer to show the list of open workbench windows. This viewer is very similar to TableViewer; after instantiating it (this time, you do specify a few extra style bits), you configure it with a content provider, a label provider, and a sorter. Because you're showing workbench windows in the list (more precisely, their titles), you have to provide your own content and label providers (implemented in two nested classes named WorkbenchWindowContentProvider and WorkbenchWindowLabelProvider, respectively). You set the workbench itself as the viewer's input (because that is the model your content provider understands). You also set the workbench window passed into the wizard's constructor as the default selection. Finally, you inform the page of the newly created control by calling its setControl(Control) method (so that it can perform any bookkeeping it may need) and passing in the viewer's underlying control.

Listing 4: WindowPage's createControl:

public void createControl(Composite parent) {
   listViewer = new ListViewer(parent, SWT.SINGLE | SWT.V_SCROLL |
                               SWT.H_SCROLL | SWT.BORDER);
   listViewer.setUseHashlookup(true);
   listViewer.setContentProvider(new WorkbenchWindowContentProvider());
   listViewer.setLabelProvider(new WorkbenchWindowLabelProvider());
   listViewer.setSorter(new ViewerSorter());
   listViewer.setInput(workbenchWindow.getWorkbench());
   listViewer.setSelection(new StructuredSelection(workbenchWindow),
                           true);
   setControl(listViewer.getControl());
}

By design, this wizard is to have an optional second page, depending on the type of view being shown. Views that allow multiple instances in a single workbench window should cause the second page to be available to the user. To accomplish this, you make the enablement of the Next button on the first wizard page, determined by method canFlipToNextPage(), conditional upon this property of the selected view (in other words, IViewDescriptor.getAllowMultiple() must return true).

The second wizard page, implemented in nested class ViewPage, is rather simple. It contains a text field, preceded by a descriptive label, where the user can enter an optional secondary ID to use when showing the view.

Now that you implemented both pages, you can instantiate them in the wizard's addPages() method and add each to the wizard using its addPage(IWizardPage) method. You also override its canFinish() method to always return true, because the second page is always optional and the first one will never contain invalid data (in other words, will never be incomplete).

To make the wizard perform the actual task of showing the selected view when the user clicks Finish, you implement its performFinish() method. Wizard tasks are usually long operations, and so you should give the user some progress feedback, optionally allow them to cancel it, and may even choose to run it in a background thread. You can accomplish all this by implementing your operation as an IRunnableWithProgress, which consists of a single run(IProgressMonitor) method. When instantiated, you pass this runnable into the wizard container's run(boolean, boolean, IRunnableWithProgress) method, which presents the user with a progress bar while running the operation.

Again, the details of showing the selected view in the selected (or new) workbench window are not important in this article. You may, however, take a closer look at how you use the progress monitor throughout the run method. First, you call its beginTask(String, int) method, which indicates the beginning of your job. Then, you wrap the entire body in a try/finally block, and call the monitor's done() method in the finally clause. In the body of the operation, you increment the indicated progress by calling the monitor's worked(int) method, and check for user's cancellation by calling the isCancelled() method (if the user did cancel, you must respond by throwing an OperationCancelledException).

Lastly, notice how the wizard container's run method throws two types of exceptions—an InvocationTargetException, which is thrown when anything goes wrong while executing the operation, and InterruptedException, when the user cancels the operation (or the background thread, if used, is interrupted). When the former is caught, you log it using your plug-in's logger and show a message box displaying the error to the user. When the latter is caught, you simply ignore it; however, in both cases, you return false from the performFinish method, which will cause the wizard to remain open. Only when the operation succeeds do you allow the wizard to close (by returning true).

Just as you did with your custom dialog, you must finish implementing the view's action that invokes the wizard. In it, you first create an instance of your wizard, pass it into a new instance of WizardDialog, and open the dialog.

Test Running the Example

There's nothing new that is required to test-run your example; simply right-click the plug-in in your Package Explorer and choose Run As -> Eclipse Application. When the run-time workbench comes up, go to Window -> Show View -> Other... and choose Developer.com -> Superview from the list. When the Superview shows up, you can test the functionality you've implemented.

Figure 3: The Extended Superview

A good way to learn and understand these simple examples is to install a few breakpoints in methods of your interest and run the plug-in in debug mode. Because SWT applications are (mostly) single-threaded, you won't find it difficult to keep track of multiple threads while debugging; you do need to keep in mind, however, that SWT uses JNI calls (which you can't step into with your debugger). It also receives events from the operating system, which are often dispatched some time after the related piece of code has executed (for example, when you programmatically check a radio button, the other radio buttons in the group receive their "uncheck" events next time the event queue is processed, so you won't immediately see the full effect).

Conclusion

Two Eclipse frameworks you will most likely rely upon when building visual plug-ins are SWT and JFace. SWT provides Java wrappers for native platform widgets, which are the foundation of most Eclipse user interface components. JFace, on the other hand, provides higher-order abstractions and patterns for building typical user interfaces in Eclipse, such as viewers, actions, dialogs, and wizards. You extended the previous article's example to demonstrate the use of JFace viewers and menu actions, as well as the creation of a dialog and a multi-page wizard. In the next installment, you will examine the API for working with the Eclipse workbench and its parts.

Resources

  • The example plug-in developed in this article is available here. To import it into your clean, new workspace as a project, click File -> Import; then choose the Existing Projects into Workspace wizard, and specify the path to your downloaded archive file.
  • Eclipse Help (click Help -> Help Contents in the main menu), also available online at http://help.eclipse.org/help31. See the Platform Plug-in Developer Guide, sections Dialogs and Wizards, JFace UI framework, and Standard Widget Toolkit in the Programmer's Guide, and packages org.eclipse.jface.* and org.eclipse.swt.* in API Reference.
  • Eclipse SDK Examples (visit http://www.eclipse.org/downloads and scroll down to the Example Plug-ins section)
  • Eclipse newsgroups (Platform, SWT)
  • SWT Designer is a visual user interface builder for Eclipse (commercial, free version available); Eclipse Visual Editor Project aims to provide a similar tool licensed under EPL.

About the Author

Peter Nehrer is a software consultant living in Toronto, Ontario. He specializes in Eclipse-based enterprise solutions and J2EE applications. His professional interests include development tools, model-driven software development, and information sharing. He is the author and contributor to several Eclipse-related Open Source projects. Peter is an IBM Certified Solutons Developer for XML and related technologies. He holds an M.S. in Computer Science from the University of Massachusetts at Amherst, MA.

Sitemap | Contact Us

Thanks for your registration, follow us on our social networks to keep up-to-date