9.3 The JFace Layer
7.1 SWT is a typical user interface toolkit that provides the standard interaction elements, such as text fields, tables, and trees, out of the box. However, 7.4 it is also designed to be minimal: Since it accesses the native widgets of the platform that the application executes on, the SWT classes must be ported to every supported platform. For that reason, SWT offers only bare-bones functionality. Any higher-level functionality is factored out into the JFace framework, which is pure Java and portable. JFace facilitates connecting the application data structures to the existing SWT widgets, and is therefore indispensable for effective development of user interfaces. It also provides standard elements such as message dialogs and application windows equipped with a menu, toolbar, and status bar.
From a conceptual point of view, JFace provides a complementary perspective on model-view separation. While usually the model is stable and the user interface remains flexible, JFace provides fixed but generic user interface components that connect flexibly to application-specific models. Studying its mechanisms will enhance the understanding of model-view separation itself.
9.3.1 Viewers
12.2.2 The basic approach of JFace is shown in Fig. 9.6(a). JFace establishes a layer between the application’s business logic and the bare-bones SWT widgets. JFace uses methods like setText and setIcon to actually display the data in widgets and registers for low-level events as necessary. It also offers events to the application itself, but these are special in that they translate from the widget level to the model level. For instance, when a user selects a row in a Table widget, SWT reports the index of the row. JFace translates that index into the model element it has previously rendered in the row, and reports that this model element has been selected. In effect, the application is shielded from the cumbersome details and can always work in terms of its own data structures. Of course, it still listens to events such as button clicks directly on the SWT widgets, and translates those into operations on 9.2.1 the model. JFace follows model-view separation in getting the data to be 9.1 displayed from the model and listening to change notifications of the model to keep the display up-to-date.
Figure 9.6 JFace Architecture
We will now discuss the various roles and relationships depicted in Fig. 9.6. This section focuses on the viewers and their collaborators. The listeners, which implement the application’s reactions to user input in the sense of controllers, are discussed in Section 9.3.2. 9.2.1
JFace viewers target specific widget types.
A core contribution of the JFace layer relates to its selection of generic viewers, each of which targets a specific type of widget: A TableViewer targets Tables, a ComboViewer targets a Combo combo box, and so on [Fig. 9.6(b), at the top]. Viewers use the widget-specific methods for displaying data and listen for widget-specific events.
JFace viewers access the application data through adapters.
One question not addressed in Fig. 9.6(a) is how JFace will actually access the application-specific data: How is a generic viewer supposed to know the right getData method and the implementation of the OBSERVER pattern of the specific data structures? Fig. 9.6(b) supplies this detail. First, each viewer holds a reference to the model, in its property input. However, that input is a generic Object, so the viewer never accesses the model itself. 1.3.4 2.4.1 Instead, the viewer is parameterized by two adapter objects that enable it to inspect the model just as required:
- The content provider is responsible for traversing the overall data structure and for splitting it up into elements for display purposes. For a table or list, it provides a linear sequence of elements; for a tree-like display, it also accesses the child and parent links between 2.4.1 the elements. Furthermore, the content provider must observe the model and notify the viewer about any changes that it receives.
- The label provider is called back for each element delivered by the content provider, usually to obtain concrete strings and icons to represent the element on the screen. A ListViewer will request one text/icon combination per element; a TableViewer or TreeViewer will request one combination for each column. The viewer will also observe the label provider to be notified about changes of the text and icons to be displayed.
Let us start with a simple example, in which an application accepts and monitors incoming TCP connections (Fig. 9.7). Whenever a new client connects, the corresponding information gets shown. When the client disconnects, its row is removed from the table.
Figure 9.7 Connection Monitor
Keep the model independent of JFace.
We start by developing the model of the application, with the intention of keeping it independent of the user interface, and more specifically the 9.1 JFace API. The model here maintains a list of connections (which contain a Socket as the endpoint of a TCP connection). Furthermore, it implements the OBSERVER pattern, which explains the registration (and omitted de-registration) of listeners (lines 13–16), as well as the fire method for notifying the listeners (lines 18–20). The method opened() and corresponding method closed() will be called back from the actual server code. Since that code runs in a separate thread, all access to the internal data structures 8.1 needs to be protected by locking. Finally, we decide that the notification of the observers can be performed in an open call (line 10), without holding 8.5 on to the lock.
connections.ConnectionList
1 public class ConnectionList { 2 private ArrayList<Connection> openConnections = 3 new ArrayList<Connection>(); 4 private ListenerList listeners = new ListenerList(); 5 6 void opened(Connection c) { 7 synchronized (this) { 8 openConnections.add(c); 9 } 10 fireConnectionOpened(c); 11 } 12 ... 13 public synchronized void addConnectionListListener( 14 ConnectionListListener l) { 15 listeners.add(l); 16 } 17 ... 18 protected void fireConnectionOpened(Connection c) { 19 ... 20 } 21 ... 22 }
The important point about the model is that it is independent of the user interface: It serves as a central list in which the server code manages the open connections, it synchronizes the different possible accesses, and it notifies interested observers. These observers are completely agnostic of a possible implementation in the user interface as well:
connections.ConnectionListListener
public interface ConnectionListListener extends EventListener { void connectionOpened(ConnectionList p, Connection c); void connectionClosed(ConnectionList p, Connection c); }
This finishes the model in Fig. 9.6(b). We will now fill in the remaining bits.
Create the widget and its viewer together.
The viewer in Fig. 9.6(b) is linked tightly to its SWT widget: The type of widget is fixed, and each viewer can fill only a single widget, since it keeps track of which data it has displayed at which position within the widget. One therefore creates the viewer and the widget together. If a viewer is created without an explicit target widget, it will create the widget by itself. 7.1 The viewer constructor also takes the parent widget and flags, as usual for SWT. The SWT widget is not encapsulated completely, since the display-related services, such as computing layouts, are accessed directly.
connections.Main.createContents
connectionsViewer = new TableViewer(shell, SWT.BORDER); connections = connectionsViewer.getTable(); connections.setLayoutData(new GridData( SWT.FILL, SWT.FILL, true, true, 2, 1)); connections.setHeaderVisible(true);
Connect the viewer to the model through a special content provider.
Each model has, of course, a different structure and API, so that each model will also require a new content provider class. The viewer then receives its own instance of that class.
connections.Main.createContents
connectionsViewer.setContentProvider( new ConnectionListContentProvider());
The reason for this one-to-one match between content provider object and viewer object is that the content provider usually has to be linked up very tightly between the viewer and its input [Fig. 9.6(b)]. The life cycle of the content provider clarifies this. Whenever the viewer receives a new input, it notifies its content provider through the inputChanged() method. The method must also make sure to de-register from the previous input 2.1.2 (lines 8–9). When the viewer is disposed, with the SWT widget, it calls the method again with a new input of null. The logic for de-registering from the old model therefore also kicks in at the end of the life cycle. At this point, the viewer calls the content provider’s dispose() method for any additional cleanup that may be necessary.
connections.ConnectionListContentProvider
1 public class ConnectionListContentProvider implements 2 IStructuredContentProvider, ConnectionListListener { 3 private ConnectionList list; 4 private TableViewer viewer; 5 public void inputChanged(Viewer viewer, Object oldInput, 6 Object newInput) { 7 this.viewer = (TableViewer) viewer; 8 if (list != null) 9 list.removeConnectionListListener(this); 10 this.list = (ConnectionList) newInput; 11 if (list != null) 12 list.addConnectionListListener(this); 13 } 14 public void dispose() {} 15 ... 16 }
The content provider knows how to traverse the model’s structure.
The content provider in Fig. 9.6(b) is an adapter that provides the interface expected by the JFace viewer on top of the application’s model. Designing this interface is an interesting task: Which kind of common structure can one expect to find on all models? The approach in JFace is to start from the minimal requirements of the TableViewer, as the (main) client: A table is 3.2.2 a linear list of rows, so the viewer has to be able to get the data elements behind these table rows. In the current example, each row is a Connection and the model already provides a method to obtain the current list. The inputElement is the viewer’s input model passed to inputChanged(); passing it again enables stateless and therefore shareable content providers.
connections.ConnectionListContentProvider.getElements
public Object[] getElements(Object inputElement) { return ((ConnectionList) inputElement).getOpenConnections(); }
To see more of the idea of generic interface components, let us consider briefly a tree, rendered in a TreeViewer. A tree has more structure than a flat table: The single elements may have children, and all but the top-level elements have a parent. Tree-like widgets usually enable multiple top-level elements, rather than a single root, so that the content provider has the same method getElements() as the provider for flat tables.
org.eclipse.jface.viewers.ITreeContentProvider
public interface ITreeContentProvider extends IStructuredContentProvider { public Object[] getElements(Object inputElement); public Object[] getChildren(Object parentElement); public Object getParent(Object element); public boolean hasChildren(Object element); }
Now the JFace viewer can traverse the application model’s data structure by querying each element in turn. As long as the model has a table-like or tree-like structure, respectively, it will fit the expectations of the JFace layer. In general, each viewer expects a specific kind of content provider stated in its documentation, according to the visual structure of the targeted widget.
org.eclipse.jface.viewers.StructuredViewer
public void setContentProvider(IContentProvider provider)
The label provider decides on the concrete visual representation.
In the end, SWT shows most data on the screen as text, perhaps with auxiliary icons to give the user visual hints for interpreting the text, such as a green check mark to indicate success. The label provider attached to JFace viewers implements just this transformation, from data to text and icons. In the example, the table has three columns for the local port, the remote IP, and the remote port. All of this data is available from the Socket stored in the connection, so the label provider just needs to look into the right places and format the data into strings.
connections.ConnectionListLabelProvider
public class ConnectionListLabelProvider extends LabelProvider implements ITableLabelProvider { ... public String getColumnText(Object element, int columnIndex) { Connection c = (Connection) element; switch (columnIndex) { case 0: return Integer.toString(c.getLocalPort()); case 1: return c.getRemoteAddr().getHostAddress(); case 2: return Integer.toString(c.getRemotePort()); default: throw new IllegalArgumentException(); } } }
By separating the concerns of model traversal and the actual display, JFace gains flexibility. For instance, different viewers might show different aspects and properties of the same model, so that the same content provider can be combined with different label providers.
The viewer manages untyped Objects.
We have found that at this point it is useful to get a quick overview of the viewer’s mechanisms, so as to better appreciate the respective roles and the interactions of the viewer, the content provider, and the label provider. At the same time, these interactions illustrate the concept of 11.1 generic mechanisms, which will become fundamental in the area of frameworks 12.3 and for providing extensibility.
Fig. 9.8 shows what happens from the point where the application supplies the model until the data shows up on the screen. The input is forwarded to the content provider, which chops up the overall model into elements. The viewer passes each of these elements to the label provider and receives back a string. It then displays that string on the screen. For deeper structures, the viewer queries children of elements, and again hands each of these to the label provider, until the structure is exhausted.
Figure 9.8 The Sequence for Displaying Data Through Viewers
In the end, the viewer’s role is to manage untyped objects belonging to the application’s model: It keeps references to the model and all elements as Objects. Whenever it needs to find out more about such an object, it passes the object to the content or label provider. In this way, the viewer can implement powerful generic display mechanisms without actually knowing anything about the application data.
Forward change notifications to the viewer.
We have now set up the display of the initial model. However, the model changes over the time, and it fires change notifications. Like any adapter [Fig. 2.10(b) on page 137], the content provider must also translate those notifications for the benefit of the viewer [Fig. 9.6(b)].
Toward that end, JFace viewers offer generic notification callbacks that reflect the possible changes in the abstract list or tree model that they envision in their content provider interface. A TableViewer, for instance, has callbacks for additions, insertions, deletions, and updates of single elements. The difference between update() and refresh() is that the first method locally recomputes the labels in a single table entry, while the latter indicates structural changes at the element, though it is relevant only for trees.
org.eclipse.jface.viewers.AbstractTableViewer
public void add(Object element) public void insert(Object element, int position) public void remove(Object element) public void update(Object element, String[] properties) public void refresh(Object element)
In the running example, connections can be added to and removed from the list of current connections. The content provider listens to these changes and notifies the viewer accordingly. Since the server uses several threads for the processing of client connections, the content provider must also switch 7.10.1 to the event thread to notify the viewer.
connections.ConnectionListContentProvider.connectionOpened
public void connectionOpened(ConnectionList p, final Connection c) { viewer.getControl().getDisplay().asyncExec(new Runnable() { public void run() { viewer.add(c); } }); }
Viewers provide higher-level services at the application level.
JFace viewers offer more services than just a mapping from application model to screen display. For instance, they enable the application code to work almost entirely at the level of the application model. Consequently, SWT widgets, for example, represent the concept of “selection” by publishing the indices of selected elements. JFace viewers, in contrast, publish IStructuredSelection objects, which are basically sets of model elements. Furthermore, viewers do not map elements directly, but perform preprocessing steps for filtering and sorting. As a final example, they implement mechanisms for inline editing: When the user clicks “into” a table cell, the table viewer creates a small overlay containing an application-specific 7.6 CellEditor that fills the cell’s screen space but is, in fact, a stand-alone widget.
9.3.2 Finishing Model-View-Controller with JFace
9.2.1 JFace viewers already cover much of the MODEL-VIEWER-CONTROLLER pattern, in that the screen reliably mirrors the state of the application’s functional core. The only missing aspect is that of controllers, which interpret the raw user input as requests for performing operations on the model. This will happen in the event-listeners shown in Fig. 9.6.
JFace enables controllers to work on the application model.
Suppose that we wish to implement the button labeled “Close” in Fig. 9.7. Since the button itself is an SWT widget independent of any viewer, we 7.1 attach a listener as usual:
connections.Main.createContents
Button btnClose = new Button(shell, SWT.NONE); btnClose.addSelectionListener(new SelectionAdapter() { public void widgetSelected(SelectionEvent e) { handleCloseSelected(); } });
The method handleCloseSelected() then relies heavily on support from JFace. Line 3 retrieves the viewer’s selection, which maps the indices of rows selected in the table widget to the model elements shown in those rows. As a result, line 5 can ask for the first (and only) selected element and be sure to obtain a Connection, because the viewer’s content provider 9.3.1 has delivered instances of only that class. The crucial point now is that the actual logic for implementing the desired reaction in line 7 remains at the application level: The model’s Connection objects also offer a method close() for terminating the TCP connection with the client.
connections.Main
1 protected void handleCloseSelected() { 2 IStructuredSelection s = 3 (IStructuredSelection) connectionsViewer.getSelection(); 4 Connection selectedConnection = 5 (Connection) s.getFirstElement(); 6 if (selectedConnection != null) { 7 selectedConnection.close(); 8 } 9 }
connections.Connection
public void close() throws IOException { channel.close(); }
Screen updates follow the MVC pattern.
Let us finally reconsider the fundamental reaction cycle of the MODEL-VIEW-CONTROLLER 9.2.1 pattern: The window system delivers events to the view, which forwards them to the controller, which interprets them as requests for operations on the model, which sends change notifications to the view, which repaints parts of the data on the screen. So far, we have seen the first half: SWT delivers the button click to the application’s event-listener, which serves as a controller and decides that the selected connection should be closed.
And now something really interesting happens, because the model is not a simple list, but involves side effects on the underlying TCP connections. Executing close() on the connection goes down to the operation system, which will declare the connection terminated some time later. This, in turn, causes the read() method accepting client input (line 4 in the next code snippet) to return with result “end of stream,” which terminates the server loop (lines 4–6). As a result, this particular server thread terminates (line 10), but not before notifying the ConnectionList about this fact (line 8).
connections.Server.run
1 public void run() { 2 list.opened(conn); 3 ... 4 while (channel.read(buf) != -1) { 5 ... send input back to client as demo 6 } 7 ... 8 list.closed(conn); 9 ... 10 }
Upon receiving this latter signal, the MVC mechanisms kick in to effect the screen update: The ConnectionListContentProvider observes the model and translates the incoming connectionClosed() event into a remove() notification of the table viewer, which removes the corresponding row from the SWT display. That’s it.
9.3.3 Data Binding
The mechanisms of JFace presented so far make it fairly simple to display data so that the screen is kept up-to-date when the data changes. However, the content and label providers have to be programmed by hand, and changing the data is not supported by the framework at all. The concept of data binding addresses both concerns. Broadly speaking, data binding 1.3.3 maps the individual properties of beans to widgets such as text fields or lists. One also says that the properties are bound to the widgets, or more symmetrically that the property and the widget are bound.
7.2 The WindowBuilder includes a graphical tool for creating bindings, so that data binding makes it simple to bridge the model-view separation by quickly creating input masks for given model elements. The usage is mostly intuitive: Select two properties to be bound and click the “bind” button. We will therefore discuss only the few nonobvious cases.
We will discuss the details of data binding in JFace using the example of editing an address book, which is essentially a list of contacts (Fig. 9.9). 1.3.3 The AddressBook and its Contact objects are simple Java beans; that 2.1 is, their state consists of public properties and they send change notifications. From top to bottom in Fig. 9.9, we see the following features of data binding, ordered by increasing complexity: The address book’s title property is bound to a text field; its contacts property is a list of Contact 9.3.1 beans shown in a JFace viewer. In a master/detail view, the details of the currently selected contact are shown in the lower part. Here, the first name, last name, and email properties of the contact are, again, bound directly to text fields. The important property holds a Boolean value and demonstrates the support for different types. Finally, the last contacted property introduces the challenge of converting between the internal Date property and the String content of the text field.
Figure 9.9 Address Book Editor
9.3.3.1 Basics of Data Binding
The data binding framework is very general and is meant to cover many possible applications. Fig. 9.10 gives an overview of the elements involved in one binding. The endpoints, to the far left and right, are the widget and bean created by the application. The purpose of a binding is to synchronize the value of selected properties in the respective beans. Bindings are, in principle, symmetric: They transfer changes from one bean to the other, and vice versa. Nevertheless, the terminology distinguishes between a model and the target of a binding, where the target is usually a widget. The figure 9.1 also indicates the role of data binding in the general scheme of model-view separation.
Figure 9.10 Overview of JFace Data Binding
To keep the data binding framework independent of the application 2.4.1 objects, these are adapted to the IObservableValue interface in the next code snippet, as indicated by the half-open objects beside the properties in Fig. 9.10. The adapters enable getting and setting a value, as well as 9.2.1 observing changes, as would be expected from the basic MVC pattern. The value type is used for consistency checking within the framework, as well as for accessing the adaptees efficiently by reflection.
org.eclipse.core.databinding.observable.value.IObservableValue
public interface IObservableValue extends IObservable { public Object getValueType(); public Object getValue(); public void setValue(Object value); public void addValueChangeListener( IValueChangeListener listener); public void removeValueChangeListener( IValueChangeListener listener); }
The IObservableValue in this code captures values of atomic types. There are analogous interfaces IObservableList, IObservableSet, and IObservableMap to bind properties holding compound values.
Creating these adapters often involves some analysis, such as looking up the getter and setter methods for a named property by reflection. The adapters are therefore usually created by IValueProperty objects, which serve as abstract factories. Again, analogous interfaces IListProperty, 1.4.12 ISetProperty, and IMapProperty capture factories for compound value properties.
org.eclipse.core.databinding.property.value.IValueProperty
public interface IValueProperty extends IProperty { public Object getValueType(); public IObservableValue observe(Object source); ... observing parts of the value }
We have now discussed enough of the framework to bind the name property of an AddressBook in the field model to a text field in the interface. Lines 1–2 in the next code snippet create an IValueProperty for the text property of an SWT widget and use it immediately to create the adapter for the bookname text field. The code specifies that the property is considered changed whenever the user leaves the field (event SWT.FocusOut); setting the event to SWT.Modify updates the model property after every keystroke. Lines 3–4 proceed analogously for the name property of the AddressBook. Finally, lines 5–6 create the actual binding.
databinding.AddressBookDemo.initDataBindings
1 IObservableValue observeTextBooknameObserveWidget = 2 WidgetProperties .text(SWT.FocusOut).observe(bookname); 3 IObservableValue nameModelObserveValue = 4 BeanProperties.value("name") .observe(model); 5 bindingContext.bindValue(observeTextBooknameObserveWidget, 6 nameModelObserveValue, null, null);
9.3.3.2 Master/Detail Views
Fig. 9.9 includes a typical editing scenario: The list contacts is a master list showing an overview; below this list, several fields give access to the details of the currently selected list element. The master list itself involves only binding a property, as seen in the following code snippet. On the viewer 9.3.1 side, special content and label providers then accomplish the data access and updates.
databinding.AddressBookDemo.initDataBindings
IObservableList contactsModelObserveList = BeanProperties .list("contacts").observe(model); contactsViewer.setInput(contactsModelObserveList);
The actual master/detail view is established by a two-step binding of properties. Lines 3–4 in the next example create a possibly changing value that tracks the currently selected Contact element as a value: Whenever the selection changes, the value of the property changes. Building on this, lines 5–8 create a two-step access path to the first name property: The observeDetail() call tracks the current Contact and registers as an observer for that contact, so that it also sees its property changes; the value() call then delivers an atomic String value for the property. Through these double observers, this atomic value will change whenever either the selection or the first name property of the current selection changes.
databinding.AddressBookDemo.initDataBindings
1 IObservableValue observeTextTxtFirstObserveWidget = 2 WidgetProperties.text(SWT.Modify).observe(txtFirst); 3 IObservableValue observeSingleSelectionContactsViewer = 4 ViewerProperties.singleSelection().observe(contactsViewer); 5 IObservableValue contactsViewerFirstnameObserveDetailValue = 6 BeanProperties 7 .value(Contact.class, "firstname", String.class) 8 .observeDetail(observeSingleSelectionContactsViewer); 9 bindingContext.bindValue(observeTextTxtFirstObserveWidget, 10 contactsViewerFirstnameObserveDetailValue, null, null);
9.3.3.3 Data Conversion and Validation
We finish this section on data binding by discussing the crucial detail of validation and conversion. The need arises from the fact that the model’s data is stored in formats optimized for internal processing, while the user interface offers only generic widgets, so that the data must often be displayed and edited in text fields. One example is the last contacted property of a Contact, which internally is a Date, but which is edited as a text with a special format (Fig. 9.9).
The basic property binding follows, of course, the master/detail approach. 9.3.3.2 The new point is the use of update strategies (Fig. 9.10), as illustrated in the next code snippet. Each binding can be characterized by separate strategies for the two directions of synchronization. Lines 1–5 specify that the text entered in the interface should be converted to a Date to be stored in the model, and that this transfer should take place only if the text is in an acceptable format. The other direction in lines 6–8 is less problematic, as any Date can be converted to a string for display. Lines 9–11 then create the binding, with the specified update policies.
databinding.AddressBookDemo.initDataBindings
1 UpdateValueStrategy targetToModelStrategy = 2 new UpdateValueStrategy(); 3 targetToModelStrategy.setConverter(new StringToDateConverter()); 4 targetToModelStrategy.setAfterGetValidator( 5 new StringToDateValidator()); 6 UpdateValueStrategy modelToTargetStrategy = 7 new UpdateValueStrategy(); 8 modelToTargetStrategy.setConverter(new DateToStringConverter()); 9 bindingContext.bindValue(observeTextTxtLastcontactedObserveWidget, 10 contactsViewerLastContactedObserveDetailValue, 11 targetToModelStrategy, modelToTargetStrategy);
To demonstrate the mechanism, let us create a custom converter, as specified by the IConverter interface. The method convert() takes a string. It returns null for the empty string and otherwise parses the string 1.5.7 into a specific format. It treats a parsing failure as an unexpected occurrence.
databinding.StringToDateConverter
public class StringToDateConverter implements IConverter { static SimpleDateFormat formatter = new SimpleDateFormat("M/d/yyyy"); ... source and destination types for consistency checking public Object convert(Object fromObject) { String txt = ((String) fromObject).trim(); if (txt.length() == 0) return null; try { return formatter.parse(txt); } catch (ParseException e) { throw new IllegalArgumentException(txt, e); } } }
The validator checks whether a particular string matches the application’s expectations. In the present case, it is sufficient that the string can be converted without error, which is checked by attempting the conversion. In other cases, further restrictions can be suitable.
databinding.StringToDateValidator
public class StringToDateValidator implements IValidator { public IStatus validate(Object value) { try { StringToDateConverter.formatter.parse((String) value); return Status.OK_STATUS; } catch (ParseException e) { return ValidationStatus.error("Incorrect format"); } } }
Conversion and validation are specified separately since they often have to vary independently. Very often, the converted value has to fulfill further restrictions beyond being convertible, such as a date being within a specified range. Also, even data that is not constrained by the internal type, such as an email address stored as a String, must obey restrictions on its form.
9.3.4 Menus and Actions
We have seen that JFace viewers connect generic SWT widgets such as lists or tables to an application model [Fig. 9.6(b) on page 473]: The viewer queries the data structures and maps the data to text and icons within the widget. It also listens to model changes and updates the corresponding entries in the widget.
A similar mechanism is used for adding entries to menus and toolbars. SWT offers only basic MenuItems, which behave like special Buttons and 7.1 notify attached listeners when they have been clicked. SWT menu items, just like other widgets, are passive: While they can show a text and icon, and can be enabled or disabled, they wait for the application to set these properties.
To keep this chapter self-contained, the presentation here refers to the example application MiniXcel, a minimal spreadsheet editor to be introduced in Section 9.4. For now, it is sufficient to understand that at the core, a SpreadSheetView displays a SpreadSheet model, as would be expected from the MODEL-VIEW-CONTROLLER pattern. 9.2.1
Actions represent application-specific operations.
JFace connects SWT menus to application-specific actions, which implement 1.8.6 IAction (shown next). Actions wrap code that can act directly on the application’s model (lines 6–7). But actions also describe themselves for display purposes (lines 3–4), and they identify themselves to avoid showing duplicates (line 2). Finally, it is anticipated that an action’s properties 9.1 will change, in much the same way that an application’s model changes (lines 9–12).
org.eclipse.jface.action.IAction
1 public interface IAction { 2 public String getId(); 3 public String getText(); 4 public ImageDescriptor getImageDescriptor(); 5 6 public void run(); 7 public void runWithEvent(Event event); 8 9 public void addPropertyChangeListener( 10 IPropertyChangeListener listener); 11 public void removePropertyChangeListener( 12 IPropertyChangeListener listener); 13 ... setters for the properties and further properties 14 }
Contribution items connect menu items to actions.
To connect SWT’s passive menu items to the application’s available actions, JFace introduces menu managers and contribution items (Fig. 9.11, upper part). Each menu is complemented by a menu manager that fills the menu and updates it dynamically when the contributions change. Each SWT menu item is complemented by a contribution item that manages its appearance. Initially, it fills the menu item’s text, icon, and enabled state. Whenever a property of the action changes, the contribution item updates the menu item correspondingly. In the reverse direction, the contribution item listens for clicks on the menu item and then invokes the action’s run() method (or more precisely, the runWithEvent() method).
Figure 9.11 Menus and Actions in JFace
Actions are usually shared between different contribution managers.
One detail not shown in Fig. 9.11 is that action objects are independent of the concrete menu or toolbar where they get displayed. They are not simply an elegant way of filling a menu, but rather represent an operation and thus have a meaning in themselves. Eclipse editors usually store their actions in a local table, from where they can be handed on to menus and toolbars. In the example, we use a simple hash map keyed on the action’s ids.
minixcel.ui.window.MainWindow
private Map<String, IAction> actions = new HashMap<String, IAction>();
Create the menu manager, then update the SWT widgets.
Once the table holds all actions, a concrete menu can be assembled quickly: Just fill a menu manager and tell it to update the menu. For instance, the MiniXcel spreadsheet application has an edit menu with typical undo and 9.4 redo actions, as well as a “clear current cell” action. Lines 1–8 create the structure of nested menu managers. Lines 9–11 flush that structure into the visible SWT menu.
minixcel.ui.window.MainWindow.createContents
1 MenuManager menu = new MenuManager(); 2 ... set up File menu 3 MenuManager editMenu = new MenuManager("Edit"); 4 menu.add(editMenu); 5 editMenu.add(actions.get(UndoAction.ID)); 6 editMenu.add(actions.get(RedoAction.ID)); 7 editMenu.add(new Separator("cellActions")); 8 editMenu.add(actions.get(ClearCellAction.ID)); 9 shlMinixcel.setMenuBar(menu.createMenuBar( 10 (Decorations)shlMinixcel)); 11 menu.updateAll(true);
Actions are usually wired to some context.
The lower part of Fig. 9.11 highlights another aspect of action objects: They are self-contained representations of some operation that the user can invoke through the user interface. The run() method is the entry point; everything else is encapsulated in the concrete action. This means, however, that the action will be linked tightly to a special context. In the example, the action that clears the currently selected cell must certainly find and access that cell, so it needs a reference to the SpreadSheetView. (The 9.5 command processor cmdProc is required for undoable operations, as seen later on.)
minixcel.ui.window.MainWindow
private void createActions() { ... actions.put(ClearCellAction.ID, new ClearCellAction(spreadSheetView, cmdProc)); }
The same phenomenon of exporting a selection of possible operations 174 is also seen in Eclipse’s wiring of actions into the global menu bar. There, again, the actions are created inside an editor component but get connected to the global menu and toolbar. This larger perspective also addresses the question of how global menu items are properly linked up to the currently open editor.