Automatic Widgeting of Look and Feels
Today I am thrilled to have Michael Kneebone as a guest spot blogger on “Pushing Pixels”. Michael has extended the widgetising support in the Laf-Widget project and has graciously agreed to write about its usage and how it works on the inside. If you have any questions, leave a comment or contact Michael directly at mlk <At> cs <dot> bham <dot> ac <dot> uk
The LAF Widget (Look and Feel) project enables Swing components to be augmented with additional features or behaviour to make them more useful. Each new behaviour is contained in a “widget” which adds some specific behaviour to one (or several) types of Swing component. For instance one type of widget augments JInternalFrame
s by adding a thumbnail preview of the content when the mouse is hovered over a minimised frame, while another widget adds a padlock icon to uneditable text fields. Some widgets are more involved like the password strength checker widget which provides visual feedback to the user on password fields indicating the quality of the chosen password.
The key motivation behind LAFWidget is that the application writer should not have to program support for any of this behaviour him/herself. GUI development is conducted in the same way as before and all widgets automatically work with the GUI, with no added effort from the developer. This is in contrast to naive approaches often advocated by less experienced Java programmers where sub-classing is used whenever alternate behaviour is required from a component. Sub-classing is a very heavy handed approach for modifying GUI components due its inflexibility. Consider adding a common context menu to text components (e.g. cut, copy, paste, etc.); you could create MyJTextField
, MyJTextArea
, etc. subclasses adding the required behaviour. You would then be required to revisit the entire application substituting the original Swing components for these modified versions – not an attractive prospect. The situation would be even worse if subclassed text components were already in use, since each of those would have to be subclassed individually as well.
Luckily, Swing includes the notion of a pluggable Look-And-Feel (LAF), allowing the appearance and behaviour of all components to be separate from their use. Each Swing component (JComboBox, JPanel
, etc.) has a specific UI delegate class in the current LAF which is responsible for its rendering and behaviour. For example, the JSlider
UI delegate must draw the slider as well as listen for MouseEvent
s on the slider thumb and all other interaction. It is this mechanism that is exploited by the LAFWidget project. There are many LAFs in existence today, some bundled with the JDK such as Metal, Windows, GTK and more recently, Nimbus as well as numerous 3rd party LAFs such as Substance, Liquid and Napkin. As of writing, only Substance has native support for LAFWidgets with widgets being automatically applied just by selecting using the Substance LAF on an application (see Getting Started for a quick set-up tutorial). The LAFWidget project is completely independent of the current LAF however, and can work with the majority of other LAFs available, including those mentioned above.
Up until now, integrating the LAFWidget project (known as “widgetising” a look-and-feel) with other look-and-feels has required extensive work on either the part of the individual LAF developer or the application writer wishing to use it. Previously, a developer could widgetise their LAF by following the instructions at How to Change Existing LAF. This amounts to adding calls to each Swing UI delegate to query the list of widgets available for that component, and then installing each widget in turn. The actual modifications to each UI delegate are fairly straightforward, but the necessity of repeating the process on all 42 UI classes is longwinded. As such, very few other LAF developers have done so.
The other option detailed on the above link under “Automatic changes” allows an application developer to modify an existing LAF. This involves getting the LAF .jar file and carrying out an intricate process involving bytecode manipulation to inject the calls into the code. Here I present the new method for widgetising LAFs which allows the LAFWidget project to easily integrate with the vast majority of LAFs without needing support from the original developer and not an Ant task in sight! It also has the advantage of working with all of the standard LAFs included with the JDK which wasn’t possible before.
How To Use The LAF Widget project
The new widgetising approach is available in version 4.2 of the LAFWidget project onwards (https://laf-widget.dev.java.net/). To utilise the LAFWidget project with your own application, simply follow these steps:
- Download version 4.2 or above of the LAFWidget from https://laf-widget.dev.java.net/
- Include the laf-widget.jar on your classpath.
- In the main method of your application add
org.jvnet.lafwidget.LAFAdapter.startWidget()
; before the GUI is created.
That’s it! All Swing components will now appear with the appropriate widgets enabled.
How The Process Works
The idea behind the LAFAdapter
class is to mimic the manual delegation (described in the How to Change Existing LAFs), replicating the steps the LAF author would have to take to widgetise a look and feel.
At the centre of Swing’s look and feel management is the UIManager
class. Crucially, it maintains a list of which class implements the UI for each Swing component through a class called UIDefaults
. When a Swing component is constructed it calls, UIManager.getUI(this)
which in turn will callback to the component requesting its “UIClassID”, a unique string identifying the type of component. You can see a list of all known UIClassIDs at the top of the LAFAdapter
class.
The UIClassID is used to query the UIDefaults
for the UI delegate class that will render the component. Once this class has been obtained, UIManager
calls that class’s static createUI()
method reflectively to create an actual instance. For example, JLabel
returns “LabelUI” as its unique ID, which when queried in the UIDefaults
table returns something like “javax.swing.plaf.metal.MetalLabelUI”. Calling createUI()
on that class creates a LabelUI
for rendering JLabel
s.
This is where we hook into the system. You can view the complete source code to LAFAdapter
here: LAFAdapter.java
When startWidget()
is called, the code jumps through a couple of hoops to ensure it is running on the Event Thread (remember all Swing code should be executed on this thread) and eventually calls setup()
where the widgeting begins.
public static void setup() throws Exception {
//check we don't initialise twice
if (initialised)
return;
//We use the LAF defaults table so we don't mess with the developer table
reinitListener = new ReinitListener();
internalListener = new InternalUIListener();
uiDelegates=new UIDefaults(); //stores the actual UI delegates
UIDefaults defaults = UIManager.getLookAndFeelDefaults();
//Store the class for each delegate locally and replace the Swing delegate
for (String uiClassID : UI_CLASSNAMES) {
uiDelegates.put(uiClassID,defaults.getString(uiClassID));
defaults.put(uiClassID,LAFAdapter.class.getName());
}
//listen for global LAF changes
UIManager.addPropertyChangeListener(reinitListener);
initialised=true;
}
ReinitListener is simply a listener which we attach to the UIManager
to listen for global LAF changes so the widgeting keeps functioning after a global LAF change (although mid application LAF changes are rare). InternalUIListener
is needed later. The next few lines are crucial, we overwrite the original entries in the UI defaults table with the name of the LAFAdapter
class while storing the original value in an internal table named “uiDelegates”. Essentially the LAFAdapter
class is telling Swing that it is going to become the UI delegate for every Swing component! Finally we note that we have been initialised to prevent double initialisation.
Now, whenever a component is created, the createUI()
method at the top of the code is called, allowing us to intercept the component creation. Here is the code from createUI()
:
public static ComponentUI createUI(JComponent c) {
//Now we use the same discovery mechanism to find the real createUI method
ComponentUI uiObject = uiDelegates.getUI(c);
uninstallLafWidgets(c);
//here we need to check we don't add a second listener. This happens
//if createUI gets called a second time, as when creating JDesktopIcons
if (!isPropertyListening(c))
c.addPropertyChangeListener("UI",internalListener);
return uiObject; //return the actual UI delegate
}
The first thing we do is retrieve the true UI delegate from our stashed list of delegates. The call to uninstallLafWidgets()
is to remove any previously added widgets from the component. Then, if we are not already listening on the UI property change, we add a listener to the component to listen for it, passing in the InternalUIListener
from above and then return the true delegate class. You might ask why we don’t add the widgets to the component now?
The answer is that createUI()
is called in the middle of component construction and we don’t want to start adding widgets to a component until it is complete as this could have unexpected effects on the component or the widgets added. All Swing components fire the UI property change when they are assigned their UI delegate so when that happens, we add the widgets:
private static class InternalUIListener implements PropertyChangeListener {
public void propertyChange(PropertyChangeEvent evt) {
JComponent c=(JComponent)evt.getSource();
// Remove old listeners that was installed when createUI was called
c.removePropertyChangeListener("UI",internalListener);
installLafWidgets(c); //here we do the install on the LAF
}
}
In the listener, it firstly de-registers itself from the component since we only need one notification, and then installLafWidgets()
is called to actually add the widgets. The code for installing LAF widgets is the most straightforward part of the code:
private static void installLafWidgets(JComponent c) {
if (LafWidgetRepository.getRepository().getLafSupport()
.getClass().equals(LafWidgetSupport.class)) {
Set lafWidgets=LafWidgetRepository.getRepository().getMatchingWidgets(c);
if (lafWidgets.size()0) {//if a new UI has been set
for (LafWidget lw : lafWidgets) {
lw.installUI();
lw.installComponents();
lw.installDefaults();
lw.installListeners();
}
c.putClientProperty(LAF_PROPERTY,lafWidgets); //stash the list of installed widgets
}
}
}
Some LAFs have native support for the LAFWidget project like Substance, so the first condition checks if that is the case. If so, then this method doesn’t need to do anything. Otherwise, the LAFWidget class LAFWidgetRepository
is queried for a list of widgets for this component. A simple loop then calls the “install” lifecycle methods for each widget in the set. The entire set is then stashed in the component so they can be uninstalled later if LAF changes occur.
The uninstallation method basically is the reverse of the above. To see it in action, try getting the SwingSet2.jar from the JDK demo folder, then use the code below to widgetise the SwingSet.
- Save the following code as SwingSet2Adapter.java
- Assuming laf-widget.jar and SwingSet2.jar are in the same folder, compile with changing the ; to : if on Linux:
- Run with:
public class SwingSet2Adapter {
public static void main(String[] args) {
org.jvnet.lafwidget.LAFAdapter.startWidget();
SwingSet2.main(null);
}
}
javac -cp SwingSet2.jar;laf-widget.jar;. SwingSet2Adapter.java
java -cp SwingSet2.jar;laf-widget.jar;. SwingSet2Adapter
Change to the Windows LAF (if on Windows) and try minimising the internal frames and hovering the mouse over the title bar (this effect is a little buggy under Metal). Also notice the padlock icon on source code and text fields that can’t be edited.
The instructions earlier are all you need to use this in your own applications. I hope this will encourage you to use the LAFWidget project in your own applications since it offers many additions to make Swing applications look that little bit slicker.