- Getting Started with Java CSS
- Java CSS Fundamentals
- Extending Java CSS with a New Property Type
- Conclusion
Java CSS Fundamentals
Before you can use Java CSS effectively, you need to be acquainted with various fundamentals. In this section, we'll review some CSS terminology in the context of Figure 1. Then you'll learn how to work with a pair of API classes to perform styling. After discovering the usefulness of global style sheets, we'll take an in-depth tour of Java CSS support for selectors and declarations.
CSS Terminology by Example
Java CSS follows the CSS convention for specifying styling information. According to this convention, a style sheet is a collection of rules, a rule is a selector and its associated brace-delimited list of declarations, a selector is a string that identifies the user interface component(s) to which corresponding declarations apply, and a declaration is a component property and its associated value.
Figure 1 reveals four rules. In the first rule, #title is the selector and font: Helvetica-BOLD-24 is the declaration. Java CSS interprets this rule as follows: Set the font property to a 24-point bold Helvetica font for all Swing components whose ID is set to title (via java.awt.Component's setName() method). Only the Tip Calculator label has its ID set to title.
In the second rule, .bold is the selector and font-weight: bold is the declaration. Java CSS sets the font-weight property to bold for all Swing components whose style class is set to bold (via javax.swing.JComponent's putClientProperty() method, whose key parameter is assigned "styleClass"). Only the Total: label belongs to the bold style class.
The third rule specifies JSlider#tip as the selector and specifies six declarations. For the JSlider with the tip ID, Java CSS sets paintTicks to true (show tick marks), majorTickSpacing to 10 and minorTickSpacing to 5 (specify major and minor tick mark spacing), paintLabels to true (show labels), background to null (no background color), and opaque to true (hide background pixels).
Finally, the fourth rule specifies JSlider#tip:{value <= 10} as the selector and background: red !over 0.3s as the declaration. For the JSlider identified as tip, Java CSS animates its background property from null to red over a 0.3-second duration whenever the slider's value property is less than or equal to 10, and animates the background property from red to null whenever value exceeds 10.
CSSParser and StyleSheet
In Java CSS, more than 50 interfaces, classes, and enumerations are organized into packages com.sun.stylesheet, com.sun.stylesheet.css, com.sun.stylesheet.styleable, and com.sun.stylesheet.types. Classes com.sun.stylesheet.css.CSSParser and com.sun.stylesheet.Stylesheet serve as the API's entry point:
- CSSParser provides six methods for parsing and re-parsing a style sheet. These methods either return a Stylesheet object (parsing) or take an existing Stylesheet object as an argument (re-parsing). Each method throws a com.sun.stylesheet.css.ParseException when parsing errors are encountered. Two of the methods that obtain a style sheet from a URL throw an IOException if a network problem occurs, and one of the URL-parsing methods also throws an UnsupportedEncodingException if its encoding argument isn't supported.
- Stylesheet describes a style sheet that can apply properties to a tree of objects. Basically, a Stylesheet object is a collection of com.sun.stylesheet.Rule objects, with each Rule object describing a specific rule in terms of one or more com.sun.stylesheet.Selectors and com.sun.stylesheet.Declarations. (I'll talk about multiple selectors for a single rule later.) For each Rule, the tree is scanned and each object is checked against the Selector(s) to see whether the Rule applies. If so, the Rule applies Declaration property values to the object's properties.
After parsing a style sheet and returning a Stylesheet object, an application typically applies the style sheet's rules to its tree of component objects by invoking Stylesheet's public void applyTo(Object root) method, where root is often a reference to the application's frame window. Subsequent changes to a style sheet can be reapplied to this tree via Stylesheet's public void reapply() method.
Listing 1 demonstrates using CSSParser's public static Stylesheet parse(String stylesheetText) method to parse a String-based style sheet, and demonstrates using applyTo() to apply the parsed style sheet's properties to a tree of five buttons rooted in their frame window container.
Listing 1FiveButtons.java.
// FiveButtons.java import java.awt.EventQueue; import java.awt.FlowLayout; import javax.swing.JButton; import javax.swing.JFrame; import com.sun.stylesheet.Stylesheet; import com.sun.stylesheet.css.CSSParser; public class FiveButtons extends JFrame { public FiveButtons () { super ("Five Buttons"); setDefaultCloseOperation (EXIT_ON_CLOSE); getContentPane ().setLayout (new FlowLayout ()); JButton btn1 = new JButton ("First"); getContentPane ().add (btn1); JButton btn2 = new JButton ("Second"); btn2.setName ("second"); getContentPane ().add (btn2); JButton btn3 = new JButton ("Third"); btn3.putClientProperty ("styleClass", "italic"); getContentPane ().add (btn3); JButton btn4 = new JButton ("Fourth"); btn4.putClientProperty ("styleClass", "italic"); getContentPane ().add (btn4); JButton btn5 = new JButton ("Fifth"); getContentPane ().add (btn5); pack (); setVisible (true); } public static void main (String [] args) { Runnable r; r = new Runnable () { public void run () { String styling = "JButton { foreground: red }"+ "JButton#second { font-size: 20pt; foreground: blue }"+ "JButton.italic { font-style: italic; foreground: #008000 }"; Stylesheet stylesheet = CSSParser.parse (styling); FiveButtons fb = new FiveButtons (); stylesheet.applyTo (fb); } }; EventQueue.invokeLater (r); } }
These buttons are styled via a String-based style sheet that assigns red text to buttons that are unnamed and that belong to no class. Further, the button with an ID matching second (only one component should match an ID; otherwise use a style class) presents blue 20-point text, and all buttons belonging to the italic class present dark green italic text. Figure 2 shows the resulting styled user interface.
Figure 2 Styling a handful of buttons.
Global Style Sheets
The previous approach to styling a user interface doesn't work where option panes and other dialog boxes are involved. To style dialog boxes, you need to take advantage of the Java CSS global style sheet feature, which lets you install a style sheet whose settings apply to all newly opened windows, without affecting existing windows.
We invoke Stylesheet's public static void setGlobalStylesheet(Stylesheet stylesheet) method to install stylesheet as the global style sheet. If a global style sheet is already present, it's removed, and any of its currently styled windows are updated to match the new style sheet. The companion public static Stylesheet getGlobalStylesheet() method returns the current global style sheet.
To demonstrate the global style sheet's usefulness, I've created an application that presents a user interface consisting of a text field and a button. If the text field doesn't contain an integer when the button is clicked, a NumberFormatException is thrown. Its handler installs a global style sheet prior to presenting an option pane, to style the pane's button. Listing 2 presents the application's source code.
Listing 2GSDemo.java.
// GSDemo.java import java.awt.EventQueue; import java.awt.FlowLayout; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import javax.swing.JButton; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JOptionPane; import javax.swing.JTextField; import com.sun.stylesheet.Stylesheet; import com.sun.stylesheet.css.CSSParser; public class GSDemo extends JFrame { static Stylesheet globalStylesheet; public GSDemo () { super ("Global Style Sheet Demo"); setDefaultCloseOperation (EXIT_ON_CLOSE); getContentPane ().setLayout (new FlowLayout ()); getContentPane ().add (new JLabel ("Age")); final JTextField txtAge = new JTextField (20); getContentPane ().add (txtAge); JButton btnValidate = new JButton ("Validate"); ActionListener al; al = new ActionListener () { public void actionPerformed (ActionEvent ae) { try { Integer.parseInt (txtAge.getText ()); } catch (NumberFormatException nfe) { Stylesheet.setGlobalStylesheet (globalStylesheet); JOptionPane.showMessageDialog (GSDemo.this, "integer expected"); } } }; btnValidate.addActionListener (al); getContentPane ().add (btnValidate); } public static void main (String [] args) { Runnable r; r = new Runnable () { public void run () { String globalStyling = "JButton { font-size: 16pt; foreground: red; "+ "text-decoration: underline }"; globalStylesheet = CSSParser.parse (globalStyling); String styling = "JButton { font-size: 16pt; foreground: blue; "+ "text-decoration: underline }"; Stylesheet stylesheet = CSSParser.parse (styling); GSDemo gsdemo = new GSDemo (); stylesheet.applyTo (gsdemo); gsdemo.pack (); gsdemo.setVisible (true); } }; EventQueue.invokeLater (r); } }
After creating global and non-global style sheets, the application invokes GSDemo() to create the user interface and applies the non-global style sheet to this interface. In this example, it's necessary to apply the style sheet prior to packing and displaying the user interface, to ensure that Swing takes the styling information into account when sizing the frame window and laying out its components.
Prior to invoking JOptionPane.showMessageDialog() in response to the thrown exception, the application executes Stylesheet.setGlobalStylesheet (globalStylesheet); to install the global style sheet. As Figure 3 shows, this method call allows the option pane's button text to be colored red, whereas the frame window's button text is colored blue.
Figure 3 You must use a global style sheet to style an option pane.
Perhaps you're wondering why I placed Stylesheet.setGlobalStylesheet (globalStylesheet); in the exception handler, instead of placing it after gsdemo.setVisible (true); in main()'s run() method. If I had done that, you'd notice the frame window's button text colored red instead of blue. This seems to contradict what I said earlier about the global style sheet not affecting existing windows.
Behind the scenes, however, setGlobalStylesheet() registers a window listener (if not registered) to listen for window-opening events. When this event occurs, the listener applies global style sheet styles to the window. Because the window-opening event will not occur until run() exits, placing setGlobalStylesheet() in run() allows the registered window listener to intercept this event, and color the button text red.
More About Selectors
Each style sheet rule begins with a selector, a string-based expression that identifies the Swing (or Abstract Window Toolkit) component(s) to which the corresponding declarations apply. Java CSS recognizes four basic kinds of selectors, which can be combined into more complex selectors:
- Java class: The name of a Java class without package information. All instances of classes matching this name and all instances of subclasses are selected. When specified, this name must appear at the beginning of the selector. Alternatively, an asterisk may appear in place of the Java class name. If present, this wildcard matches all Java classes.
- ID: An object's identifier. For objects whose classes descend from java.awt.Component, the ID is specified via Component's public void setName(String name) method and returned via its public String getName() method. The ID follows the Java class name (if present) in the selector and is prefixed with a hash character (#).
- Style class: A Swing component's style class name (bold, for example). For objects whose classes descend from javax.swing.JComponent, the style class is specified via JComponent's public final void putClientProperty(Object key, Object value) method and returned via its public final Object getClientProperty(Object key) method. In either case, key is set to "styleClass" (although it's preferable to specify Stylesheet.STYLE_CLASS_KEY). All components with matching style classes are selected. The style class follows any Java class name and ID in the selector, and is prefixed with a period (.).
- Pseudoclass: The category to which a component belongs at certain times, based on the component's current state. A pseudoclass appears at the end of a selector and is prefixed with a colon (:) character. Pseudoclasses are inherently dynamic, and their values may be applied to and removed from the component many times during the course of execution.
- mouseoverapplies when the mouse is over the component.
- mouseoutapplies when the mouse is outside of the component.
- mousedownapplies when the mouse is pressed on the component.
- mouseupapplies when the mouse is not pressed on the component.
- focusedapplies when the component has focus.
- unfocusedapplies when the component doesn't have focus.
- armedapplies when an AbstractButton is armed.
- unarmedapplies when an AbstractButton isn't armed.
- activeapplies when a Window is active.
- inactiveapplies when a Window is inactive.
- selectedapplies when the component's isSelected() method returns true. This pseudoclass is shorthand for the ${selected} programmatic pseudoclass.
- unselectedapplies when the component's isSelected() method returns false. This pseudoclass is shorthand for the ${!selected} programmatic pseudoclass.
- programmaticany Boolean-valued Beans Binding expression can be used as a pseudoclass. For example, JTextField:{text == ""} { background: red } tells Java CSS to color a text field's background red when it contains no text (as a reminder to the user that a value must be entered into the text field). Behind the scenes, text == "" is treated as getText ().equals ("").
The following pseudoclasses are available:
For example, JButton and * are Java class selectors, #title is an ID selector, .bold is a style class selector, and JTextField.focused is a combined Java class and pseudoclass selector. In addition to supporting these and other simple selectors, Java CSS lets you combine simple selectors into compound selectors, as follows:
- E Fany object matched by F that is a descendent of an object matched by E. For example, Window:mouseover JButton { foreground: #008000 } changes the foreground color of all JButtons to dark green whenever the mouse moves over the window (but not over a button or other child component) containing these buttons.
- E > Fany object matched by F that is the child of an object matched by E. For example, JPanel#panel > JButton { foreground: red } changes the foreground color to red for all JButtons that are added to the JPanel named panel (but not added to containers that are added to this panel).
Finally, multiple selectors can be specified for a single rule, provided that each selector is separated from its predecessor via a comma. Java CSS will apply the rule to selector-specific components if it's able to match any of the selectors to existing components. For example, JButton, JLabel { foreground: blue } specifies blue as the foreground color of all JButtons and JLabels.
More About Declarations
A style sheet's rule ends with a brace-delimited list of declarations arranged into property name/value pairs. Each property name must be separated from its value via a colon (:) character. Furthermore, each declaration must be separated from its successor via a semicolon (;). You also have the option to place a semicolon after the final declaration and before the closing brace character.
Java CSS lets you tag a declaration value with the !over modifier to provide a smooth animated transition between property values. This modifier must be followed by a duration value specified in milliseconds (500ms), seconds (10s), or minutes (1.5m). In Figure 1, for example, red !over 0.3s causes the background of the slider identified as tip to transition from its current color to red over a 0.3-second interval.
By default, this animation is nonlinear, with slight acceleration at the beginning and deceleration at the end. However, you can append a keyword from the following list to !over and its duration to change the form of animation:
- default: Use the default curve, with slight acceleration at the beginning and deceleration at the end.
- linear: No acceleration or deceleration.
- ease-in: Animation starts slow and accelerates.
- ease-out: Animation starts fast and decelerates.
- ease-in-out: Stronger acceleration and deceleration than default.
For example, appending linear (as in !over 0.8s linear) removes the acceleration and deceleration from the animation.
Java CSS also lets you tag a declaration value with the !important modifier to increase a declaration's priority, so that the declaration overrides similar declarations that are not tagged with this modifier. For example, suppose a style sheet contains the following two rules, which pertain to the user interface's text fields:
JTextField:{text == ""} { background: red !important } JTextField#foo:{text == ""} {background: pink }
Normally, the JTextField with an ID matching foo would have its background colored pink whenever this text field contained no text (JTextField#foo is more specific than JTextField). However, !important forces foo's background to be colored red.
Many of the properties previously presented are associated with setter methods that Java CSS invokes to set property values. For example, JSlider's paintTicks property is associated with a setPaintTicks() setter method. However, Java CSS also generates a few synthetic properties (for convenience) that don't have setter methods:
- font-family: Font family name (such as Arial).
- font-size: Expressed using standard CSS units (14pt or 0.5in, for example). Percentage sizes such as 150% are also supported.
- font-style: plain or italic.
- font-weight: normal or bold.
- text-decoration: none, underline, or line-through.
Java CSS automatically generates the first four synthetic properties for any object having a font property; that is, setting the font property automatically sets the font-family, font-size, font-style, and font-weight synthetic properties. Java CSS automatically generates the final synthetic property for JLabels and AbstractButtons.
Finally, each property has its own type, which implies a set of values that can be legally associated with the property's name. Java CSS supports the property types (and their value sets) listed below:
- boolean or Boolean: true or false.
- Border: Calls to BorderFactory methods, such as BorderFactory.createEtchedBorder(), or null.
- byte or Byte: Any string accepted by Byte.valueOf().
- char or Character: Any string of length 1.
- Color: # followed by six hexadecimal digits, or one of the following, where r, g, b, and a are placeholders for integer values ranging from 0255:
- new java.awt.Color (r, g, b)
- new java.awt.Color (r, g, b, a)
- new Color (r, g, b)
- new Color (r, g, b, a)
- java.awt.Color.BLUE (or other field constant)
- Color.blue (or other field constant)
- red (or other field constant)
- null
- Dimension: Two comma-separated integer values; a call to Dimension's public Dimension(int width, int height) constructor, as in new Dimension(200, 100); or null.
- double or Double: Any string accepted by Double.valueOf().
- Enum: Any enumerated constant name.
- float or Float: Any string accepted by Float.valueOf().
- Font: Any string accepted by Font.decode(), as in Arial-BOLD-18, or null.
- Insets: Four comma-separated integer values; a call to Insets' public Insets(int top, int left, int bottom, int right) constructor, as in new Insets(2, 2, 2, 2); or null.
- int or Integer: Any string accepted by Integer.valueOf().
- Keystroke: Any string accepted by Keystroke.getKeyStroke(), or null.
- long or Long: Any string accepted by Long.valueOf().
- short or Short: Any string accepted by Short.valueOf().
- String: Any string surrounded by single or double quotes (the string may contain \", \', \\, \n, \r, \t, or \b escape sequences), or null.
- com.sun.stylesheet.types.Size: Any string representing a size expressed in percentages (10%), inches (3.5in), centimeters (25cm), millimeters (60.8mm), points (72pt), pixels (600px), picas (10.2pc), ems (1.0em), or exes (22ex).
- com.sun.stylesheet.types.Time: Any string representing a time expressed in milliseconds (500ms), seconds (20.5s), or minutes (50.3m).