Even though the JFC comes complete with look-and-feel implementations for popular windowing systems and a platform-neutral look-and-feel, you may still be required to create a custom look-and-feel. An example of such a requirement is a corporation that wants a company-branded application to be available on all platforms used by the company. This reduces the learning curve for users and reduces the dependency on hardware platforms. The Java look-and-feel can be used to meet the requirement of the same user interface on all platforms but does not meet the branding requirement.
Another requirement for a custom look-and-feel implementation is a UI designer who may want to implement a complete look-and-feel to demonstrate his or her design.
There are multiple levels of customizations that can be made to a look-and-feel. These range from changing colors and icons in an existing look-and-feel implementation to providing a custom user interface implementation for every component.
In this chapter, you will learn
An amazing feature of the pluggable look-and-feel architecture the Swing components are built on is how much of the "look" can be customized without extending a single class. A look-and-feel implementation should not hard code its look into the source code. Instead, properties that define the look are registered with the UIManager. When the user interface delegate is to render a component, the properties are read from the UIManager and used appropriately for the component.
A client application can alter the UI properties to create a custom look. The set of properties that defines a look are often referred to as a theme. The Java look-and-feel has built-in support for installing custom themes. An ad hoc approach must be taken with other look-and-feel implementations.
Setting properties in the UIManager provides a simple mechanism for altering the look of JFC components. However, you need to know the list of available properties to be able to change them. The available properties can be determined by examining the source code for the look-and-feel or by examining the properties defined in the UIManager at runtime. In this section, a combination of both techniques will be presented.
The PropertyViewer application shown in Listing 23.1 is an application that displays the properties of any of the registered look-and-feels. In the createComboBox method, the registered look-and-feels are queried from the UIManager by using the following line of code. An array of LookAndFeelInfo objects is returned from the getInstalled LookAndFeels method in the UIManager class. The LookAndFeelInfo class is defined in the UIManager class and contains information about a look-and-feel. This class contains two methods of interest: getName and getClassName. The name is a string suitable for use in menus or combo boxes, as shown here. The getClassName returns the name of the class that defines the look-and-feel. The result is a combo box containing the name of all the registered look-and-feels available in the JFC installation.
UIManager.LookAndFeelInfo[] info = UIManager.getInstalledLookAndFeels();
An ItemListener is added to the combo box. When the item is changed, the new look-and-feel is loaded and the properties are updated in the table. The setLookAndFeel method is used to update the look-and-feel for the application. The look-and-feel is set by passing the class name of the desired look-and-feel to the setLookAndFeel method contained in the UIManager class. The setLookAndFeel method updates the UIDefaults object that will cause the new look-and-feel to be used for newly created objects. However, it does not alter existing components. To change the UI object for existing components, call the updateComponenetTreeUI method in the SwingUtilities class. As its name implies, this method updates the UI object for the tree of components starting at the given component. If this method is called for each top-level window in an application, the look-and-feel can be totally changed at runtime. There is a single JFrame for this application, and that is passed to the updateComponenetTreeUI method. The following code fragment demonstrates this technique:
UIManager.setLookAndFeel( className ); SwingUtilities.updateComponentTreeUI( JOptionPane.getFrameForComponent( this ) );
The dumpLAFProperties method is used to query the UIDefaults instance from the UIManager and update the table model using these values. The following code performs this task:
UIDefaults defaults = UIManager.getDefaults(); Enumeration keys = defaults.keys(); Enumeration elements = defaults.elements(); tableModel.update( keys, elements );
The complete PropertyViewer application is in Listing 23.1. The executing application is shown in Figure 23.1.
Listing 23.1 The PROPERTYVIEWER
Application
package com.foley.test; import java.awt.*; import java.awt.event.*; import java.util.*; import javax.swing.*; import javax.swing.table.*; import com.foley.utility.*; /** * An application that displays the properties * of the installed look-and-feels * * @author Mike Foley **/ public class PropertyViewer extends JPanel implements ItemListener { JComboBox comboBox; JTable table; UITableModel tableModel; /** * PropertyViewer, constructor * <p> * Create the **/ public PropertyViewer() { super(); setLayout( new BorderLayout() ); comboBox = createComboBox(); tableModel = new UITableModel(); sorter = new TableSorter( tableModel ); table = new JTable( sorter ); comboBox.addItemListener( this ); add( comboBox, BorderLayout.NORTH ); add( new JScrollPane( table ), BorderLayout.CENTER ); dumpLAFProperties(); } /** * Create a combo box containing the installed * look-and-feels. * <p> * @return A combo box containing the installed L&Fs. **/ protected JComboBox createComboBox() { JComboBox comboBox = new JComboBox(); UIManager.LookAndFeelInfo[] info = UIManager.getInstalledLookAndFeels(); for( int i = 0; i < info.length; i++ ) { comboBox.addItem( info[i].getName() ); } return( comboBox ); } public void itemStateChanged( ItemEvent event ) { if( event.getStateChange() == ItemEvent.SELECTED ) { System.out.println( event.getItem() ); String name = event.getItem().toString(); UIManager.LookAndFeelInfo[] info = UIManager.getInstalledLookAndFeels(); for( int i = 0; i < info.length; i++ ) { if( name.equals( info[i].getName() ) ) { setLookAndFeel( info[i].getClassName() ); dumpLAFProperties(); break; } } } } /** * Set the look-and-feel to the given L&F. * Update your UI to the new look-and-feel. * <p> * @param className The class name of the L&F to set. **/ private void setLookAndFeel( String className ) { try { UIManager.setLookAndFeel( className ); SwingUtilities.updateComponentTreeUI( JOptionPane.getFrameForComponent( this ) ); } catch( Exception e ) { System.err.println( "Cound not set look-and-feel" ); } } /** * Print the properties of the current look- * and-feel to the text area component. **/ public void dumpLAFProperties() { UIDefaults defaults = UIManager.getDefaults(); Enumeration keys = defaults.keys(); Enumeration elements = defaults.elements(); tableModel.update( keys, elements ); } /** * Application entry point. * Create a frame and a table and display them. * * @param args Command line parameter. Not used. **/ public static void main( String args[] ) { JFrame frame = new ApplicationFrame( "PropertyViewer" ); PropertyViewer propertyViewer = new PropertyViewer(); propertyViewer.setBorder( BorderFactory.createLoweredBevelBorder() ); Container content = frame.getContentPane(); content.add( propertyViewer, BorderLayout.CENTER ); frame.pack(); frame.setVisible( true ); } // main } // PropertyViewer class UITableModel extends AbstractTableModel { private Vector keyData; private Vector valueData; public UITableModel() { super(); keyData = new Vector(); valueData = new Vector(); } public int getRowCount() { return( keyData.size() ); } public int getColumnCount() { return( 2 ); } public Class getColumnClass( int column ) { return( String.class ); } public String getColumnName( int column ) { if( column == 0 ) { return( "Key" ); } else { return( "Current Value" ); } } public Object getValueAt(int row, int column) { if( column == 0 ) { return( keyData.elementAt( row ) ); } else { return( valueData.elementAt( row ) ); } } public void update( Enumeration keys, Enumeration values ) { keyData.removeAllElements(); valueData.removeAllElements(); while( keys.hasMoreElements() ) { keyData.addElement( keys.nextElement() ); valueData.addElement( values.nextElement() ); } fireTableStructureChanged(); } } // UITableModel
Figure 23.1 : The PropertyViewer application.
The previous section presented an application that displays the look-and-feel properties for any of the installed look-and-feels. Now that you know what properties are available for customizing Swing components, you can register your preferences with the UIManager.
As an example, customize the JTree class' UI object by altering some of its properties. Figure 23.1 displays the available properties for the JTree class and their default values for the Java look-and-feel. Properties range from the foreground and background colors to the icons used for the various tree node states. The main method of the TreeTest application presented in Chapter 11, "Tree Component," will be modified to register properties for the JTree class's UI object. The unmodified application is shown in Figure 23.2.
The following lines of code were added to the main method of the TreeTest application before the components were created. These lines of code set properties in the UIDefaults object. The put method in the UIManager class is a convenience method and is the equivalent of first getting the UIDefaults from the UIManager and then calling the put method of the UIDefaults object directly. The UIDefaults class is an extension of the Hashtable class, allowing the familiar Hashtable class's API to be used when modifying UI properties.
UIManager.put( "Tree.background", Color.lightGray ); UIManager.put( "Tree.textForeground", Color.blue ); UIManager.put( "Tree.textBackground", Color.yellow ); UIManager.put( "Tree.font", new Font( "Helvetica", Font.BOLD, 24 ) ); UIManager.put( "Tree.leftChildIndent", new Integer( 24 ) );
Figure 23.2 : The unmodified TreeTest application.
Setting the "Tree.background" property in the first line sets the tree's background color to black. Similarly, the text foreground and background color properties are set to blue and yellow, respectively. The font for the tree is set to an eye-popping 24-point Helvetica bold font. Because of the large font, the default indentation seemed too small, so the final line sets this property to 24 pixels. The resulting tree is shown in Figure 23.3. Any of the UI object's properties can be set by using this technique.
Figure 23.3 : The modified TreeTest application.
NOTE |
Care must be taken when setting properties with the UIManager.put method. The keys are Stringinstances. A typo in the key will execute without producing an error, but the property will not be used. The property will be added to the UIDefaults table but, since the key was not correct, when the UI object retrieves the property by using the correct key, your value will not be used. An interface containing all the keys defined as constants would eliminate this type of error. |
A look-and-feel is defined in a LookAndFeel class. The standard naming convention for this class is the look-and-feel implementation's name followed by LookAndFeel. For example, the MS Windows look-and-feel is defined in the WindowsLookAndFeel class. The LookAndFeel class contains methods to query the name, description, and ID for the look-and-feel. It also contains the initialization code for the UIDefaults properties. This file presents a valuable resource for property names and their default values.
When using the Java look-and-feel, also known as Metal, changing the theme can easily alter properties being used in an application. A theme consists of colors and fonts used by the look-and-feel.
The Java look-and-feel is designed by using three primary colors and three secondary colors for most components. The primary colors are the foreground colors, and the secondary colors are the background colors. Creating a theme class that specifies these six colors gives an easy method to change an application's look. Specifying fonts and colors in the theme can provide further customization.
The complete MetalTheme abstract class is shown in Listing 23.2. The listing shows the complete set of methods that a theme can override to customize the look-and-feel. The Metal look-and-feel defines the DefaultMetalTheme class, which is a concrete extension of the MetalTheme class. The DefaultMetalTheme class provides the default theme for the Metal look-and-feel and is also shown in Listing 23.2.
Listing 23.2 The METALTHEME and
DEFAULTMETALTHEME Classes
public abstract class MetalTheme { private static ColorUIResource white = new ColorUIResource( 255, 255, 255 ); private static ColorUIResource black = new ColorUIResource( 0, 0, 0 ); public abstract String getName(); // these are blue in Metal Default Theme protected abstract ColorUIResource getPrimary1(); protected abstract ColorUIResource getPrimary2(); protected abstract ColorUIResource getPrimary3(); // these are gray in Metal Default Theme protected abstract ColorUIResource getSecondary1(); protected abstract ColorUIResource getSecondary2(); protected abstract ColorUIResource getSecondary3(); public abstract FontUIResource getControlTextFont(); public abstract FontUIResource getSystemTextFont(); public abstract FontUIResource getUserTextFont(); public abstract FontUIResource getMenuTextFont(); public abstract FontUIResource getWindowTitleFont(); public abstract FontUIResource getSubTextFont(); protected ColorUIResource getWhite() { return white; } protected ColorUIResource getBlack() { return black; } public ColorUIResource getFocusColor() { return getPrimary2(); } public ColorUIResource getDesktopColor() { return getPrimary2(); } public ColorUIResource getControl() { return getSecondary3(); } public ColorUIResource getControlShadow() { return getSecondary2(); } public ColorUIResource getControlDarkShadow() { return getSecondary1(); } public ColorUIResource getControlInfo() { return getBlack(); } public ColorUIResource getControlHighlight() { return getWhite(); } public ColorUIResource getControlDisabled() { return getSecondary2(); } public ColorUIResource getPrimaryControl() { return getPrimary3(); } public ColorUIResource getPrimaryControlShadow() { return getPrimary2(); } public ColorUIResource getPrimaryControlDarkShadow() { return getPrimary1(); } public ColorUIResource getPrimaryControlInfo() { return getBlack(); } public ColorUIResource getPrimaryControlHighlight() { return getWhite(); } public ColorUIResource getSystemTextColor() { return getPrimary1(); } public ColorUIResource getControlTextColor() { return getControlInfo(); } public ColorUIResource getInactiveControlTextColor() { return getControlDisabled(); } public ColorUIResource getInactiveSystemTextColor() { return getSecondary2(); } public ColorUIResource getUserTextColor() { return getBlack(); } public ColorUIResource getTextHighlightColor() { return getPrimary3(); } public ColorUIResource getHighlightedTextColor() { return getControlTextColor(); } public ColorUIResource getWindowBackground() { return getWhite(); } public ColorUIResource getWindowTitleBackground() { return getPrimary3(); } public ColorUIResource getWindowTitleForeground() { return getBlack(); } public ColorUIResource getWindowTitleInactiveBackground() { return getSecondary3(); } public ColorUIResource getWindowTitleInactiveForeground() { return getBlack(); } public ColorUIResource getMenuBackground() { return getSecondary3(); } public ColorUIResource getMenuForeground() { return getBlack(); } public ColorUIResource getMenuSelectedBackground() { return getPrimary2(); } public ColorUIResource getMenuSelectedForeground() { return getBlack(); } public ColorUIResource getMenuDisabledForeground() { return getSecondary2(); } public ColorUIResource getSeparatorBackground() { return getWhite(); } public ColorUIResource getSeparatorForeground() { return getPrimary1(); } public ColorUIResource getAcceleratorForeground() { return getPrimary1(); } public ColorUIResource getAcceleratorSelectedForeground() { return getBlack(); } public void addCustomEntriesToTable(UIDefaults table) { } } public class DefaultMetalTheme extends MetalTheme { private final ColorUIResource primary1 = new ColorUIResource(102, 102, 153); private final ColorUIResource primary2 = new ColorUIResource(153, 153, 204); private final ColorUIResource primary3 = new ColorUIResource(204, 204, 255); private final ColorUIResource secondary1 = new ColorUIResource(102, 102, 102); private final ColorUIResource secondary2 = new ColorUIResource(153, 153, 153); private final ColorUIResource secondary3 = new ColorUIResource(204, 204, 204); private final FontUIResource controlFont = new FontUIResource("Dialog", Font.BOLD, 12); private final FontUIResource systemFont = new FontUIResource("Dialog", Font.PLAIN, 12); private final FontUIResource windowTitleFont = new FontUIResource("SansSerif", Font.BOLD, 12); private final FontUIResource userFont = new FontUIResource("Dialog", Font.PLAIN, 12); private final FontUIResource smallFont = new FontUIResource("Dialog", Font.PLAIN, 10); public String getName() { return "Steel"; } // these are blue in Metal Default Theme protected ColorUIResource getPrimary1() { return primary1; } protected ColorUIResource getPrimary2() { return primary2; } protected ColorUIResource getPrimary3() { return primary3; } // these are gray in Metal Default Theme protected ColorUIResource getSecondary1() { return secondary1; } protected ColorUIResource getSecondary2() { return secondary2; } protected ColorUIResource getSecondary3() { return secondary3; } public FontUIResource getControlTextFont() { return controlFont;} public FontUIResource getSystemTextFont() { return systemFont;} public FontUIResource getUserTextFont() { return userFont;} public FontUIResource getMenuTextFont() { return controlFont;} public FontUIResource getWindowTitleFont() { return controlFont;} public FontUIResource getSubTextFont() { return smallFont;} }
The OutlandishTheme class presented in Listing 23.3 shows an extension to the DefaultMetalTheme class that overrides the primary and secondary colors. When this theme is set for the TreeTest application, the result is the window shown in Figure 23.4. The window using the DefaultMetalTheme was shown in Figure 23.2.
Listing 23.3 The OUTLANDISHTHEME
Class
package com.foley.utility; import javax.swing.plaf.ColorUIResource; import javax.swing.plaf.metal.DefaultMetalTheme; /** * This class provides an outlandish color theme. * The primary and secondary colors are overridden. * The fonts and other theme properties are left unaltered. * <p> * @author Mike Foley **/ public class OutlandishTheme extends DefaultMetalTheme { public String getName() { return "OutlandishTheme"; } // // Primary Colors. // private final ColorUIResource primary1 = new ColorUIResource( 255, 0, 0 ); private final ColorUIResource primary2 = new ColorUIResource(200, 25, 25); private final ColorUIResource primary3 = new ColorUIResource(150, 150, 150); protected ColorUIResource getPrimary1() { return primary1; } protected ColorUIResource getPrimary2() { return primary2; } protected ColorUIResource getPrimary3() { return primary3; } // // Secondary Colors. // private final ColorUIResource secondary1 = new ColorUIResource( 0, 255, 0); private final ColorUIResource secondary2 = new ColorUIResource(25, 200, 25); private final ColorUIResource secondary3 = new ColorUIResource(40, 180, 40); protected ColorUIResource getSecondary1() { return secondary1; } protected ColorUIResource getSecondary2() { return secondary2; } protected ColorUIResource getSecondary3() { return secondary3; } } // OutlandishTheme
Figure 23.4 : The OutlandishTheme set for the TreeTest application.
The theme is set for the application by calling the static setCurrentTheme method contained in the MetalLookAndFeel class. The following line of code was added to the main method in the TreeTest application to set OutlandishTheme for the application. The theme can be dynamically changed when the application is running. This allows themes to be selected from a menu or read from a property file while the application is executing.
MetalLookAndFeel.setCurrentTheme( new OutlandishTheme() );
NOTE |
The Metalworks demonstration application that is part of the JDK distribution provides many custom themes that can be dynamically changed by selecting the desired theme from a menu. An example of reading a theme from a property file is also demonstrated. |
The OutlandishTheme only customizes colors. However, other properties are just as easily specified in a theme. For example, adding the following lines of code to the OutlandishTheme class will set all the application's fonts to an 18-point sans serif bold font. Of course all the fonts don't have to be the same, and typically they aren't. The resulting window is shown in Figure 23.5. Creating a theme with large fonts is useful when giving demonstrations of an application.
Figure 23.5 : Large fonts added to the TreeTest application.
// // Fonts. // private final FontUIResource font = new FontUIResource("SansSerif", Font.BOLD, 18); public FontUIResource getControlTextFont() { return font;} public FontUIResource getSystemTextFont() { return font;} public FontUIResource getUserTextFont() { return font;} public FontUIResource getMenuTextFont() { return font;} public FontUIResource getWindowTitleFont() { return font;} public FontUIResource getSubTextFont() { return font;}
The last method of interests in the MetalTheme class is the addCustomEntriesToTable method. This method is intended to provide the theme an opportunity to register custom properties in the UIDefaults table. These are the same UI properties that were identified and set in the previous section. Being able to set the properties in the theme class allows all customizations to be encapsulated in the same class. For example, the customizations to the TreeUI shown in the previous section would be specified in a theme by setting the properties in the addCustomEntriesToTable method. An example for this method that achieves the same customizations for the TreeUI object is shown next. Notice how the superclass' addCustomEntriesToTable method is called first to initialize the UIDefaults table with values inherited from the parent theme. (In the DefaultMetalTheme this method is empty.) After the table is initialized, the custom values can be specified in the table. The look-and-feel calls this method during initialization or when the theme is changed. After the method returns, the UIDefaults object is set in the UIManager.
public void addCustomEntriesToTable(UIDefaults table) { super.addCustomEntriesToTable(table); table.put( "Tree.background", Color.black ); table.put( "Tree.textForeground", Color.blue ); table.put( "Tree.textBackground", Color.yellow ); table.put( "Tree.font", new Font( "Helvetica", Font.BOLD, 24 ) ); table.put( "Tree.leftChildIndent", new Integer( 24 ) ); }
NOTE |
Themes are an extremely powerful method for customizing an application. Reading the theme from a property file provides an easy mechanism that gives users the ability to customize an application. However, themes are only available when using the Java look-and-feel. This is unfortunate. |
To this point, all the customizations presented have to do with the look of an application, not its feel. This is for two reasons. First, there are not as many hooks in the JFC for customizing feel. The UIDefaults object does not contain properties that map user gestures to actions. However, this would be a welcome addition.
Secondly, more care must be used when altering the feel of an application. Changing the way an application responds to user gestures can make the program difficult to use. For example, if the feel of a button is changed to require a double-click instead of a single-click to perform the action, users will not know how to use the application and can become confused.
Changing colors, fonts, and icons can make an application look drastically different, but the standard components will still behave the way the user expects. This may not be the situation when the feel is changed. Moreover, there is no visual clue to the user that the feel has been changed. Thus, changing the feel of standard components must be performed with extreme caution and only for very good reasons.
When the situation does arise when the feel of a component needs to be changed, it is possible to do so. The technique is similar to that shown in the previous chapter when a new UI object was installed for the ToolTipUI.
The TreeUI class expands and contracts a node when it is double-clicked with the mouse. I had an application that opened a dialog box when a node in the tree was double-clicked, and the tree expanding or contracting was a distraction to the user. The JTree class and the TreeUI implementations do not contain a property to control this action, so a new UI class was written and installed for the JTree class. The double-click causing the tree to expand and collapse was the only modification made to the tree's feel. Since the control immediately to the left on the node's text expands and collapses the tree, and the double-click performs another action that the user sees, changing this aspect of the tree's feel did not detract from the user's ability to use the application-instead, it enhanced it.
The NonExpandingTreeUI class shown in Listing 23.4 will prevent a JTree instance from expanding and collapsing when the user double-clicks a node. The createUI method is boilerplate look-and-feel code. It creates the UI object for the given component-in this case, a new instance of the NonExpandingTreeUI class. The isToggleEvent method is defined in the BasicTreeUI class to determine if the given mouse event should toggle the expansion state of a node. The BasicTreeUI class tests the click count in the event and returns true for a count of two and false for other counts. The NonExpandingTreeUI class always returns false from this method. Thus, no mouse event will toggle the node's expansion state.
Listing 23.4 The NONEXPANDINGTREEUI
Class
package com.foley.utility; import java.awt.event.MouseEvent; import javax.swing.JComponent; import javax.swing.plaf.ComponentUI; import javax.swing.plaf.basic.BasicTreeUI; /** * A TreeUI that does not automatically expand or * collapse the tree on a double click event. * <p> * @author Mike Foley **/ public class NonExpandingTreeUI extends BasicTreeUI { /** * Create the UI object for the given tree. This * is a new instance of this class. **/ public static ComponentUI createUI( JComponent tree ) { return( new NonExpandingTreeUI() ); } /** * NonExpandingTreeUI, constructor **/ public NonExpandingTreeUI() { super(); } /** * Returning true indicates the row under the mouse should be toggled * based on the event. This is invoked after * checkForClickInExpandControl, implying the location is not in the * expand (toggle) control * <p> * This is the behavior we want to stop, so always return false. * <p> * @return false **/ protected boolean isToggleEvent( MouseEvent event ) { return( false ); } } // NonExpandingTreeUI
Registering the UI Class
The code to register the NonExpandingTreeUI class is shown next. This code is the same as that shown in the previous chapter when the ToolTipUI was replaced. Two keys in the UIDefaults table define which UI class will be used for a component's UI object. The first uses the uiClassID, defined in the component as the key, and maps to the class key. The second uses the class key defined in the first entry to the name of the class to use for that UI object. If there is not an entry in the UIDefaults table for the class key, the class key is assumed to be the name of the class, and an instance of that class is instantiated. Thus, if the value returned from the uiClassID key lookup is the name of the UI object's class, the second entry in the table can be omitted. The following code will map all TreeUI requests to the NonExpandingTreeUI.
try { String nonExpandingTreeUIClassName = "com.foley.utility.NonExpandingTreeUI"; UIManager.put( "TreeUI", nonExpandingTreeUIClassName ); UIManager.put( nonExpandingTreeUIClassName, Class.forName( nonExpandingTreeUIClassName ) ); } catch( ClassNotFoundException cnfe ) { System.err.println( "NonExpanding Tree UI class not found" ); System.err.println( cnfe ); }
NOTE |
The NonExpandingTreeUI class in the previous example extends the BasicTreeUI class and is registered for all look-and-feels. This has the side effect of losing the custom look- and-feel behavior defined in the specific look-and-feel TreeUI implementations. For exam- ple, the dividing lines available in the Java look-and-feel are not available when the NonExpandingTreeUI class is used. The workaround to this situation is to have a version of the NonExpandingTreeUI for each look-and-feel that your application supports. The NonExpandingTreeUI would extend the specific UI, rather than the UI in the basic package. For example, a WindowsNonExpandingTreeUI extends the WindowsTreeUI class, MetalNonExpandingTreeUI extends the MetalTreeUI class, and MacNonExpandingTreeUI extends the MacTreeUI class if your application uses the native look-and-feel on Windows and the Macintosh, and the Java look-and-feel on UNIX platforms. If multiple UI classes are supported, the registration process with the UIManager changes slightly. Instead of mapping the TreeUI key to the custom UI class key, the TreeUI map- ping is left alone. Instead, the mapping from the look-and-feel class is altered to the cus- tomized class. The following code shows this: try { UIManager.put( "javax.swing.plaf.metal.MetalTreeUI", Class.forName("com.foley.utility.MetalNonExpandingTreeUI")); UIManager.put( "com.sun.java.swing.plaf.windows.WindowsTreeUI", Class.forName("com.foley.utility.WindowsNonExpandingTreeUI")); UIManager.put( "com.sun.java.swing.plaf.windows.MacTreeUI", Class.forName( "com.foley.utility.MacNonExpandingTreeUI" ) ); } catch( ClassNotFoundException cnfe ) { System.err.println( "NonExpanding Tree UI classes not found" ); System.err.println( cnfe ); } |
In the previous section, a custom look-and-feel class was created and
registered for the TreeUI. The customization was particularly easy
because the behavior you wanted to alter was defined in a method that you could
override. This is a good programming
technique. Instead of burying
implementation details deep in a method, define them in properties or query them
from a "getter" method that can be overridden by extensions.
The classes in the basic package use this technique when creating the behavior of the look-and-feel. Instead of hard coding the listeners in the class, protected create methods are called. This allows an extension to override the create method and return a custom listener. If the isToggleEvent method was not defined in the BasicTreeUI class, you could have achieved the same result by changing the mouse listener for the tree. This would have been more complex, as the other existing behavior needs to remain. However, the hooks are in place to replace a listener. For example, in the installUI method in the BasicTreeUI class, the following lines appear:
// Boilerplate install block installDefaults(); installListeners(); installKeyboardActions(); installComponents();
Any of these methods can be overridden to change the feel of the component with which the UI is used. Looking at the installListeners method shown next shows that a single class of listener can be changed by overriding the appropriate create method. For the previous example, the createMouseListener method would have to have been overridden to return the custom mouse listener. This listener would then have implemented the desired feel.
protected void installListeners() { if((propertyChangeListener = createPropertyChangeListener())!= null){ tree.addPropertyChangeListener(propertyChangeListener); } if ( (mouseListener = createMouseListener()) != null ) { tree.addMouseListener(mouseListener); } if ((focusListener = createFocusListener()) != null ) { tree.addFocusListener(focusListener); } if ((keyListener = createKeyListener()) != null) { tree.addKeyListener(keyListener); } if((treeExpansionListener = createTreeExpansionListener()) != null) { tree.addTreeExpansionListener(treeExpansionListener); } if((treeModelListener = createTreeModelListener()) != null && treeModel != null) { treeModel.addTreeModelListener(treeModelListener); } if((selectionModelPropertyChangeListener = createSelectionModelPropertyChangeListener()) != null && treeSelectionModel != null) { treeSelectionModel.addPropertyChangeListener (selectionModelPropertyChangeListener); } if((treeSelectionListener = createTreeSelectionListener()) != null && treeSelectionModel != null) { treeSelectionModel.addTreeSelectionListener(treeSelectionListener); } }
The hooks are in place in the basic package to allow fine-grain customizations of the look-and-feel class. Creating the customized feel often involves looking at the UI to be customized in the basic package and determining the proper location to tie the custom code into the look-and-feel implementation.
In the last chapter and previous sections in this chapter, you have seen many techniques for customizing the look-and-feel implementations delivered with the JFC. You have seen how to customize properties in the UIDefaults table maintained by the UIManager, how to register themes when using the Java look-and-feel, and how to selectively replace user interface implementations. If this level of customizing does not meet your requirements, an entire look-and-feel can be developed. You have already seen most of the techniques required to create a complete look-and-feel. In this section, the remaining pieces of the puzzle will be presented, and a look-and-feel will be implemented.
The elements that comprise a look-and-feel are defined in the look-and-feel class. These elements include description and function methods, as well as a method to create the UIDefaults table for the look-and-feel. You have already seen examples of the user interface classes for the ToolTipUI and TreeUI objects.
The look-and-feel class must be an extension of the abstract javax.swing.LookAndFeel class. LookAndFeel is a class, not an interface, that defines the API for a look-and-feel. This class is shown in Listing 23.5. The class contains three types of methods. There are many static convenience methods that are used internally by Swing, and they are not overridden by extensions. These methods include the installColors, installColorsAndFont, installBorder, uninstallBorder, parseKeyStroke, makeKeyBindings, and makeIcon methods. The next type of methods are the informational methods. These are typically one-line or very small methods provided by extensions of the LookAndFeel class. The getName, getID, getDescription, isNativeLookAndFeel, and isSupportedLookAndFeel all fall into this category. The final type of methods in the LookAndFeel class are the functional methods. These are the methods that are called to install and uninstall a look-and-feel. The initialize, uninitialize, and getDefaults methods fall into this category.
Listing 23.5 The LOOKANDFEEL
Class
public abstract class LookAndFeel { /** * Convenience method for initializing a component's foreground * and background color properties with values from the current * defaults table. The properties are only set if the current * value is either null or a UIResource. * * @param c the target component for installing default * color/font properties * @param defaultBgName the key for the default background * @param defaultFgName the key for the default foreground * * @see #installColorsAndFont * @see UIManager#getColor */ public static void installColors(JComponent c, String defaultBgName, String defaultFgName) { Color bg = c.getBackground(); if (bg == null || bg instanceof UIResource) { c.setBackground(UIManager.getColor(defaultBgName)); } Color fg = c.getForeground(); if (fg == null || fg instanceof UIResource) { c.setForeground(UIManager.getColor(defaultFgName)); } } /** * Convenience method for initializing a components foreground * background and font properties with values from the current * defaults table. The properties are only set if the current * value is either null or a UIResource. * * @param c the target component for installing default * color/font properties * @param defaultBgName the key for the default background * @param defaultFgName the key for the default foreground * @param defaultFontName the key for the default font * * @see #installColors * @see UIManager#getColor * @see UIManager#getFont */ public static void installColorsAndFont(JComponent c, String defaultBgName, String defaultFgName, String defaultFontName) { Font f = c.getFont(); if (f == null || f instanceof UIResource) { c.setFont(UIManager.getFont(defaultFontName)); } installColors(c, defaultBgName, defaultFgName); } /** * Convenience method for installing a component's default Border * object on the specified component if either the border is * currently null or already an instance of UIResource. * @param c the target component for installing default border * @param defaultBorderName the key specifying the default border */ public static void installBorder(JComponent c, String defaultBorderName) { Border b = c.getBorder(); if (b == null || b instanceof UIResource) { c.setBorder(UIManager.getBorder(defaultBorderName)); } } /** * Convenience method for un-installing a component's default * border on the specified component if the border is * currently an instance of UIResource. * @param c the target component for uninstalling default border */ public static void uninstallBorder(JComponent c) { if (c.getBorder() instanceof UIResource) { c.setBorder(null); } } /** * @see parseKeyStroke */ private static class ModifierKeyword { final String keyword; final int mask; ModifierKeyword(String keyword, int mask) { this.keyword = keyword; this.mask = mask; } int getModifierMask(String s) { return (s.equals(keyword)) ? mask : 0; } } ; /** * @see parseKeyStroke */ private static ModifierKeyword[] modifierKeywords = { new ModifierKeyword("shift", InputEvent.SHIFT_MASK), new ModifierKeyword("control", InputEvent.CTRL_MASK), new ModifierKeyword("meta", InputEvent.META_MASK), new ModifierKeyword("alt", InputEvent.ALT_MASK), new ModifierKeyword("button1", InputEvent.BUTTON1_MASK), new ModifierKeyword("button2", InputEvent.BUTTON2_MASK), new ModifierKeyword("button3", InputEvent.BUTTON3_MASK) } ; /** * Parse a string with the following syntax and return an * a KeyStroke: * <pre> * "<modifiers>* <key>" * modifiers := shift | control | meta | alt | * button1 | button2 | button3 * key := KeyEvent keycode name, i.e. the name following "VK_". * </pre> * Here are some examples: * <pre> * "INSERT" => new KeyStroke(0, KeyEvent.VK_INSERT); * "control DELETE" => new KeyStroke(InputEvent.CTRL_MASK, * KeyEvent.VK_DELETE); * "alt shift X" => new KeyStroke(InputEvent.CTRL_MASK | InputEvent.SHIFT_MASK, KeyEvent.VK_X); * </pre> */ private static KeyStroke parseKeyStroke(String s) { StringTokenizer st = new StringTokenizer(s); String token; int mask = 0; while((token = st.nextToken()) != null) { int tokenMask = 0; /* if token matches a modifier keyword update mask and continue */ for(int i = 0; (tokenMask == 0) && (i < modifierKeywords.length); i++) { tokenMask = modifierKeywords[i].getModifierMask(token); } if (tokenMask != 0) { mask |= tokenMask; continue; } /* otherwise token is the keycode name less the "VK_" prefix */ String keycodeName = "VK_" + token; int keycode; try { keycode = KeyEvent.class.getField(keycodeName).getInt(KeyEvent.class); } catch (Exception e) { e.printStackTrace(); throw new Error("Unrecognized keycode name: " + keycodeName); } return KeyStroke.getKeyStroke(keycode, mask); } throw new Error("Can't parse KeyStroke: \ "" + s + "\ ""); } /** * Convenience method for building lists of KeyBindings. * <p> * Return an array of KeyBindings, one for each KeyStroke,Action pair * in <b>keyBindingList</b>. A KeyStroke can either be a string in * the format specified by <code>parseKeyStroke</code> or a KeyStroke * object. Actions are strings. Here's an example: * <pre> * JTextComponent.KeyBinding[] multilineBindings = * makeKeyBindings( new Object[] { * "UP", DefaultEditorKit.upAction, * "DOWN", DefaultEditorKit.downAction, * "PAGE_UP", DefaultEditorKit.pageUpAction, * "PAGE_DOWN", DefaultEditorKit.pageDownAction, * "ENTER", DefaultEditorKit.insertBreakAction, * "TAB", DefaultEditorKit.insertTabAction * } ); * * </pre> * @see #parseKeyStroke * @return an array of KeyBindings * @param keyBindingList an array of KeyStroke,Action pairs */ public static JTextComponent.KeyBinding[] makeKeyBindings(Object[] keyBindingList) { JTextComponent.KeyBinding[] rv = new JTextComponent.KeyBinding[keyBindingList.length / 2]; for(int i = 0; i < keyBindingList.length; i += 2) { KeyStroke keystroke = (keyBindingList[i] instanceof KeyStroke) ? (KeyStroke)keyBindingList[i] : parseKeyStroke((String)keyBindingList[i]); String action = (String)keyBindingList[i+1]; rv[i / 2] = new JTextComponent.KeyBinding(keystroke, action); } return rv; } /** * Utility method that creates a UIDefaults.LazyValue that creates * an ImageIcon UIResource for the specified <code>gifFile</code> * filename. */ public static Object makeIcon(final Class baseClass, final String gifFile) { return new UIDefaults.LazyValue() { public Object createValue(UIDefaults table) { byte[] buffer = null; try { /* Copy resource into a byte array. This is * necessary because several browsers consider * Class.getResource a security risk because it * can be used to load additional classes. * Class.getResourceAsStream just returns raw * bytes, which we can convert to an image. */ InputStream resource = baseClass.getResourceAsStream(gifFile); if (resource == null) { System.err.println(baseClass.getName() + "/" + gifFile + " not found."); return null; } BufferedInputStream in = new BufferedInputStream(resource); ByteArrayOutputStream out = new ByteArrayOutputStream(1024); buffer = new byte[1024]; int n; while ((n = in.read(buffer)) > 0) { out.write(buffer, 0, n); } in.close(); out.flush(); buffer = out.toByteArray(); if (buffer.length == 0) { System.err.println("warning: " + gifFile + " is zero-length"); return null; } } catch (IOException ioe) { System.err.println(ioe.toString()); return null; } return new IconUIResource(new ImageIcon(buffer)); } } ; } /** * Return a short string that identifies this look and feel, e.g. * "CDE/Motif". This string should be appropriate for a menu item. * Distinct look and feels should have different names, e.g. * a subclass of MotifLookAndFeel that changes the way a few * components are rendered should be called "CDE/Motif My Way"; * something that would be useful to a user trying to select a * L&F from a list of names. */ public abstract String getName(); /** * Return a string that identifies this look and feel. This string * will be used by applications/services that want to recognize * well known look and feel implementations. Presently * the well known names are "Motif", "Windows", "Mac", "Metal". Note * that a LookAndFeel derived from a well known superclass * that doesn't make any fundamental changes to the look or feel * shouldn't override this method. */ public abstract String getID(); /** * Return a one line description of this look and feel * implementation, e.g. "The CDE/Motif Look and Feel". * This string is intended for the user, e.g. in the title * of a window or in a ToolTip message. */ public abstract String getDescription(); /** * If the underlying platform has a "native" look and feel, and this * is an implementation of it, return true. For example a CDE/Motif * look and implementation would return true when the underlying * platform was Solaris. */ public abstract boolean isNativeLookAndFeel(); /** * Return true if the underlying platform supports and or permits * this look and feel. This method returns false if the look * and feel depends on special resources or legal agreements that * aren't defined for the current platform. * * @see UIManager#setLookAndFeel */ public abstract boolean isSupportedLookAndFeel(); /** * UIManager.setLookAndFeel calls this method before the first * call (and typically the only call) to getDefaults(). Subclasses * should do any one-time setup they need here, rather than * in a static initializer, because look and feel class objects * may be loaded just to discover that isSupportedLookAndFeel() * returns false. * * @see #uninitialize * @see UIManager#setLookAndFeel */ public void initialize() { } /** * UIManager.setLookAndFeel calls this method just before we're * replaced by a new default look and feel. Subclasses may * choose to free up some resources here. * * @see #initialize */ public void uninitialize() { } /** * This method is called once by UIManager.setLookAndFeel to create * the look and feel specific defaults table. Other applications, * for example an application builder, may also call this method. * * @see #initialize * @see #uninitialize * @see UIManager#setLookAndFeel */ public UIDefaults getDefaults() { return null; } public String toString() { return "[" + getDescription() + " - " + getClass().getName() + "]"; } }
The first decision to make when creating a LookAndFeel class is to determine which class to extend. As already pointed out, the class must be a descendant of the LookAndFeel class that can be extended directly. However, a more common approach is to extend the BasicLookAndFeel class in the javax.swing.plaf.basic package. Even if the LookAndFeel class is extended directly, the programming techniques in the BasicLookAndFeel class should be reviewed. This class provides a good example of a class that allows extensions to easily alter only the portions that they need to change. The look-and-feel implementations shipped with the JFC extend the BasicLookAndFeel class, as will be seen in the example in this section.
Regardless of which class your look-and-feel class extends, the informational methods must be provided. These methods are also easy to implement. For most custom look-and-feel implementations, these will be simple one-line methods. These methods for the UnleashedLookAndFeel are shown in Listing 23.6. Look-and-feel implementations that extend a concrete existing look-and-feel such as Metal should not override the getID method unless they make substantial changes to the look-and-feel. The UnleashedLookAndFeel look-and-feel does not fall into this category, so the getID method is overridden to return a unique ID for this look-and-feel. The name and description for the look-and-feel can be any strings that accurately describe the look-and-feel. The name should be unique from the collection of known look-and-feel implementations. Unless you are creating a better look-and-feel for one of the supported platforms, which really wouldn't be that difficult, your look-and-feel will return false from the isNativeLookAndFeel method. Finally, unless you want to restrict the platforms where your look-and-feel can execute, your LookAndFeel class should return true from the isSupportedLookAndFeel method.
Listing 23.6 UNLEASHEDLOOKANDFEEL
Informational Methods
/** * @return The name for this look-and-feel. **/ public String getName() { return "Unleashed"; } /** * We are not a simple extension of an existing * look-and-feel, so provide our own ID. * <p> * @return The ID for this look-and-feel. **/ public String getID() { return "Unleashed"; } /** * @return A short description of this look-and-feel. **/ public String getDescription() { return "The JFC Unleashed Look and Feel"; } /** * This is not a native look and feel on any platform. * <p> * @return false, this isn't native on any platform. **/ public boolean isNativeLookAndFeel() { return false; } /** * This look and feel is supported on all platforms. * <p> * @return true, this L&F is supported on all platforms. **/ public boolean isSupportedLookAndFeel() { return true; }
Recall that the LookAndFeel class defines three functional methods: initialize, uninitialize, and getDefaults. However, these are not abstract methods. Each contains an empty implementation. The initialize method is a hook that allows the look-and-feel to perform any required initialization tasks. This method is called by the UIManager before the getDefaults method is called. The uninitialize method is called by the UIManager before a different look-and-feel replaces this look-and-feel and provides a hook to do any post-processing required by the look-and-feel. If any resources were allocated in the initialize method, they should be released in the uninitialize method. The current look-and-feel implementations that are part of the JFC do not perform any processing in these methods, nor will the Unleashed look-and-feel implementation. Thus these methods are not overridden.
That leaves the getDefaults methods. In this method the look-and-feel must create and initialize the UIDefaults object for the look-and-feel. Recall that the UIDefaults class is an extension of the Hashtable class. It maps component properties to values. The UIDefaults object is fundamental to the customizability features inherent in the JFC. You have seen how application code can change a property to alter the look of a component. This technique should be adhered to when creating your own look-and-feel. Under no circumstances should a developer hard code fonts, colors, or similar properties in a component.
As you saw earlier in this chapter, the standard look-and-feel implementations that are part of the Java platform define many properties for each component. Your look-and-feel implementation should use these properties whenever feasible. It is acceptable to define new properties that are specific to the new look-and-feel implementation. However, a new property that has the same meaning as an existing property should not be defined. Also, an existing property should not be redefined for a different use in your look-and-feel.
The BasicLookAndFeel class further divides the getDefaults method into class, system, and component defaults. This is shown in the getDefaults method contained in the BasicLookAndFeel class shown next. When extending the BasicLookAndFeel class, only the methods called from this method need to be overridden in the subclass.
public UIDefaults getDefaults() { UIDefaults table = new UIDefaults(); initClassDefaults(table); initSystemColorDefaults(table); initComponentDefaults(table); return table; }
The BasicLookAndFeel initClassDefaults Method
The initClassDefaults method registers the uiClassID strings to the class key. You previously saw how this table is used by the UIManager to determine which class to use as the UI object for a component. This method for the BasicLookAndFeel class is shown in Listing 23.7. To create a look-and-feel that provides an implementation for every UI class, an implementation is required for each entry in this table. This gives a graphic example of how much work is required to create a complete look-and-feel.
Listing 23.7 The BASICLOOKANDFEEL Class'
INITCLASSDEFAULTS Method
protected void initClassDefaults(UIDefaults table) { String basicPackageName = "com.sun.java.swing.plaf.basic."; Object[] uiDefaults = { "ButtonUI", basicPackageName + "BasicButtonUI", "CheckBoxUI", basicPackageName + "BasicCheckBoxUI", "ColorChooserUI", basicPackageName + "BasicColorChooserUI", "MenuBarUI", basicPackageName + "BasicMenuBarUI", "MenuUI", basicPackageName + "BasicMenuUI", "MenuItemUI", basicPackageName + "BasicMenuItemUI", "CheckBoxMenuItemUI", basicPackageName + "BasicCheckBoxMenuItemUI", "RadioButtonMenuItemUI", basicPackageName + "BasicRadioButtonMenuItemUI", "RadioButtonUI", basicPackageName + "BasicRadioButtonUI", "ToggleButtonUI", basicPackageName + "BasicToggleButtonUI", "PopupMenuUI", basicPackageName + "BasicPopupMenuUI", "ProgressBarUI", basicPackageName + "BasicProgressBarUI", "ScrollBarUI", basicPackageName + "BasicScrollBarUI", "ScrollPaneUI", basicPackageName + "BasicScrollPaneUI", "SplitPaneUI", basicPackageName + "BasicSplitPaneUI", "SliderUI", basicPackageName + "BasicSliderUI", "SeparatorUI", basicPackageName + "BasicSeparatorUI", "ToolBarSeparatorUI", basicPackageName + "BasicToolBarSeparatorUI", "PopupMenuSeparatorUI", basicPackageName + "BasicPopupMenuSeparatorUI", "TabbedPaneUI", basicPackageName + "BasicTabbedPaneUI", "TextAreaUI", basicPackageName + "BasicTextAreaUI", "TextFieldUI", basicPackageName + "BasicTextFieldUI", "PasswordFieldUI", basicPackageName + "BasicPasswordFieldUI", "TextPaneUI", basicPackageName + "BasicTextPaneUI", "EditorPaneUI", basicPackageName + "BasicEditorPaneUI", "TreeUI", basicPackageName + "BasicTreeUI", "LabelUI", basicPackageName + "BasicLabelUI", "ListUI", basicPackageName + "BasicListUI", "ToolBarUI", basicPackageName + "BasicToolBarUI", "ToolTipUI", basicPackageName + "BasicToolTipUI", "ComboBoxUI", basicPackageName + "BasicComboBoxUI", "TableUI", basicPackageName + "BasicTableUI", "TableHeaderUI", basicPackageName + "BasicTableHeaderUI", "InternalFrameUI", basicPackageName + "BasicInternalFrameUI", "StandardDialogUI", basicPackageName + "BasicStandardDialogUI", "DesktopPaneUI", basicPackageName + "BasicDesktopPaneUI", "DesktopIconUI", basicPackageName + "BasicDesktopIconUI", "OptionPaneUI", basicPackageName + "BasicOptionPaneUI", "PanelUI", basicPackageName + "BasicPanelUI", "ViewportUI", basicPackageName + "BasicViewportUI", } ; table.putDefaults(uiDefaults); }
The BasicLookAndFeel initSystemColorDefaults Method
The initSystemColorDefaults method registers the system color default values. The BasicLookAndFeel class' implementation of this method is shown in Listing 23.8. When extending the BasicLookAndFeel, these colors can be used as is, selectively modified, or totally replaced. To change a color, register a new value with the same key shown in the initSystemColorDefaults method. UI objects that require a system color in your look-and-feel should obtain the color from the UIDefaults object to ensure that, when the value changes, the correct color is used.
Listing 23.8 The BASICLOOKANDFEEL
INITSYSTEMCOLORDEFAULTS Method
protected void initSystemColorDefaults(UIDefaults table) { String[] defaultSystemColors = { /* Color of the desktop background */ "desktop", "#005C5C", /* Color for captions (title bars) when they are active. */ "activeCaption", "#000080", /* Text color for text in captions (title bars). */ "activeCaptionText", "#FFFFFF", /* Border color for caption (title bar) window borders. */ "activeCaptionBorder", "#C0C0C0", /* Color for captions (title bars) when not active. */ "inactiveCaption", "#808080", /* Text color for text in inactive captions (title bars). */ "inactiveCaptionText", "#C0C0C0", /*Border color for inactive caption (title bar) window borders.*/ "inactiveCaptionBorder", "#C0C0C0", /* Default color for the interior of windows */ "window", "#FFFFFF", /* ??? */ "windowBorder", "#000000", /* ??? */ "windowText", "#000000", /* Background color for menus */ "menu", "#C0C0C0", /* Text color for menus */ "menuText", "#000000", /* Text background color */ "text", "#C0C0C0", /* Text foreground color */ "textText", "#000000", /* Text background color when selected */ "textHighlight", "#000080", /* Text color when selected */ "textHighlightText", "#FFFFFF", /* Text color when disabled */ "textInactiveText", "#808080", /* Default color for controls (buttons, sliders, etc) */ "control", "#C0C0C0", /* Default color for text in controls */ "controlText", "#000000", /* ??? */ "controlHighlight", "#C0C0C0", /* Highlight color for controls */ "controlLtHighlight", "#FFFFFF", /* Shadow color for controls */ "controlShadow", "#808080", /* Dark shadow color for controls */ "controlDkShadow", "#000000", /* Scrollbar background (usually the "track") */ "scrollbar", "#E0E0E0", /* ??? */ "info", "#FFFFE1", /* ??? */ "infoText", "#000000" } ; loadSystemColors(table, defaultSystemColors, isNativeLookAndFeel()); }
The BasicLookAndFeel initComponentDefaults Method
The final method in the getDefaults method is the initComponentDefaults method. In this method, the properties for the components contained in the look-and-feel are registered with the UIDefaults object. It is in this method that the properties you modified earlier in this section get defined and are given default values. As with the other default properties, your look-and-feel should not redefine the standard properties defined in this method. For example, it would be a mistake to change the key CheckBox.font to CheckBox.Font to specify the font in a check box in a new look-and-feel. However, your look-and-feel may need to define additional keys to define properties that are new for your look-and-feel implementation. The initComponentDefaults method contained in the BasicLookAndFeel class is shown in Listing 23.9. The size of this method shows the number and depth of properties defined in a look-and-feel. It shows how many properties can be customized without creating a look-and-feel and may give pause to the decision to create a new look-and-feel.
Listing 23.9 The BASICLOOKANDFEEL
INITCOMPONENTDEFAULTS Method
protected void initComponentDefaults(UIDefaults table) { // *** Shared Fonts FontUIResource dialogPlain12 = new FontUIResource("Dialog", Font.PLAIN, 12); FontUIResource serifPlain12 = new FontUIResource("Serif", Font.PLAIN, 12); FontUIResource sansSerifPlain12 = new FontUIResource("SansSerif", Font.PLAIN, 12); FontUIResource monospacedPlain12 = new FontUIResource("Monospaced", Font.PLAIN, 12); FontUIResource dialogBold12 = new FontUIResource("Dialog", Font.BOLD, 12); // *** Shared Colors ColorUIResource red = new ColorUIResource(Color.red); ColorUIResource black = new ColorUIResource(Color.black); ColorUIResource white = new ColorUIResource(Color.white); ColorUIResource yellow = new ColorUIResource(Color.yellow); ColorUIResource gray = new ColorUIResource(Color.gray); ColorUIResource lightGray = new ColorUIResource(Color.lightGray); ColorUIResource darkGray = new ColorUIResource(Color.darkGray); ColorUIResource scrollBarTrack = new ColorUIResource(224, 224, 224); // *** Shared Insets InsetsUIResource zeroInsets = new InsetsUIResource(0,0,0,0); // *** Shared Borders Border zeroBorder = new BorderUIResource.EmptyBorderUIResource(0,0,0,0); Border marginBorder = new BasicBorders.MarginBorder(); Border etchedBorder = BorderUIResource.getEtchedBorderUIResource(); Border loweredBevelBorder = BorderUIResource.getLoweredBevelBorderUIResource(); Border raisedBevelBorder = BorderUIResource.getRaisedBevelBorderUIResource(); Border blackLineBorder = BorderUIResource.getBlackLineBorderUIResource(); Border focusCellHighlightBorder = new BorderUIResource.LineBorderUIResource(yellow); // *** Button value objects Object buttonBorder = new BorderUIResource.CompoundBorderUIResource( new BasicBorders.ButtonBorder( table.getColor("controlShadow"), table.getColor("controlDkShadow"), table.getColor("controlHighlight"), table.getColor("controlLtHighlight")), marginBorder); Object buttonToggleBorder = new BorderUIResource.CompoundBorderUIResource( new BasicBorders.ToggleButtonBorder( table.getColor("controlShadow"), table.getColor("controlDkShadow"), table.getColor("controlHighlight"), table.getColor("controlLtHighlight")), marginBorder); Object radioButtonBorder = new BorderUIResource.CompoundBorderUIResource( new BasicBorders.RadioButtonBorder( table.getColor("controlShadow"), table.getColor("controlDkShadow"), table.getColor("controlHighlight"), table.getColor("controlLtHighlight")), marginBorder); // *** FileChooser / FileView value objects Object newFolderIcon = LookAndFeel.makeIcon(getClass(), "icons/NewFolder.gif"); Object upFolderIcon = LookAndFeel.makeIcon(getClass(), "icons/UpFolder.gif"); Object homeFolderIcon = LookAndFeel.makeIcon(getClass(), "icons/HomeFolder.gif"); Object detailsViewIcon = LookAndFeel.makeIcon(getClass(), "icons/DetailsView.gif"); Object listViewIcon = LookAndFeel.makeIcon(getClass(), "icons/ListView.gif"); Object directoryIcon = LookAndFeel.makeIcon(getClass(), "icons/Directory.gif"); Object fileIcon = LookAndFeel.makeIcon(getClass(), "icons/File.gif"); Object computerIcon = LookAndFeel.makeIcon(getClass(), "icons/Computer.gif"); Object hardDriveIcon = LookAndFeel.makeIcon(getClass(), "icons/HardDrive.gif"); Object floppyDriveIcon = LookAndFeel.makeIcon(getClass(), "icons/FloppyDrive.gif"); // *** InternalFrame value objects Object internalFrameBorder = new UIDefaults.LazyValue() { public Object createValue(UIDefaults table) { return new BorderUIResource.CompoundBorderUIResource( new BevelBorder(BevelBorder.RAISED, table.getColor("controlHighlight"), table.getColor("controlLtHighlight"), table.getColor("controlDkShadow"), table.getColor("controlShadow")), BorderFactory.createLineBorder( table.getColor("control"), 1)); } } ; // *** List value objects Object listCellRendererActiveValue = new UIDefaults.ActiveValue() { public Object createValue(UIDefaults table) { return new DefaultListCellRenderer.UIResource(); } } ; // *** Menus value objects Object menuBarBorder = new BasicBorders.MenuBarBorder( table.getColor("controlShadow"), table.getColor("controlLtHighlight")); Object menuItemCheckIcon = new UIDefaults.LazyValue() { public Object createValue(UIDefaults table) { return BasicIconFactory.getMenuItemCheckIcon(); } } ; Object menuItemArrowIcon = new UIDefaults.LazyValue() { public Object createValue(UIDefaults table) { return BasicIconFactory.getMenuItemArrowIcon(); } } ; Object menuArrowIcon = new UIDefaults.LazyValue() { public Object createValue(UIDefaults table) { return BasicIconFactory.getMenuArrowIcon(); } } ; Object checkBoxIcon = new UIDefaults.LazyValue() { public Object createValue(UIDefaults table) { return BasicIconFactory.getCheckBoxIcon(); } } ; Object radioButtonIcon = new UIDefaults.LazyValue() { public Object createValue(UIDefaults table) { return BasicIconFactory.getRadioButtonIcon(); } } ; Object checkBoxMenuItemIcon = new UIDefaults.LazyValue() { public Object createValue(UIDefaults table) { return BasicIconFactory.getCheckBoxMenuItemIcon(); } } ; Object radioButtonMenuItemIcon = new UIDefaults.LazyValue() { public Object createValue(UIDefaults table) { return BasicIconFactory.getRadioButtonMenuItemIcon(); } } ; // *** OptionPane value objects Object optionPaneMinimumSize = new DimensionUIResource(262, 90); Object optionPaneBorder = new BorderUIResource.EmptyBorderUIResource(10, 10, 12, 10); Object optionPaneButtonAreaBorder = new BorderUIResource.EmptyBorderUIResource(6,0,0,0); // *** ProgessBar value objects Object progressBarBorder = new BorderUIResource.LineBorderUIResource(Color.green, 2); // ** ScrollBar value objects Object minimumThumbSize = new UIDefaults.LazyValue() { public Object createValue(UIDefaults table) { return new DimensionUIResource(8,8); } ; } ; Object maximumThumbSize = new UIDefaults.LazyValue() { public Object createValue(UIDefaults table) { return new DimensionUIResource(4096,4096); } ; } ; // ** Slider value objects Object sliderFocusInsets = new InsetsUIResource( 2, 2, 2, 2 ); Object toolBarSeparatorSize = new DimensionUIResource( 10, 10 ); // *** SplitPane value objects Object splitPaneBorder = new BasicBorders.SplitPaneBorder( table.getColor("controlLtHighlight"), table.getColor("controlDkShadow")); // ** TabbedBane value objects Object tabbedPaneTabInsets = new InsetsUIResource(2, 4, 2, 4); Object tabbedPaneTabPadInsets = new InsetsUIResource(2, 2, 2, 1); Object tabbedPaneTabAreaInsets = new InsetsUIResource(3, 2, 0, 2); Object tabbedPaneContentBorderInsets = new InsetsUIResource(2, 2, 3, 3); // *** Text value objects Object textFieldBorder = new BasicBorders.FieldBorder( table.getColor("controlShadow"), table.getColor("controlDkShadow"), table.getColor("controlHighlight"), table.getColor("controlLtHighlight")); Object editorMargin = new InsetsUIResource(3,3,3,3); JTextComponent.KeyBinding[] fieldBindings = makeKeyBindings( new Object[]{ "ENTER", JTextField.notifyAction } ); JTextComponent.KeyBinding[] multilineBindings = makeKeyBindings( new Object[]{ "UP", DefaultEditorKit.upAction, "DOWN", DefaultEditorKit.downAction, "PAGE_UP", DefaultEditorKit.pageUpAction, "PAGE_DOWN", DefaultEditorKit.pageDownAction, "ENTER", DefaultEditorKit.insertBreakAction, "TAB", DefaultEditorKit.insertTabAction } ); Object caretBlinkRate = new Integer(500); // *** Component Defaults Object[] defaults = { // *** Buttons "Button.font", dialogPlain12, "Button.background", table.get("control"), "Button.foreground", table.get("controlText"), "Button.border", buttonBorder, "Button.margin", new InsetsUIResource(2, 14, 2, 14), "Button.textIconGap", new Integer(4), "Button.textShiftOffset", new Integer(0), "ToggleButton.font", dialogPlain12, "ToggleButton.background", table.get("control"), "ToggleButton.foreground", table.get("controlText"), "ToggleButton.border", buttonToggleBorder, "ToggleButton.margin", new InsetsUIResource(2, 14, 2, 14), "ToggleButton.textIconGap", new Integer(4), "ToggleButton.textShiftOffset", new Integer(0), "RadioButton.font", dialogPlain12, "RadioButton.background", table.get("control"), "RadioButton.foreground", table.get("controlText"), "RadioButton.border", radioButtonBorder, "RadioButton.margin", new InsetsUIResource(2, 2, 2, 2), "RadioButton.textIconGap", new Integer(4), "RadioButton.textShiftOffset", new Integer(0), "RadioButton.icon", radioButtonIcon, "CheckBox.font", dialogPlain12, "CheckBox.background", table.get("control"), "CheckBox.foreground", table.get("controlText"), "CheckBox.border", radioButtonBorder, "CheckBox.margin", new InsetsUIResource(2, 2, 2, 2), "CheckBox.textIconGap", new Integer(4), "CheckBox.textShiftOffset", new Integer(0), "CheckBox.icon", checkBoxIcon, // *** ColorChooser "ColorChooser.font", dialogPlain12, "ColorChooser.background", table.get("control"), "ColorChooser.foreground", table.get("controlText"), "ColorChooser.selectedColorBorder", loweredBevelBorder, // *** ComboBox "ComboBox.font", dialogPlain12, "ComboBox.background", white, "ComboBox.foreground", black, "ComboBox.selectionBackground", table.get("textHighlight"), "ComboBox.selectionForeground", table.get("textHighlightText"), "ComboBox.disabledBackground", table.get("control"), "ComboBox.disabledForeground", table.get("textInactiveText"), // *** FileChooser "FileChooser.acceptAllFileFilterText", new String ("All Files (*.*)"), "FileChooser.cancelButtonText", new String("Cancel"), "FileChooser.saveButtonText", new String("Save"), "FileChooser.openButtonText", new String("Open"), "FileChooser.updateButtonText", new String("Update"), "FileChooser.helpButtonText", new String("Help"), "FileChooser.cancelButtonToolTipText", new String("Abort file chooser dialog."), "FileChooser.saveButtonToolTipText", new String("Save selected file."), "FileChooser.openButtonToolTipText", new String("Open selected file."), "FileChooser.updateButtonToolTipText", new String("Update directory listing."), "FileChooser.helpButtonToolTipText", new String("FileChooser help."), "FileChooser.newFolderIcon", newFolderIcon, "FileChooser.upFolderIcon", upFolderIcon, "FileChooser.homeFolderIcon", homeFolderIcon, "FileChooser.detailsViewIcon", detailsViewIcon, "FileChooser.listViewIcon", listViewIcon, "FileView.directoryIcon", directoryIcon, "FileView.fileIcon", fileIcon, "FileView.computerIcon", computerIcon, "FileView.hardDriveIcon", hardDriveIcon, "FileView.floppyDriveIcon", floppyDriveIcon, // *** InternalFrame "InternalFrame.titleFont", dialogBold12, "InternalFrame.border", internalFrameBorder, "InternalFrame.icon", LookAndFeel.makeIcon(getClass(), "icons/JavaCup.gif"), // Default frame icons are undefined for Basic. "InternalFrame.maximizeIcon", BasicIconFactory.createEmptyFrameIcon(), "InternalFrame.minimizeIcon", BasicIconFactory.createEmptyFrameIcon(), "InternalFrame.iconifyIcon", BasicIconFactory.createEmptyFrameIcon(), "InternalFrame.closeIcon", BasicIconFactory.createEmptyFrameIcon(), "InternalFrame.activeTitleBackground", table.get("activeCaption"), "InternalFrame.activeTitleForeground", table.get("activeCaptionText"), "InternalFrame.inactiveTitleBackground", table.get("inactiveCaption"), "InternalFrame.inactiveTitleForeground", table.get("inactiveCaptionText"), "DesktopIcon.border", internalFrameBorder, "Desktop.background", table.get("desktop"), // *** Label "Label.font", dialogPlain12, "Label.background", table.get("control"), "Label.foreground", table.get("controlText"), "Label.disabledForeground", white, "Label.disabledShadow", table.get("controlShadow"), "Label.border", null, // *** List "List.font", dialogPlain12, "List.background", table.get("window"), "List.foreground", table.get("textText"), "List.selectionBackground", table.get("textHighlight"), "List.selectionForeground", table.get("textHighlightText"), "List.focusCellHighlightBorder", focusCellHighlightBorder, "List.border", null, "List.cellRenderer", listCellRendererActiveValue, // *** Menus "MenuBar.font", dialogPlain12, "MenuBar.background", table.get("menu"), "MenuBar.foreground", table.get("menuText"), "MenuBar.border", menuBarBorder, "MenuItem.font", dialogPlain12, "MenuItem.acceleratorFont", dialogPlain12, "MenuItem.background", table.get("menu"), "MenuItem.foreground", table.get("menuText"), "MenuItem.selectionForeground", table.get("textHighlightText"), "MenuItem.selectionBackground", table.get("textHighlight"), "MenuItem.disabledForeground", null, "MenuItem.acceleratorForeground", table.get("menuText"), "MenuItem.acceleratorSelectionForeground", table.get("textHighlightText"), "MenuItem.border", marginBorder, "MenuItem.borderPainted", Boolean.FALSE, "MenuItem.margin", new InsetsUIResource(2, 2, 2, 2), "MenuItem.checkIcon", menuItemCheckIcon, "MenuItem.arrowIcon", menuItemArrowIcon, "RadioButtonMenuItem.font", dialogPlain12, "RadioButtonMenuItem.acceleratorFont", dialogPlain12, "RadioButtonMenuItem.background", table.get("menu"), "RadioButtonMenuItem.foreground", table.get("menuText"), "RadioButtonMenuItem.selectionForeground", table.get("textHighlightText"), "RadioButtonMenuItem.selectionBackground", table.get("textHighlight"), "RadioButtonMenuItem.disabledForeground", null, "RadioButtonMenuItem.acceleratorForeground", table.get("menuText"), "RadioButtonMenuItem.acceleratorSelectionForeground", table.get("textHighlightText"), "RadioButtonMenuItem.border", marginBorder, "RadioButtonMenuItem.borderPainted", Boolean.FALSE, "RadioButtonMenuItem.margin", new InsetsUIResource(2, 2, 2, 2), "RadioButtonMenuItem.checkIcon", radioButtonMenuItemIcon, "RadioButtonMenuItem.arrowIcon", menuItemArrowIcon, "CheckBoxMenuItem.font", dialogPlain12, "CheckBoxMenuItem.acceleratorFont", dialogPlain12, "CheckBoxMenuItem.background", table.get("menu"), "CheckBoxMenuItem.foreground", table.get("menuText"), "CheckBoxMenuItem.selectionForeground", table.get("textHighlightText"), "CheckBoxMenuItem.selectionBackground", table.get("textHighlight"), "CheckBoxMenuItem.disabledForeground", null, "CheckBoxMenuItem.acceleratorForeground", table.get("menuText"), "CheckBoxMenuItem.acceleratorSelectionForeground", table.get("textHighlightText"), "CheckBoxMenuItem.border", marginBorder, "CheckBoxMenuItem.borderPainted", Boolean.FALSE, "CheckBoxMenuItem.margin", new InsetsUIResource(2, 2, 2, 2), "CheckBoxMenuItem.checkIcon", checkBoxMenuItemIcon, "CheckBoxMenuItem.arrowIcon", menuItemArrowIcon, "Menu.font", dialogPlain12, "Menu.acceleratorFont", dialogPlain12, "Menu.background", table.get("menu"), "Menu.foreground", table.get("menuText"), "Menu.selectionForeground", table.get("textHighlightText"), "Menu.selectionBackground", table.get("textHighlight"), "Menu.disabledForeground", null, "Menu.acceleratorForeground", table.get("menuText"), "Menu.acceleratorSelectionForeground", table.get("textHighlightText"), "Menu.border", marginBorder, "Menu.borderPainted", Boolean.FALSE, "Menu.margin", new InsetsUIResource(2, 2, 2, 2), "Menu.checkIcon", menuItemCheckIcon, "Menu.arrowIcon", menuArrowIcon, "PopupMenu.font", dialogPlain12, "PopupMenu.background", table.get("menu"), "PopupMenu.foreground", table.get("menuText"), "PopupMenu.border", raisedBevelBorder, // *** OptionPane "OptionPane.font", dialogPlain12, "OptionPane.background", table.get("control"), "OptionPane.foreground", table.get("controlText"), "OptionPane.messageForeground", table.get("controlText"), "OptionPane.border", optionPaneBorder, "OptionPane.messageAreaBorder", zeroBorder, "OptionPane.buttonAreaBorder", optionPaneButtonAreaBorder, "OptionPane.minimumSize", optionPaneMinimumSize, "OptionPane.errorIcon", LookAndFeel.makeIcon(getClass(), "icons/Error.gif"), "OptionPane.informationIcon", LookAndFeel.makeIcon(getClass(), "icons/Inform.gif"), "OptionPane.warningIcon", LookAndFeel.makeIcon(getClass(), "icons/Warn.gif"), "OptionPane.questionIcon", LookAndFeel.makeIcon(getClass(), "icons/Question.gif"), // *** Panel "Panel.font", dialogPlain12, "Panel.background", table.get("control"), "Panel.foreground", table.get("textText"), // *** ProgressBar "ProgressBar.font", dialogPlain12, "ProgressBar.foreground", table.get("textHighlight"), "ProgressBar.background", table.get("control"), "ProgressBar.selectionForeground", table.get("control"), "ProgressBar.selectionBackground", table.get("textHighlight"), "ProgressBar.border", progressBarBorder, "ProgressBar.cellLength", new Integer(1), "ProgressBar.cellSpacing", new Integer(0), // *** Separator "Separator.shadow", table.get("controlShadow"), "Separator.highlight", table.get("controlLtHighlight"), // *** ScrollBar/ScrollPane/Viewport "ScrollBar.background", scrollBarTrack, "ScrollBar.foreground", table.get("control"), "ScrollBar.track", table.get("scrollbar"), "ScrollBar.trackHighlight", table.get("controlDkShadow"), "ScrollBar.thumb", table.get("control"), "ScrollBar.thumbHighlight", table.get("controlLtHighlight"), "ScrollBar.thumbDarkShadow", table.get("controlDkShadow"), "ScrollBar.thumbLightShadow", table.get("controlShadow"), "ScrollBar.border", null, "ScrollBar.minimumThumbSize", minimumThumbSize, "ScrollBar.maximumThumbSize", maximumThumbSize, "ScrollPane.font", dialogPlain12, "ScrollPane.background", table.get("control"), "ScrollPane.foreground", table.get("controlText"), "ScrollPane.border", etchedBorder, "ScrollPane.viewportBorder", null, "Viewport.font", dialogPlain12, "Viewport.background", table.get("control"), "Viewport.foreground", table.get("textText"), // *** Slider "Slider.foreground", table.get("control"), "Slider.background", table.get("control"), "Slider.highlight", table.get("controlLtHighlight"), "Slider.shadow", table.get("controlShadow"), "Slider.focus", table.get("controlDkShadow"), "Slider.border", null, "Slider.focusInsets", sliderFocusInsets, // *** SplitPane "SplitPane.background", table.get("control"), "SplitPane.highlight", table.get("controlLtHighlight"), "SplitPane.shadow", table.get("controlShadow"), "SplitPane.border", splitPaneBorder, "SplitPane.dividerSize", new Integer(5), // *** TabbedPane "TabbedPane.font", dialogPlain12, "TabbedPane.background", table.get("control"), "TabbedPane.foreground", table.get("controlText"), "TabbedPane.lightHighlight", table.get("controlLtHighlight"), "TabbedPane.highlight", table.get("controlHighlight"), "TabbedPane.shadow", table.get("controlShadow"), "TabbedPane.darkShadow", table.get("controlDkShadow"), "TabbedPane.focus", table.get("controlText"), "TabbedPane.textIconGap", new Integer(4), "TabbedPane.tabInsets", tabbedPaneTabInsets, "TabbedPane.selectedTabPadInsets", tabbedPaneTabPadInsets, "TabbedPane.tabAreaInsets", tabbedPaneTabAreaInsets, "TabbedPane.contentBorderInsets", tabbedPaneContentBorderInsets, "TabbedPane.tabRunOverlay", new Integer(2), // *** Table "Table.font", dialogPlain12, "Table.foreground", table.get("controlText"), "Table.background", table.get("window"), "Table.selectionForeground", table.get("textHighlightText"), "Table.selectionBackground", table.get("textHighlight"), "Table.gridColor", gray, "Table.focusCellBackground", table.get("window"), "Table.focusCellForeground", table.get("controlText"), "Table.focusCellHighlightBorder", focusCellHighlightBorder, "Table.scrollPaneBorder", loweredBevelBorder, "TableHeader.font", dialogPlain12, "TableHeader.foreground", table.get("controlText"), "TableHeader.background", table.get("control"), "TableHeader.cellBorder", raisedBevelBorder, // *** Text "TextField.font", sansSerifPlain12, "TextField.background", table.get("window"), "TextField.foreground", table.get("textText"), "TextField.inactiveForeground", table.get("textInactiveText"), "TextField.selectionBackground", table.get("textHighlight"), "TextField.selectionForeground", table.get("textHighlightText"), "TextField.caretForeground", table.get("textText"), "TextField.caretBlinkRate", caretBlinkRate, "TextField.border", textFieldBorder, "TextField.margin", zeroInsets, "TextField.keyBindings", fieldBindings, "PasswordField.font", monospacedPlain12, "PasswordField.background", table.get("window"), "PasswordField.foreground", table.get("textText"), "PasswordField.inactiveForeground", table.get("textInactiveText"), "PasswordField.selectionBackground", table.get("textHighlight"), "PasswordField.selectionForeground", table.get("textHighlightText"), "PasswordField.caretForeground", table.get("textText"), "PasswordField.caretBlinkRate", caretBlinkRate, "PasswordField.border", textFieldBorder, "PasswordField.margin", zeroInsets, "PasswordField.keyBindings", fieldBindings, "TextArea.font", monospacedPlain12, "TextArea.background", table.get("window"), "TextArea.foreground", table.get("textText"), "TextArea.inactiveForeground", table.get("textInactiveText"), "TextArea.selectionBackground", table.get("textHighlight"), "TextArea.selectionForeground", table.get("textHighlightText"), "TextArea.caretForeground", table.get("textText"), "TextArea.caretBlinkRate", caretBlinkRate, "TextArea.border", marginBorder, "TextArea.margin", zeroInsets, "TextArea.keyBindings", multilineBindings, "TextPane.font", serifPlain12, "TextPane.background", white, "TextPane.foreground", table.get("textText"), "TextPane.selectionBackground", lightGray, "TextPane.selectionForeground", table.get("textHighlightText"), "TextPane.caretForeground", table.get("textText"), "TextPane.inactiveForeground", table.get("textInactiveText"), "TextPane.border", marginBorder, "TextPane.margin", editorMargin, "TextPane.keyBindings", multilineBindings, "EditorPane.font", serifPlain12, "EditorPane.background", white, "EditorPane.foreground", table.get("textText"), "EditorPane.selectionBackground", lightGray, "EditorPane.selectionForeground", table.get("textHighlightText"), "EditorPane.caretForeground", red, "EditorPane.inactiveForeground", table.get("textInactiveText"), "EditorPane.border", marginBorder, "EditorPane.margin", editorMargin, "EditorPane.keyBindings", multilineBindings, // *** TitledBorder "TitledBorder.font", dialogPlain12, "TitledBorder.titleColor", table.get("controlText"), "TitledBorder.border", etchedBorder, // *** ToolBar "ToolBar.font", dialogPlain12, "ToolBar.background", table.get("control"), "ToolBar.foreground", table.get("controlText"), "ToolBar.dockingBackground", table.get("control"), "ToolBar.dockingForeground", red, "ToolBar.floatingBackground", table.get("control"), "ToolBar.floatingForeground", darkGray, "ToolBar.border", etchedBorder, "ToolBar.separatorSize", toolBarSeparatorSize, // *** ToolTips "ToolTip.font", sansSerifPlain12, "ToolTip.background", table.get("info"), "ToolTip.foreground", table.get("infoText"), "ToolTip.border", blackLineBorder, // *** Tree "Tree.font", dialogPlain12, "Tree.background", table.get("window"), "Tree.foreground", table.get("textText"), "Tree.hash", gray, "Tree.textForeground", table.get("textText"), "Tree.textBackground", table.get("text"), "Tree.selectionForeground", table.get("textHighlightText"), "Tree.selectionBackground", table.get("textHighlight"), "Tree.selectionBorderColor", black, "Tree.editorBorder", blackLineBorder, "Tree.leftChildIndent", new Integer(7), "Tree.rightChildIndent", new Integer(13), "Tree.rowHeight", new Integer(16), "Tree.scrollsOnExpand", Boolean.TRUE, "Tree.openIcon", LookAndFeel.makeIcon(getClass(), "icons/TreeOpen.gif"), "Tree.closedIcon", LookAndFeel.makeIcon(getClass(), "icons/TreeClosed.gif"), "Tree.leafIcon", LookAndFeel.makeIcon(getClass(), "icons/TreeLeaf.gif"), "Tree.expandedIcon", null, "Tree.collapsedIcon", null, } ; table.putDefaults(defaults); }
The UnleashedLookAndFeel initClassDefaults Method
The UnleashedLookAndFeel class does not contain a getDefaults method. Instead, it inherits the method from its parent class, BasicLookAndFeel, and overrides the initClassDefaults and initComponentDefaults methods. It is in the initClassDefaults method that the UI classes comprising the new look-and-feel are registered. Looking at this method, you will quickly notice that the Unleashed look-and-feel does not provide UI classes for every component. When a UI class is not defined in this table, the implementation is inherited from the basic look-and-feel.
In this example, the UI object implementations are only being provided for a couple of classes. However, the technique for adding additional UI classes is the same. This points out a powerful aspect of extending an existing look-and-feel. Your implementation can start with a couple of UI classes and build from there. The look-and-feel is operational with a minimal set of UI classes as the complete set is being developed. This allows you to deploy the partial look-and-feel implementation with example applications that create components by using the completed UI classes and obtain feedback from users. This process can save you from developing a complete look-and-feel that users will not accept.
Listing 23.10 The UNLEASHEDLOOKANDFEEL
INITCLASSDEFAULTS Method
/** * Initialize the uiClassID to UnleashedComponentUI mapping. * <p> * @see #getDefaults */ protected void initClassDefaults( UIDefaults table ) { // // Register the basic class defaults. // super.initClassDefaults( table ); String basicPackageName = "com.foley.laf."; Object[] uiDefaults = { "ButtonUI", basicPackageName + "BasicButtonUI", "TreeUI", basicPackageName + "UnleashedTreeUI", "LabelUI", basicPackageName + "BasicLabelUI", "ToolTipUI", basicPackageName + "UnleashedToolTipUI", } ; table.putDefaults( uiDefaults ); } // initClassDefaults
The UnleashedLookAndFeel initComponentDefaults Method
The initClassDefaults method in the UnleashedLookAndFeel is similar to the initClassDefaults method. The parent class's initComponentDefaults method is used to register the bulk of the component default properties. Then the overridden properties are registered. In the UnleashedLookAndFeel, a UI is provided for the TreeUI key. There are five icons associated with the component that are customized in the UnleashedLookAndFeel. These need to be registered in the UIDefaults table in this method. The UnleashedLookAndFeel class' initComponentDefaults method is shown in Listing 23.11.
Listing 23.11 The UNLEASHEDLOOKANDFEEL
INITCOMPONENTDEFAULTS Method
/** * Register the component defaults. * Let our parent do the real work, then update the defaults * to the icons used in this look-and-feel. * <p> * @param table The UIDefaults table to configure. **/ protected void initComponentDefaults( UIDefaults table ) { // // Register the basic component defaults. // super.initComponentDefaults( table ); Object[] defaults = { "Tree.openIcon", LookAndFeel.makeIcon(getClass(), "images/TreeOpen.gif"), "Tree.closedIcon", LookAndFeel.makeIcon(getClass(), "images/TreeClosed.gif"), "Tree.leafIcon", LookAndFeel.makeIcon(getClass(), "images/TreeLeaf.gif"), "Tree.expandedIcon", LookAndFeel.makeIcon(getClass(), "images/TreeExpanded.gif"), "Tree.collapsedIcon", LookAndFeel.makeIcon(getClass(), "images/TreeCollapsed.gif"), "Tree.textBackground", table.get("window"), } ; table.putDefaults( defaults ); } // initComponentDefaults
That's it! That's all there is to defining a look-and-feel. The Unleashed look-and-feel is a first class look-and-feel that can be used in the same way as any of the stock look-and-feel implementations that are part of the JFC. The complete listing for the UnleashedLookAndFeel is shown in Listing 23.12.
Listing 23.12 The UNLEASHEDLOOKANDFEEL
Class
package com.foley.laf; import java.io.Serializable; import javax.swing.*; import javax.swing.plaf.basic.BasicLookAndFeel; /** * @author MikeFoley */ public class UnleashedLookAndFeel extends BasicLookAndFeel implements Serializable { /** * @return The name for this look-and-feel. **/ public String getName() { return "Unleashed"; } /** * We are not a simple extension of an existing * look-and-feel, so provide our own ID. * <p> * @return The ID for this look-and-feel. **/ public String getID() { return "Unleashed"; } /** * @return A short description of this look-and-feel. **/ public String getDescription() { return "The JFC Unleashed Look and Feel"; } /** * This is not a native look and feel on any platform. * <p> * @return false, this isn't native on any platform. **/ public boolean isNativeLookAndFeel() { return false; } /** * This look and feel is supported on all platforms. * <p> * @return true, this L&F is supported on all platforms. **/ public boolean isSupportedLookAndFeel() { return true; } /** * Initialize the uiClassID to UnleashedComponentUI mapping. * <p> * @see #getDefaults */ protected void initClassDefaults( UIDefaults table ) { // // Register the basic class defaults. // super.initClassDefaults( table ); String basicPackageName = "com.foley.plaf.unleashed."; Object[] uiDefaults = { "ButtonUI", basicPackageName + "BasicButtonUI", "TreeUI", basicPackageName + "UnleashedTreeUI", "LabelUI", basicPackageName + "BasicLabelUI", "ToolTipUI", basicPackageName + "UnleashedToolTipUI", } ; table.putDefaults( uiDefaults ); } // initClassDefaults /** * Register the component defaults. * Let our parent do the real work, then update the defaults * to the icons used in this look-and-feel. * <p> * @param table The UIDefaults table to configure. **/ protected void initComponentDefaults( UIDefaults table ) { // // Register the basic component defaults. // super.initComponentDefaults( table ); Object[] defaults = { "Tree.openIcon", LookAndFeel.makeIcon(getClass(), "images/TreeOpen.gif"), "Tree.closedIcon", LookAndFeel.makeIcon(getClass(), "images/TreeClosed.gif"), "Tree.leafIcon", LookAndFeel.makeIcon(getClass(), "images/TreeLeaf.gif"), "Tree.expandedIcon", LookAndFeel.makeIcon(getClass(), "images/TreeExpanded.gif"), "Tree.collapsedIcon", LookAndFeel.makeIcon(getClass(), "images/TreeCollapsed.gif"), "Tree.textBackground", table.get("window"), } ; table.putDefaults( defaults ); } // initComponentDefaults } // UnleashedLookAndFeel
In the previous section the Unleashed look-and-feel was developed. Unleashed is an extension of the basic look-and-feel that adds a couple of UI classes. It can be registered and used just like any of the look-and-feels that are part of the JFC. The TestLookAndFeel application shown in Listing 23.13 registers the Unleashed look-and-feel, sets it as the current look-and-feel, and then creates a frame and components that have a UI class defined in this look-and-feel. This is a slightly modified version of the TreeTest application presented in Chapter 11. In the main method the UIManager's installLookAndFeel is called to install the Unleashed look-and-feel. The two parameters to this method are the look-and-feel's name and the fully qualified name of the class that defines the look-and-feel. Then the setLookAndFeel method is called to specify the Unleashed look-and-feel for the application. The single parameter to this method is the fully qualified class name of the Unleashed look-and-feel. There is also a version of this method that takes a reference to the LookAndFeel class that defines the look-and-feel. The resulting window is shown in Figure 23.6. In the figure, you can see that the UnleashedLabelUI implementation allows for multiple lines in a single label.
Listing 23.13 The TESTLOOKANDFEEL
Application
package com.foley.test; import java.awt.*; import java.awt.event.*; import java.util.*; import javax.swing.*; import javax.swing.event.*; import javax.swing.tree.*; import javax.swing.border.*; import com.foley.utility.*; /** * An application that registers the Unleashed * look-and-feel and displays a frame containing * some of the Swing components that have their * UI Object modified with this look-and-feel. * * @author Mike Foley **/ public class LookAndFeelTest implements ActionListener, TreeSelectionListener { /** * The tree used in the center of the dipslay. **/ JTree tree; /** * The component where the selected path is displayed. **/ JTextArea textArea; /** * The text contained in the JTextArea initially and after a clear. **/ private static final String INITIAL_TEXT = "Selected Path Events\ n"; /** * Create the tree test component. * The display is a tree in the center of a BorderLayout. * The EAST region contains a JTextArea where selected * paths are displayed. * The bottom region contains buttons. * <p> * @return The component containing the tree. **/ public JComponent createTreePanel() { JPanel treePanel = new JPanel(); treePanel.setLayout( new BorderLayout() ); JLabel title = new JLabel( "Tree Event Viewer\ nSelect an item in the tree." ); title.setHorizontalAlignment( SwingConstants.CENTER ); treePanel.add( title, BorderLayout.NORTH ); JSplitPane splitPane = new JSplitPane(); splitPane.setOneTouchExpandable( true ); treePanel.add( splitPane, BorderLayout.CENTER ); tree = new JTree( createTreeModel() ); tree.setEditable( true ); tree.setCellEditor( new TreeLeafEditor( tree ) ); tree.addTreeSelectionListener( this ); tree.setBorder( BorderFactory.createLoweredBevelBorder() ); splitPane.setLeftComponent( tree ); JPanel buttonPanel = new JPanel(); JButton expand = new JButton( "Expand Selected" ); expand.addActionListener( this ); expand.setToolTipText( "This button will expand\ n" + "the selected node in the tree." ); JButton clear = new JButton( "Clear Path Display" ); clear.addActionListener( new ActionListener() { public void actionPerformed( ActionEvent event ) { textArea.setText( INITIAL_TEXT ); } } ); clear.setToolTipText( "This button will clear\ n" + "the text in the event log." ); buttonPanel.add( expand ); buttonPanel.add( clear ); treePanel.add( buttonPanel, BorderLayout.SOUTH ); textArea = new JTextArea( INITIAL_TEXT, 16, 50 ); textArea.setBackground( expand.getBackground() ); textArea.setBorder( null ); textArea.setBorder( BorderFactory.createEmptyBorder( 4, 4, 4, 4 ) ); splitPane.setRightComponent( new JScrollPane( textArea ) ); treePanel.setBorder( BorderFactory.createLoweredBevelBorder() ); return( treePanel ); } /** * Create the data model used in the tree contained * in the application. * <p> * @return The data model for the tree. **/ protected TreeModel createTreeModel() { DefaultMutableTreeNode root = new DefaultMutableTreeNode( "Music Collection" ); DefaultMutableTreeNode albums = new DefaultMutableTreeNode( "Albums" ); DefaultMutableTreeNode cds = new DefaultMutableTreeNode( "CDs" ); DefaultMutableTreeNode tapes = new DefaultMutableTreeNode( "Tapes" ); root.add( albums ); root.add( cds ); root.add( tapes ); DefaultMutableTreeNode stonesAlbums = new DefaultMutableTreeNode( "Rolling Stones" ); albums.add( stonesAlbums ); stonesAlbums.add( new DefaultMutableTreeNode( "Hot Rocks" ) ); stonesAlbums.add( new DefaultMutableTreeNode( "Black and Blue" ) ); stonesAlbums.add( new DefaultMutableTreeNode( "Sticky Finger" ) ); DefaultMutableTreeNode c1 = new DefaultMutableTreeNode( "Classical" ); DefaultMutableTreeNode c2 = new DefaultMutableTreeNode( "Best Rock of the 60's" ); DefaultMutableTreeNode c3 = new DefaultMutableTreeNode( "70's Disco Favorites" ); DefaultMutableTreeNode c4 = new DefaultMutableTreeNode( "Broadway Hits" ); DefaultMutableTreeNode c5 = new DefaultMutableTreeNode( "Country's Best?" ); cds.add( c1 ); cds.add( c2 ); cds.add( c3 ); cds.add( c4 ); cds.add( c5 ); DefaultMutableTreeNode s1 = new DefaultMutableTreeNode( "Rolling Stones" ); DefaultMutableTreeNode s2 = new DefaultMutableTreeNode( "Beatles" ); DefaultMutableTreeNode s3 = new DefaultMutableTreeNode( "The Who" ); c1.add( new DefaultMutableTreeNode( "Beethoven's Fifth" ) ); c2.add( s1 ); c2.add( s2 ); c2.add( s3 ); s1.add( new DefaultMutableTreeNode( "Gimmie Shelter" ) ); s1.add( new DefaultMutableTreeNode( "Some Girls" ) ); s1.add( new DefaultMutableTreeNode( "Emotional Rescue" ) ); s2.add( new DefaultMutableTreeNode( "White Album" ) ); s2.add( new DefaultMutableTreeNode( "Abby Road" ) ); s2.add( new DefaultMutableTreeNode( "Let it be" ) ); s3.add( new DefaultMutableTreeNode( "Tommy" ) ); s3.add( new DefaultMutableTreeNode( "The Who" ) ); c3.add( new DefaultMutableTreeNode( "Saturday Night Fever" ) ); c3.add( new DefaultMutableTreeNode( "Earth Wind and Fire" ) ); c4.add( new DefaultMutableTreeNode( "Cats Soundtrack" ) ); c5.add( new DefaultMutableTreeNode( "Unknown" ) ); return( new DefaultTreeModel( root ) ); } // createTreeModel /** * actionEvent, from ActionListener. * <p> * The expand button was pressed. Expand the selected paths * in the tree. * <p> * @param event The actionEvent causing this method call. **/ public void actionPerformed( ActionEvent event ) { TreePath[] paths = tree.getSelectionPaths(); if( paths != null ) { for( int i = 0; i < paths.length; i++ ) { expandPath( paths[i] ); } } } /** * valueChanged, from TreeSelectionListener. * <p> * The selected state in the tree changed. Show the * selected path in the text area. * <p> * @param event The event causing this method to be called. **/ public void valueChanged( TreeSelectionEvent event ) { TreePath[] paths = event.getPaths(); for( int i = 0; i < paths.length; i++ ) { // // Diplay the path and state. // Object[] path = paths[i].getPath(); textArea.append( event.isAddedPath( paths[i] ) ? "ADDED: " : "REMOVED: " ); textArea.append( paths[i] + "\ n" ); for( int j = 0; j < path.length; j++ ) { textArea.append( "\ t" + path[j] + "\ n" ); } if( event.isAddedPath( paths[i] ) ) { Object pathObject = paths[i].getLastPathComponent(); // // This test is unfortunate. The MutableTreeNode // interface does not have a getUserObject method, so // we must test for a specific implementation. // if( pathObject instanceof DefaultMutableTreeNode ) { Object userObject = ( ( DefaultMutableTreeNode ) pathObject ).getUserObject(); textArea.append( "User Object: " + userObject + "\ n" ); } } } textArea.append( "-------------------------------------\ n" ); } /** * Expand the node at the end of the given path. * This requires expanding all children node of the * path recursively until the entire subtree has been * expanded. **/ private void expandPath( TreePath path ) { Object o = path.getLastPathComponent(); if( o instanceof TreeNode ) { for( Enumeration e = ( ( TreeNode )o ).children(); e.hasMoreElements(); ) { TreeNode node = ( TreeNode )e.nextElement(); expandChildren( node ); } } } /** * Expand all the children of the given node. * <p> * @param expandNode The root of the subtree to expand. **/ private void expandChildren( TreeNode expandNode ) { if( expandNode.isLeaf() ) { Stack nodes = new Stack(); // // Push the parents of the current node // until the root of the tree is reached. // TreeNode node = expandNode; nodes.push( node ); while( ( node = node.getParent() ) != null ) nodes.push( node ); // // Create a path to the node passed into this // method by popping each element from the stack. // TreeNode[] path = new TreeNode[ nodes.size() ]; for( int i = 0; i < path.length; i++ ) path[i] = ( TreeNode )nodes.pop(); TreePath treePath = new TreePath( path ); // // Expand the path. // tree.expandPath( treePath ); } else { // // Expand the children children of the // node passed to the method. // for( Enumeration e = expandNode.children(); e.hasMoreElements(); ) { expandChildren( ( DefaultMutableTreeNode ) e.nextElement() ); } } // else } // expandChildren /** * Application entry point. * Create the frame, and place a tree into it. * * @param args Command line parameter. Not used. **/ public static void main( String args[] ) { // // Set the look-and-feel to Unleashed. // try { String className = "com.foley.plaf.unleashed.UnleashedLookAndFeel"; UIManager.installLookAndFeel( "Unleashed", className ); UIManager.setLookAndFeel( className ); } catch( Exception ex ) { System.err.println( "Could not load look-and-feel" ); System.err.println( ex ); ex.printStackTrace(); } LookAndFeelTest treeTest = new LookAndFeelTest(); JFrame frame = new ApplicationFrame( "Tree Test" ); frame.getContentPane().add( treeTest.createTreePanel(), BorderLayout.CENTER ); frame.pack(); frame.setVisible( true ); } // main } // LookAndFeelTest
Figure 23.6 : The LookAndFeelTest application.
Up to this point, your focus has been on visual look-and-feels. However, the multiplexing look-and-feel allows multiple look-and-feels to be simultaneously active. However, only one of the look-and-feel implementations is visual. The others are known as auxiliary look-and-feels. The auxiliary look-and-feels can be used to interact with other devices such as sound cards or Braille readers.
NOTE |
Notice that the portion of the code in the LookAndFeelTest application that creates and configures the user interface is identical to applications presented earlier in this book. The fact that the look-and-feel is different has no effect on the code that creates and configures the user interface. This is a fundamental concept of the pluggable look-and-feel architecture. Indeed, it is its biggest advantage. |
NOTE |
In the LookAndFeelTest application shown in Listing 23.13, the UnleashedLookAndFeel is specified in the main method as the look-and-feel for this application. The default look-and-feel for a Java installation can be specified by setting the swing.defaultlaf property in the swing.properties file. The UnleashedLookAndFeel could be installed as the default look-and-feel for every Java application by adding the following line to the swing.properties file: swing.defaultlaf=com.foley.plaf.unleashed.UnleashedLookAndFeel |
If auxiliary look-and-feels are installed in the virtual machine, a multiplexing UI object is installed when a component's UI object is created. The multiplexing UI object passes a UI message to each look-and-feel registered. The visual look-and-feel is used without modification. In fact, each look-and-feel is unaware that the multiplexing is occurring.
The multiplexing UI object is guaranteed to contain the visual UI object as the first item in its multiplexed UI object array. This array is obtained by calling the getUIs method of the multiplexing UI object. This method is available in all classes in the swing.plaf.multi package.
The intent of auxiliary look-and-feels is to augment the primary visual look-and-feel used by the application. For this reason, they tend to be nonvisual. However, there is nothing in the architecture that prevents an auxiliary look-and-feel from rendering to the display. This is both a good and a bad thing. This gives the developer of the auxiliary look-and-feel complete flexibility but also adds responsibilities. For example, the auxiliary look-and-feel should not compete with the default look-and-feel to paint a component.
When creating an auxiliary look-and-feel, you probably will not extend a visual look-and-feel. This is to avoid painting or processing input events that conflict with the default look-and-feel when your auxiliary look-and-feel is used. Instead, you can choose to extend the classes in the swing.plaf package or extend the swing.LookAndFeel class directly. Even when classes in the swing.plaf package are extended, care must be taken to override any methods that render to the screen. For example, the update method in the ComponentUI class clears the component's background if it is opaque. This behavior is undesirable in most auxililiary look-and-feels, because it will compete with the update method from the default visual look-and-feel being used for the application.
Before the multiplexing look-and-feel will be used, one or more auxiliary look-and-feel implementations must be registered with the Virtual Machine. This is done by adding entries to the swing.auxiliarylaf property in the swing.properties file or calling the addAuxiliaryLookandFeel method in the UIManager. The property file is located in the $JDKHOME/lib directory. Multiple auxiliary look-and-feel implementations can be specified for this property by separating each look-and-feel with a comma. Each property entry is the fully qualified class name of the look-and-feel class. The following line added to the swing.properties file would enable two auxiliary look-and-feel implementations in the Virtual Machine:
Swing.auxiliarylaf=com.foley.plaf.debug.DebugLookAndFeel, com.foley.plaf.audio.AudioLookAndFeel
After auxiliary look-and-feel implementations are registered in the Virtual Machine, the multiplexing look-and-feel will be used without further programmer or user intervention when Swing components are created. The registered auxiliary look-and-feel implementations may be queried from the UIManager by calling the getAuxiliaryLookAndFeels method. Finally, an auxiliary look-and-feel may be removed from the Virtual Machine by passing the removeAuxiliaryLookAndFeel method the LookAndFeel instance to be removed.
NOTE |
The swing.properties file is only read at application startup. Changes in this file will not be used until the next time the Virtual Machine is started. |
A tremendous amount of material was presented in this chapter. The flexibility of the pluggable look-and-feel architecture that the Swing components are built on was explored. You saw that a "J" component is not a single object. Instead, it is two: the component that defines the API that the application programs to and the user interface object that renders the component on the display and interacts with the user. The separation of component class and UI object allows the UI object to be changed without affecting application code. This is the major strength of the pluggable look-and-feel architecture.
This chapter presented many options for configuring a look-and-feel. The look-and-feel implementations that are part of the JFC define a multitude of properties that define the characteristics of the look-and-feel. These properties include such items as colors, fonts, icons, and borders. An application can modify any or all of these properties to achieve an enormous amount of customization. The Java look-and-feel contains a theme mechanism. Changing the theme provides an easy alternative to changing properties for colors and fonts in the UIManager. A theme can be switched dynamically while the application is executing. Building a theme editor will give the user the ability to alter the colors in a theme while the application is executing. Alternatively, a theme can be read from a property file to allow the user to edit the theme file to easily configure the application.
Customizing the feel of an application isn't as easy as customizing its look. To customize the feel of a component, its UI object must be replaced with a customized version. The hooks in the basic UI implementations that facilitate this process were explored. A UI class that eliminates the double-click gesture from expanding or collapsing a node in a tree instance was presented.
If customizing an existing look-and-feel doesn't meet your requirements, an entirely new look-and-feel can be created and installed. In this chapter, the Unleashed look-and-feel was presented. This look-and-feel implementation, like the standard JFC look-and-feel implementations, extends the BasicLookAndFeel class. The Unleashed look-and-feel only provides UI classes for a few components. The other UI classes are those in the basic package. Adding additional UI classes and registering them in the UnleashedLook AndFeel class can expand the Unleashed look-and-feel. To develop a look-and-feel that provides a UI class for every component is a large undertaking. The ability to use existing implementations and adding classes as they are completed gives the developer a clear path for completing a look-and-feel. Also, the completed UI classes can be tested and reviewed as further classes are being developed. This is a nice way to develop a complete look-and-feel.
Auxiliary look-and-feels can be used to interface to devices such as sound cards and Braille readers. When an auxiliary look-and-feel is registered with the system, the JFC uses the multiplexing look-and-feel. When a UI object is created for a component, an instance of the appropriate multiplexing look-and-feel is created. This UI object manages the visual and auxiliary UI objects. When a UI method is to be called, the multiplexing UI object calls the corresponding methods in each look-and-feel. There should only be one visual look-and-feel being used at any given time.
© Copyright, Macmillan Computer Publishing. All rights reserved.