Skip to main content

Enhancing Swing Applications

October 3, 2006

{cs.r.title}







Quite a few of the more successful applications that came out during the last few years owe some of their success to their extensibility. The application developers provide the basic functionality and the API to extend the basic behavior. These extensions are called plugins or widgets. If written properly, these widgets coexist together on the same platform and can be installed and uninstalled at any time, allowing the end user to tailor his experience according to his taste. These application range from IDEs (Eclipse, NetBeans, and IntelliJ) to web browsers (Firefox and Opera) to dedicated widget hosts (Yahoo Widgets [a.k.a. Konfabulator] and Vista sidebar). Many of them wouldn't be as popular as they are today without this extensibility layer. Unfortunately, Swing doesn't provide an easy way to write such widgets that can be plugged in to any application without changing the application code. This article shows how this can be done in "widgetized" third-party look and feels.

How Swing Provides Consistent Component Behavior

The code base for every core Swing component consists of two separate layers: the component's API (javax.swing package) and the set of look-and-feel delegates for the component (in the javax.swing.plaf package). Let's take a look at the JComboBox component. It provides a very broad API that allows for manipulating the combo box model, editor, listeners, and renderer, but doesn't have a single painting method. Moreover, two very important things that affect the appearance of the combo box are apparently missing: the code that creates the combo box inner components (such as the arrow button, the text field, and the combo pop-up) and the code that lays out these inner components.

All these building blocks of a combo box (and any other core Swing component) are the responsibility of a look-and-feel UI delegate that does the component layout, painting (the look part), and platform- and LAF-specific behavior (the feel part). Although in most cases the component (the combo box, in our case) will look (almost) the same under all core look-and-feels (with a text box and an arrow button that pops up the list of all elements), this is not a mandatory requirement from the specific UI delegate. A UI delegate targeted at a specific OS that doesn't have a combo box may decide to emulate this behavior by creating a spinner-like buttons that iterate through the model items. The end result is a combo box component with a text field and two buttons, but no pop-up.

This separation is the major reason why the JComboBox API doesn't provide any way to get the text field and the arrow button--they simply may not exist for a specific look-and-feel. This is also the reason why the JComboBox API doesn't provide any specific painting API or a layout manager, since these two belong in the specific UI delegate (although in most cases the UI delegate would inherit from the Basic implementation with minor tweaks).

The mechanism for associating a UI delegate with the specific component follows the Factory of factories pattern. The BasicLookAndFeel class provides various init() methods that are called when a new look-and-feel is set. One of these methods (initClassDefaults) adds the UI delegate mapping entries into the UIDefaults table. The mapping entry is a pair that consists of a UI class ID and a UI delegate class name. The UI class ID is a unique identifier for the components of a certain class. The JComboBox defines its UI class ID as:

    private static final String uiClassID = "ComboBoxUI";

The UI delegate class name is the fully qualified class name of the UI delegate for the specific component class. And so, for example, in MetalLookAndFeel.initClassDefaults we can find the following lines:

String metalPackageName = "javax.swing.plaf.metal.";
Object[] uiDefaults = {
   ...
   "ComboBoxUI", metalPackageName + "MetalComboBoxUI",
   ...
};
table.putDefaults(uiDefaults);

When a new look-and-feel is set, the application code usually calls the SwingUtilities.updateComponentTreeUI() method. This method traverses the component hierarchy and calls the updateUI on every component. Although the basic implementation of this method in JComponent is empty, every core Swing component overrides this implementation, fetching the relevant UI delegate using the UIManager.getUI call and setting the resulting delegate on itself. The core Swing UI delegates largely follow the same API (which unfortunately is not specified in the ComponentUI base class). The most important method is a static createUI, which is invoked by reflection in the UIManager.getUI. Two additional methods that are specified in the ComponentUI base class are:

public void installUI(JComponent c)
public void uninstallUI(JComponent c)

The implementation of the first one should install all default settings (colors, borders, etc.); the layout manager; subcomponents; listeners; and other relevant data; while the second one should properly uninstall all of the above to prevent memory leaks. In most cases, the implementation of these two methods is delegated to the following API, which exists in one form or another on all core Swing UI delegates, varying mainly in the method signatures (some are passed the component itself, and some are not):

void installComponents()
void installDefaults()
void installListeners()
void uninstallComponents()
void uninstallDefaults()
void uninstallListeners()

This API separation allows the inheriting UI delegates to provide LAF-specific behavior in a modular way. For example, a look-and-feel that wishes to provide rollover animation sequences will override the installListeners() and uninstallListeners() to install/uninstall the relevant listener without touching the subcomponent/layout/defaults code.

Extending an Existing Component

So what happens when you want to add some behavior to the existing components? One of the more popular features missing in Swing is a contextual edit menu for text components--you right-click on a text field and have a contextual menu with Cut/Copy/Paste/Delete/Select All properly enabled based on the text field selection and clipboard contents. There are three main approaches to provide this behavior:

  • Extending the existing core component (such as JTextField) and adding the relevant mouse listener in the constructor.
  • Extending the existing UI delegate, registering the relevant mouse listener in the installListeners(), and unregistering it in the uninstallListeners().
  • Writing a custom event queue implementation, checking the component under the mouse, and showing the context edit menu if that component is a text component.

The first approach has four limitations. The first is the code reuse. In order to reuse this component in a different application, you will have to either copy and paste the class or refactor it into a separate .jar, which will have to be bundled along with the relevant application. The second is the fact that it's no longer a core component that everybody knows. Instead of using the JTextComponent, the developer now has to learn the new JTextComponentWithContextMenu. The third is the fact that when you want to add another additional behavior, you will have to create a new class that extends your new implementation. This may pose a serious problem when you'll have a collection of multiple behaviors that you'll want to use on the same components, especially when they come from different code bases. A possible solution would be to provide a decorator that adds the relevant behavior to the component without changing the component class. The last limitation is the runtime issue of having to have all the relevant behavior implementations in the classpath. Once one is missing, the component can not be created, resulting in a NoClassDefFoundError.

The second approach provides better code reuse, but has even more serious problems of combining different behaviors on the same component (which can be addressed to a certain point by using delegation in the newly created UI delegate). The main limitation, however, is that this approach is not cross-LAF. Once you select the base UI class, you're stuck with it, making your UI delegate "tied" into the specific look-and-feel. In addition, it places the burden on the application code to register the relevant UI delegate either on the UIManager or on the specific component.

The third approach poses very serious implications of disrupting the entire mechanism of Swing-OS interaction and may be very specific to the JDK version. In addition, combining different behaviors on the same component becomes next to impossible.

Introducing the laf-widget Project

The laf-widget project aims to address the issues outlined above, providing a common layer that is used in third-party look-and-feel implementations that:

  • Allows the UI delegate implementation of the specific look-and-feel to get the list of all extensions (widgets) available for the specific component, and subsequently instantiate and register them.
  • Allows the extension writer to write the widget targeted at a specific component (such as context edit menu for the text components), without worrying about how it will be found and registered at runtime and without targeting a specific look-and-feel implementation.

The extension writer needs to implement the LafWidget interface, which has the following API:

/** Associates a component with this widget. */
public void setComponent(JComponent jcomp);

/** Returns indication whether this widget requires custom LAF
* support */
public boolean requiresCustomLafSupport();

/** Installs UI on the associated component. */
public void installUI();

/** Installs default settings for the associated component. */
public void installDefaults();

/** Installs listeners for the associated component. */
public void installListeners();

/** Installs components for the associated component. */
public void installComponents();

/** Uninstalls UI on the associated component. */
public void uninstallUI();

/** Uninstalls default settings for the associated component. */
public void uninstallDefaults();

/** Uninstalls listeners for the associated component. */
public void uninstallListeners();

/** Uninstalls components for the associated component. */
public void uninstallComponents();

The document "How to Change Existing LAF" describes the changes that need to be done in a look-and-feel to "widgetize" it. This may be done either manually across all the UI delegates or automatically by the provided Ant tasks that use the ASM bytecode manipulation framework; for more information on this approach, see the "How It Works" document on the project site. This process is quite transparent to the widget writer. If the current look-and-feel doesn't support this layer, the widget will be ignored. In addition, only the widgets that are found in the classpath are instantiated and initialized, so there are no NoClassDefFoundErrors. Moreover, the widgets are autonomous "units" that "co-exist" on the same component (as long as they don't interfere with each other, which shouldn't happen if the widget behaves properly).

Writing a Custom Widget

Although laf-widget comes with a wide range of core widgets, there's always room for more. For this article, we show a sample implementation of the following two widgets (see more documentation in the "How to Write Your own Widget" document):

  • Adding page-scroll to the scroll panes when the Ctrl key is pressed during the mouse wheel scroll.
  • Adding tab-scroll to the tabbed panes on the mouse wheel.

The code for the first example is quite straightforward, registering a mouse wheel listener on the associated component. The mouse wheel listener checks if the relevant event is not a wheel block and that a control key is pressed. Then, it decides which scroll bar to use (note that if the Alt key is pressed, the scroll will be done horizontally). Finally, it computes the scroll amount, offsetting the wheel unit event handled by the existing mouse wheel listener and providing the ten-pixel overlay zone. Note how the listener is uninstalled to prevent memory leaks.

public class BlockScrollWidget extends LafWidgetAdapter {
  protected JScrollPane scrollPane;

  protected MouseWheelListener wheelListener;

  public boolean requiresCustomLafSupport() {
    return false;
  }

  @Override
  public void setComponent(JComponent c) {
    super.setComponent(c);
    this.scrollPane = (JScrollPane) c;
  }

  @Override
  public void installListeners() {
    this.wheelListener = new MouseWheelListener() {
      public void mouseWheelMoved(MouseWheelEvent e) {
        if (scrollPane.isWheelScrollingEnabled()
          && (e.getScrollAmount() != 0)
          && (e.getScrollType() ==
             MouseWheelEvent.WHEEL_UNIT_SCROLL)
          && ((e.getModifiers() & InputEvent.CTRL_MASK) != 0)) {

          JScrollBar toScroll =
              scrollPane.getVerticalScrollBar();
          int direction = 0;

          // find which scrollbar to scroll, or return if none
          if ((toScroll == null) || !toScroll.isVisible()
              || ((e.getModifiers() &
                InputEvent.ALT_MASK) != 0)) {
            toScroll = scrollPane.getHorizontalScrollBar();

            if ((toScroll == null) || !toScroll.isVisible()) {
              return;
            }
          }

          direction = (e.getWheelRotation() < 0) ? (-1) : 1;

          int oldValue = toScroll.getValue();
          int blockIncrement =
              toScroll.getBlockIncrement(direction)
              - toScroll.getUnitIncrement(direction);
          // allow for partial page overlapping
          blockIncrement -= 10;
          int delta = blockIncrement *
              ((direction > 0) ? +1 : -1);
          int newValue = oldValue + delta;

          // Check for overflow.
          if ((delta > 0) && (newValue < oldValue)) {
            newValue = toScroll.getMaximum();
          } else if ((delta < 0) && (newValue > oldValue)) {
            newValue = toScroll.getMinimum();
          }

          toScroll.setValue(newValue);
        }
      }
    };

    this.scrollPane.addMouseWheelListener(this.wheelListener);
  }

  @Override
  public void uninstallListeners() {
    this.scrollPane.removeMouseWheelListener(this.wheelListener);
    this.wheelListener = null;
  }
}

The second example is even simpler, and if necessary, can be easily extended to "skip" over disabled tabs.

public class TabScrollWidget extends LafWidgetAdapter {
    private MouseWheelListener listener;

    public boolean requiresCustomLafSupport() {
        return false;
    }

    @Override
    public void installListeners() {
        this.listener = new MouseWheelListener() {
            public void mouseWheelMoved(MouseWheelEvent e) {
                JTabbedPane jtp = (JTabbedPane) jcomp;
                int currSel = jtp.getSelectedIndex();
                currSel += e.getWheelRotation();
                if (currSel < 0)
                    currSel = jtp.getTabCount() - 1;
                if (currSel >= jtp.getTabCount())
                    currSel = 0;
                jtp.setSelectedIndex(currSel);
            }
        };
        this.jcomp.addMouseWheelListener(this.listener);
    }

    @Override
    public void uninstallListeners() {
        this.jcomp.removeMouseWheelListener(this.listener);
        this.listener = null;
    }
}

The final "glue" piece is a META-INF/lafwidget.properties file that will allow locating these two widgets at runtime:

org.jvnet.lafwidget.sample.BlockScrollWidget = javax.swing.JScrollPane

org.jvnet.lafwidget.sample.TabScrollWidget = javax.swing.JTabbedPane

Figures 1 through 6 below illustrate the block scroll widget in action under Substance and the widgetized Looks and Squareness (see batch files in this document or the source .zip to see how these two look-and-feels have been automatically widgetized). Note that four other third-party look-and-feels (Liquid, Pagosoft, InfoNode, and Napkin) have been automatically widgetized as well, to show the minimum amount of changes needed to have a third-party LAF widget-ready.

roll pane under Substance LAF before block scroll
Figure 1. Scroll pane under Substance LAF before block scroll

Scroll pane under Substance LAF after block scroll
Figure 2. Scroll pane under Substance LAF after block scroll

Scroll pane under Looks Plastic XP LAF before block scroll
Figure 3. Scroll pane under Looks Plastic XP LAF before block scroll

Scroll pane under Looks Plastic XP LAF after block scroll
Figure 4. Scroll pane under Looks Plastic XP LAF after block scroll

Scroll pane under Squareness LAF before block scroll
Figure 5. Scroll pane under Squareness LAF before block scroll

Scroll pane under Squareness LAF after block scroll
Figure 6. Scroll pane under Squareness LAF after block scroll

Conclusion

Where to go now? If you want to try some of the existing widgets, you can play with the Substance look-and-feel (version 3.0 and up) or with any other widgetized look and feel (you can find the batch scripts in the source .zip). If you want to write your own widget, head over to the laf-widget project site, download the binaries and the sources, read the documentation and start hacking away. If you want to suggest a widget, drop me a

Resources

width="1" height="1" border="0" alt=" " />
Kirill Grouchnikov has been writing software since he was in junior high school, and after finishing his BSc in computer science, he happily continues doing it for a living. His main fields of interest are desktop applications, imaging algorithms, and advanced UI technologies.
Related Topics >> Swing   |