The (J)Face of Eclipse
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.
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 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).