Skinning Eclipse Applications with a Swing Look and Feel

Skinning Eclipse Applications with a Swing Look and Feel

By Tim Dalton, OCI Senior Software Engineer & Mario Aquino, OCI Principal Software Engineer

April 2009


Introduction

Over the last 20 years the human interaction with the computer has evolved from simple text or form based terminals to a much richer format with graphics, animation and other aids to improve the user experience. Human factors engineers have spent considerable time understanding user-base needs and tailoring both the look and the flow amongst screens to ensure optimal behavior. As applications evolve and add new capabilities, the associated user interactions also expand and evolve. In order to minimize retraining and avoid misinterpretation of new icons, widgets consistency is a must. This consistency is often referred to as promoting portability of skills; users can intuitively grasp new capabilities and respond correctly.

GUI tools which assist developers in creating rich user experiences fall into two classes, those that follow an existing platform model and those that can span platforms. Java-based GUI toolkits such as Swing benefit from the JVM abstraction layer so that regardless of the underlying OS the GUI appears the same. Conversely, Eclipse is based on extending the underlying platform characteristics. This has the benefit of maintaining some common look and feel across unrelated applications sharing the same platform. However it has the drawback that it is not easy to get the same application looking and feeling the same, on different platforms.

Eclipse applications use a library called the Standard Widget Toolkit (SWT) to build graphical user interfaces. SWT utilizes the underlying native operating system to render graphical widgets (or controls) like buttons, combo boxes, and scrollbars. Organizations for whom establishing a common look and feel across multiple platforms provides business value may avoid developing on top of SWT because of its close integration with natively designed controls. Companies with an investment in SWT that desire either a uniform look and feel across platforms or the ability customize application UI should not have to rearchitect their applications, replacing SWT with Swing, to gain the ability to control their appearance. As well, enterprises with a substantial number of Swing applications considering a transition to SWT might hesitate at the prospect of losing an established look and feel standard. This paper explores the ways in which developers can overcome the natural predilection of Eclipse to conform to the underlying platform GUI model and apply dramatic customization to SWT-based applications.

SWT and Swing

On Windows platforms, SWT interacts with the Win32 and WPF APIs, on Linux with the GTK+ (Gimp ToolKit), and on MacOS with the Carbon or Cocoa API. Strengths of SWT include fidelity to the appearance of the underlying platform's native widgets and an edge in performance since the widgets are being rendered natively.

Swing renders the widgets by itself, which allows it to maintain a more consistent look and feel across platforms. On top of that, Swing includes an API for creating customized look and feels for all components in the toolkit (read more about Swing's architecture here). Java developers can choose between the default look and feel and several variations shipped with the JDK as well as many third-party libraries for customizing the appearance of their Swing GUI applications. In recent versions of the Java Runtime Environment (JRE), particularly since version 1.6, Swing has been well optimized and has greatly narrowed the performance gap between it and native components.

The merits of SWT and Swing have been a subject of debates in various forums on the Internet over the years. Both toolkits have differing design goals and can provide varying advantages depending on the context of their use. The fidelity of SWT to the look of the underlying platform becomes a weakness for an application that requires a look and feel that spans platforms. Rewriting such applications to use Swing seems an impractical approach for moving towards that goal, which leaves two options: re-implement SWT "on top" of Swing much in the way SWT is built on top of native API interaction or add the ability to customize or "skin" the appearance of SWT widgets.

Re-implementing SWT in Swing

An open-source project, SWTSwing, was created to realize the goal of re-implementing SWT using Swing components. That project made great strides toward re-implementing SWT, completing 90% compatibility with the reference library of "snippets" maintained by the SWT development project. However, work on SWTSwing stopped in mid-2007 and as of early-2009 had not resumed. Drawbacks to the re-implementation approach include limited support for embedding native components (like a web browser — which is fully supported by SWT) and a difference in threading constraints; SWT applications use event dispatch loops that can be nested, which under SWTSwing can result in a CPU-intensive loop running on the Event Dispatch Thread.

Skinning SWT with a Swing "Facade"

A popular open-source SWT application that uses a skinning technique is the Bittorrent client, Azureus Vuze. There the skinning is limited to custom components implemented as SWT Canvas objects, but similar techniques can apply to any SWT widget to seamlessly skin them without changes to applications using them. Below is a summary of techniques that can be used to skin a small subset of SWT widgets. The approach is generic enough that it could be applied to all SWT controls for which there are analogous components in Swing.

Buttons

The SWT Button is a configurable component that can act as a normal push button, a radio button, a checkbox, or a toggle button. It is a simple component that usually displays some text (or an icon) and responds to selection events that trigger the button's action. Unlike the Swing analog (JButton), the SWT Button class does not paint itself, in fact neither do any of the other SWT components. The Button class accepts listeners for various kinds of events (button selection, mouse entry and exit, focus entry and exit, etc). The sequence of widget painting-related interactions is illustrated in the diagram below. When a user-triggered interaction occurs (1), the appropriate listeners to the button are notified (2) and then the button calls a system-level library that is responsible for repainting the view of the button (3). The native library calls back into the button (4), giving it a chance to let other listeners know that the button has just painted itself (5).

Widget Call Chain

SWT does not include a facility for customizing the appearance of native components displayed in the UI. However, the sequence of event notification — alerting component listeners when user interaction has occurred, painting a component, and then notifying listeners that painting has occurred — presents an opportunity to paint over the native component with whatever customized imagery is desired.

The Swing approach for event notification and repainting follows a similar pattern; user interaction triggers event notification to any registered listeners of a component and after notification, the component repaints itself depending on the effect of the listener notification and the initial action. It is convenient that the two GUI toolkits follow similar sequences (interaction-response-repaint) because it allows the synchronization of analogous component appearance based on the invocation of common events. For example, both SWT Button and Swing JButton controls respond to a mouse entering their display area. In both cases, an event object is passed to the respective UI element, causing event listeners to be notified and ultimately repainting the component. This similar event sequence can be exploited so that a Swing JButton's appearance can appear in place of an SWT Button. An object added as a listener to an SWT Button can collect SWT events, translate those to their Swing analogs and pass the translated events to a JButton (not actually displayed in the application), then call for the JButton to paint itself. Any special highlighting or effects produced by the Swing Look and Feel utilized by the JButton would be included when the JButton paint operation occurs. The output of the JButton painting itself (a BufferedImage) can be overlaid in front of the native SWT Button display, giving the effect of the native component customized to look like a Swing component.

The customized look is just an illusion; the native widget is still there, beneath the facade of the painted-over Swing component image. This is advantageous because it means that all other events the widget normally responds to will still work. For those other event types, the same approach of listening for interaction cues and synchronizing the analog Swing component so that its paint output can be overlaid in front of the native widget also works. The following graphic looks at the interaction more in depth:

Full Widget Repaint Cycle

As an interaction event comes to the widget (1), the widget calls the appropriate listener for the interaction event (2). A single object implementing multiple listener interfaces and registered appropriately with the widget (i.e. listening for all mouse or keyboard-driven events) is represented in the image with the name EventMonitor (in bolded text). This object caches details of the events it has received from the widget. Next, the widget calls an OS-level paint API (3) which paints the native widget and then calls back (4) to the widget to indicate that the native widget has been painted. The widget then notifies any objects listening for paint events (5) that widget painting has completed. Again, the image above indicates that a specific paint listener (shown as EventPainter ) receives the paint notice, retrieves the cached event details (6) from the EventMonitor and synchronizes the SWT event details with the appropriate Swing/AWT event type and passes that to a JComponent that will produce a painted Image object (7) representing what that component would look like if it were actually realized in the application's UI. The Image object painted by the JComponent is finally (8) overlayed using the screen coordinates from the PaintEvent passed from the Widget to the EventPainter (from step 5) on top of the native widget's display. At the completion of this cycle, the native widget is no longer visible because the image of the analogous JComponent has been painted in its place on the screen.

The approach as it has been described so far has left out an important detail: connecting listeners to SWT widgets so events can be forwarded to a Swing component for customized painting. Ideally, a customized look to an SWT GUI application would happen for free; the client code that creates widgets and lays them out in Shell and Composite containers would be written as normal and the customized widget appearance would somehow just happen. This is the way that Swing custom look and feels work — they are largely transparent to the client UI-building code so that looks can be swapped out without needing to change application source. AspectJ - a framework for applying Aspect-Oriented programming techniques to Java applications - makes the task of inconspicuously adding custom paint responders a simple one; creating aspects that intercept at the point of Widget creation and attach listeners for interaction and paint events removes the need for client code to do anything at all to achieve a custom appearance over native widgets. The following sample code illustrates this:

  1. public aspect ButtonPainting {
  2. pointcut init(Button b) : this(b) && execution(Button.new(..));
  3.  
  4. after(Button b): init(b) {
  5. EventMonitor monitor = EventMonitor.newMonitor();
  6. b.addMouseTrackListener(monitor);
  7. b.addMouseListener(monitor);
  8. b.addFocusListener(monitor);
  9. AbstractEventPainter<Button> painter = ButtonPainterFactory
  10. .newButtonPainter(b, monitor);
  11. painter.decorate(b);
  12. b.addPaintListener(painter);
  13. }
  14. }

An event monitor listens for and caches interaction events, then the SWT events are translated into an analogous Swing event object which is then passed to the Swing component used for creating the paint facade. The synchronized painting happens in response to the SWT paint event listener receiving notification that the native component has repainted itself.

Since the native widget is briefly displayed before being overlaid by the Swing facade, there will be a flicker. Unfortunately, the solution to this problem is platform-specific. On Win32, notification messages from the OS are sent to callback functions on the SWT widget. For example, the WM_PAINT message is a notifier that indicates a widget needs to be painted. By intercepting that message it becomes possible to suppress the native rendering of the widget and eliminate the flicker. Handling the message interception can be done with aspects injected into the Java code that handles callbacks in the SWT library.

Combo Boxes

SWT Combo widgets are a bit more complicated than buttons because they are composite types made up of several other widgets: a button trigger, a text component for displaying selection and a popup window for showing the options. The approach of attaching listeners to the widget and synchronizing the interaction event with a JComboBox is as valid for Combos as it is for Buttons. However, because the Combo is a composite, the event painter needs to determine exactly where on the Combo certain events (like mouse enter, exit, click, etc.) are happening. The Swing look and feel may call for the down arrow button to display a different image when the mouse is hovering over the button versus when the button has nothing in front of it. The mouse may also move over the text display of the Combo but that shouldn't cause the down arrow to repaint itself. Event listeners for the Combo must therefore determine relative location of some the interaction events and pass the appropriate messages to a JComboBox used for painting in order for it to paint the appropriate effect. Fortunately, mouse-triggered events from SWT capture the coordinate location of the mouse relative to its position on the widget. When a mouse event is processed by the event listener for Combos, the listener only needs to calculate the width of the (down arrow) JButton used by the JComboBox and subtract that from the width of the JComboBox itself to deduce whether the mouse event needs to be forwarded to the JButton within the JComboBox or to the text field to its left.

Scrollbars

Intercepting SWT PaintEvents is not feasible for the customization of scrollbars on Windows. SWT Widgets that use scrollbars inherit from org.eclipse.swt.widget.Scrollable class, which allow this problem to be solved for multiple components in one place. On Windows, SWT scrollable controls utilize scrollbars provided by the underlying OS. Neither SWT nor the Win32 API provide a means to customize those scrollbars. On Win32, scrollbars are rendered in the "non-client area" (a region of the display not accessible by PaintListeners), so it becomes necessary to overlay those native scrollbars with Win32 STATIC controls. Subclassing the STATIC controls ensures that the WM_PAINT messages from the OS are captured and that the image of a Swing scrollbar can be rendered on top of the native scrollbar. The STATIC control allows events to be passed through it to the underlying native scrollbar so it continues to function as if it is not overlaid. Below are screenshots demonstrating the scrollbars, the first one has the scrollbars deliberately offset to show the underlying Win32 controls:

Complications can arise if the Swing Look and Feel scrollbar has significant differences in various characteristics especially the thickness of the bar itself. One minor complication would also be if the "thumb" sizes are different. The user must have the mouse over the thumb of the underlying scrollbar in order for it to work. The Swing scrollbar is just a facade and will not appear to work correctly if misaligned with underlying scrollbar. In the above screenshot showing the offset, slight differences in thumb sizes are evident especially in the vertical scrollbar, but in this case side-effects are barely noticeable.

Shells

A Shell in SWT is analogous to a Container in Swing. Most Swing Look and Feels do not use any special customizations (like gradient painting or background images) for Containers. In those cases, synchronizing the characteristics Swing and SWT have in common like foreground color, background color, and font is sufficient to match any gap in appearance between a native (SWT) rendering and a custom Swing Look and Feel. This produces a facsimile of a Swing components look and feel rather than a complete overpaint of a Swing facade. One notable exception to this is the Napkin Look and Feel, which uses a background image for all Containers. Napkin provides a unique challenge for UI synchronization because SWT does not support background images for Shells. Implementing a "paint over" is possible, but would need to be performance-optimized with native paint interception techniques to prevent flicker during the repainting of large image sizes. Since techniques may vary depending on the Swing look and feel used, it would be prudent to apply a configurable painting strategy (driven by a properties file, for example).

Screenshots

Native SWT Widgets

Ocean Look and Feel

Napkin Look and Feel

Another Swing Look and Feel

Live Demonstration

The following video shows an SWT frame with simple controls (button, radio control, checkbox, toggle, combo, scrollable list) then shows the same frame code running "skinned" with a Swing look and feel.

The video shows how the skinned-UI responds to standard mouse entry/exit/selection events. The source for this frame is as follows:

  1. import org.eclipse.swt.SWT;
  2. import org.eclipse.swt.events.SelectionEvent;
  3. import org.eclipse.swt.events.SelectionListener;
  4. import org.eclipse.swt.layout.GridData;
  5. import org.eclipse.swt.layout.GridLayout;
  6. import org.eclipse.swt.widgets.Button;
  7. import org.eclipse.swt.widgets.Combo;
  8. import org.eclipse.swt.widgets.Composite;
  9. import org.eclipse.swt.widgets.Display;
  10. import org.eclipse.swt.widgets.List;
  11. import org.eclipse.swt.widgets.Shell;
  12.  
  13. public class SkinTest1 {
  14.  
  15. public static void main(String[] args) {
  16. final Display display = new Display();
  17. final Shell shell = new Shell(display);
  18. shell.setLayout(new GridLayout());
  19. final Composite c = new Composite(shell, SWT.NONE);
  20. GridLayout layout = new GridLayout();
  21. layout.numColumns = 4;
  22. c.setLayout(layout);
  23. addButton(c, "OK", SWT.PUSH);
  24. addButton(c, "Radio", SWT.RADIO);
  25. addButton(c, "Check", SWT.CHECK);
  26. addButton(c, "Toggle", SWT.TOGGLE);
  27. addCombo(c, SWT.READ_ONLY, "Read Only");
  28. addList(c, SWT.SINGLE, "Single");
  29.  
  30. shell.pack();
  31. shell.open();
  32. while (!shell.isDisposed()) {
  33. if (!display.readAndDispatch())
  34. display.sleep();
  35. }
  36. display.dispose();
  37. }
  38.  
  39. private static void addList(final Composite c, int style, String text) {
  40. final List list = new List(c, SWT.BORDER | style | SWT.V_SCROLL);
  41. GridData layoutData = new GridData();
  42. layoutData.minimumWidth = 100;
  43. layoutData.widthHint = 100;
  44. layoutData.heightHint = 150;
  45. layoutData.minimumHeight = 150;
  46. list.setLayoutData(layoutData);
  47. for (int i=0; i<128; i++) list.add (text + " selection " + i);
  48. }
  49.  
  50. private static void addCombo(Composite c, int style, String buttonText) {
  51. Combo combo = new Combo(c, style);
  52. for(int i = 0; i < 3; i++) combo.add(buttonText + " " + i);
  53. }
  54.  
  55. private static void addButton(Composite c, final String buttonText, int style) {
  56. Button b = new Button(c, style);
  57. GridData layoutData = new GridData();
  58. layoutData.minimumWidth = 60;
  59. layoutData.widthHint = 60;
  60. layoutData.heightHint = 25;
  61. layoutData.minimumHeight = 25;
  62. b.setLayoutData(layoutData);
  63. b.setText(buttonText);
  64. }
  65. }

Possible Optimizations

Since SWT and Swing each have their own Image class, it is necessary to convert from the java.awt.Image that contains the Swing facade to a org.eclipse.swt.graphics.Image to be drawn by SWT. It is essential that this be optimized as much as possible. One potential optimization would be to have the java.awt.Image drawn directly on the drawing context of the underlying OS. This would require platform-specific code written for Windows, Linux, and MacOS to handle image drawing according to native APIs. Overlaying scrollbars with Swing look and feel custom images requires this approach because SWT doesn't receive event callbacks (from the platform scrollbar implementation) as that component is interacted with via mouse or keyboard, rather everything is done at the native OS level.

Another area for optimization could be caching images generated for certain components (buttons, read-only combos, etc.) that aren't likely to change. The approach described in this article depends on image generation from Swing components compelled to paint themselves. The images output from those components can be cached and reused as similar events reoccur, further improving the responsiveness of the user interface.

Conclusion

Though SWT does not specifically include an API for enhancing the appearance of its widgets, customizing the look and feel of Eclipse-based applications is most certainly achievable. The details described in this article are based on work done for the purposes of exploring how an SWT GUI can be altered without requiring modification of the main application source (the code that actually composes the UI). The non-invasive look and feel customization is a great strength of the Swing toolkit and this article has shown that the same flexibility can exist for SWT. Though this article does not present a completed library for applying any Swing look and feel to SWT applications, the approach detailed here provides a validated basis for such a library. Any groups interested in finding out more about customizing SWT applications with Swing Look and Feels should contact OCI directly: sales@ociweb.com.

secret