CSS is an integral part of JavaFX but heretofore has been implemented exclusively in private API within, primarily, the com.sun.javafx.css package. Adding a styleable property or pseudo-class state in JavaFX is fairly straight-forward and exposing certain elements of the internal API will facilitate the development of custom UI controls in the open source environment. This document outlines this public API.
Say a contributor wished to develop a Watermark control which could overlay a region with copyright information. The Watermark API has a DoubleProperty that controls then angle at which the copyright text is displayed. The contributor wishes to make this copyrightAngle styleable through CSS. The contributor also wants to be able to modify some visual aspect of the Watermark once the copyright has been viewed; perhaps making the Watermark more transparent. The contributor wants to use a 'viewed' pseudo-class state to achieve this.
Making copyrightAngle styleable
First, the copyrightAngle is made styleable by having it extend one of the
StyleableProperty classes; in this case,
StyleableDoubleProperty. The code shown here follows the typical pattern. Notice that the only difference between a
DoubleProperty and the
StyleableDoubleProperty is the addition of the
getCssMetaData method which links the copyrightAngle property to its corresponding
CssMetaData instance provides information about the CSS style and some methods that allow CSS to set the property's value. The convention is to instantiate the
CssMetaData as a singleton (note that this example code is not thread safe, but is sufficient). Here, the CSS property name is "-my-copyright-angle" and the initial value of 45 degrees with the default value of the copyrightAngle property.
In order for the CSS engine to know about the styleable properties of Watermark, the methods
getCssMetaData() need to be implemented. The first is a static method that returns the
CssMetaData of the Watermark class and of its super class; in Watermark's case, the super class is
Control. The second method returns the same value but is implemented as an instance method so that it is not necessary to use reflection to call
getClassCssMetaData(). These methods are called quite frequently; for efficiency, the List<CssMetaData> is only created once. The following code shows a typical implementation.
At this point, the copyrightAngle can be styled through CSS. For example, to display the copyright from lower left to upper right:
Making 'viewed' a pseudo-class
The 'viewed' pseudo-class will be implemented as a BooleanProperty. Although BooleanProperty is most commonly used for pseudo-class state, any property type can be a pseudo-class, even a Styleable*Property. When the 'viewed' property changes value, the code needs to notify CSS that the state has changed. The place to do this is in the
invalidated() method of the property which calls the
pseudoClassStateChanged method, passing it then
PseudoClass.State of the pseudo-class that changed state. Again, this code follows the typical pattern of implementing a property. Note that a "simple" property could be used here, but the invalidated method would still need to be overridden. Since the anonymous class will be created anyway, the few bytes needed for the bean and name will be saved by using BooleanPropertyBase instead of SimpleBooleanProperty.
getPseudoClassStates() which returns the pseudo-class state of the node. The node's pseudo-class state should include the state from the node class and all its super classes. Therefore, the first thing
getPseudoClassStates() should do is call
With this framework in place, the following style can be used to make the watermark more transparent if it has been viewed.
- The primary goal is to create an API that allows CSS styling to be applied to a JavaFX property and to support the use of pseudo-class state in the open-software environment.
- The API should support the following javafx.beans.property types: BooleanProperty, FloatProperty, DoubleProperty, IntegerProperty, LongProperty, and StringProperty. Support for ObjectProperty is desired but would, possibly, require additional hooks into the parser or public API to convert a parsed value to the parameterized type which may not be feasible at this time. However, the ability to style an ObjectProperty with a parameterized type such as Insets or Paint is essential. Likewise, ObjectProperty<MyEnumType> presents challenges, particularly in parsing and converting, that make support for this type of property impracticable at this time.
- A secondary goal is to make the API such that an IDEs whould be able to automatically generate much of the necessary code. It is not a goal, however, to create the IDE boilerplate.
- While it is possible to have classes other than instances of Node be styleable through CSS, this support requires an additional interface which exists in private implementation. Including this interface in the public API is not a goal.
- The current implementation of CSS in JavaFX is only a small portion of the W3C standards and is only partially compliant with those standards. It is beyond the scope of this API implementation to rectify differences between the W3C standards and the current JavaFX implementation.
There are two main pieces to the styleable property architecture. First is a CssMetaData whose value can be represented syntactically in a .css file. A CssMetaData encapsulates the CSS property name, the type into which the string value is converted, and the default value of the property. Second is the JavaFX property to which the parsed CssMetaData value applies. Any JavaFX property that supports this styling is a StyleableProperty. A StyleableProperty also incorporates additional logic to ensure that values set by the user through calls to set methods are not overridden by styles in a user agent stylesheet.
There is a one-to-one correspondence between a CssMetaData and a StyleableProperty. A CssMetaData is scoped to a class whereas a StyleableProperty is an attribute of a class instance. Typically, a node will assume the CssMetaData of its ancestors. During CSS processing, the CSS engine iterates over a List<CssMetaData> (particular to the node) and looks up the parsed value of each CssMetaData in turn. If the CssMetaData has a parsed value, the parsed value is converted to the type of the StyleableProperty and the StyleableProperty is set.
Making a property styleable, then, consist of:
- defining the javafx.beans.property as a StyleableProperty
- creating a corresponding CssMetaData
- ensuring the CssMetaData is returned in the List<CssMetaData>
Pseudo-class state support consists of notifying CSS of a pseudo-class state change via the
invalidated() method of a property, and implementation of a method that returns the pseudo-class state of the node.
StyleableProperty is an interface that is implemented by various classes that extend from javafx.beans.property properties; for example,
getCssMetaData method is useful for getting from a
StyleableProperty to the corresponding
CssMetaData, which is useful for tooling and unit testing. For example, one can get the CssMetaData of the fillProperty of a Rectangle:
There is an implementing classes for each of the javafx.beans.property.*PropertyBase types and for each of the javafx.beans.property.Simple*Property types. The developer should choose the simple variety unless there is some method that needs to be overridden (but even then, it is a matter of choice).
public class StyleableBooleanProperty extends BooleanPropertyBase implements StyleableProperty<Boolean>
public class StyleableFloatProperty extends FloatPropertyBase implements StyleableProperty<Float>
public class StyleableDoubleProperty extends DoublePropertyBase implements StyleableProperty<Double>
public class StyleableIntegerProperty extends IntegerPropertyBase implements StyleableProperty<Integer>
public class StyleableLongProperty extends LongPropertyBase implements StyleableProperty<Long>
public class StyleableStringProperty extends StringPropertyBase implements StyleableProperty<String>
public class StyleableObjectProperty<T> extends ObjectPropertyBase<T> implements StyleableProperty<T>
Each of the classes have a constructor taking a
CssMetaData arg and another taking a
CssMetaData arg and an initial value. For example,
StyleableBooleanProperty has the following constructors:
public class SimpleStyleableBooleanProperty extends SimpleBooleanProperty implements StyleableProperty<Boolean>
public class SimpleStyleableFloatProperty extends SimpleFloatProperty implements StyleableProperty<Float>
public class SimpleStyleableDoubleProperty extends SimpleDoubleProperty implements StyleableProperty<Double>
public class SimpleStyleableIntegerProperty extends SimpleIntegerProperty implements StyleableProperty<Integer>
public class SimpleStyleableLongProperty extends SimpleLongProperty implements StyleableProperty<Long>
public class SimpleStyleableStringProperty extends SimpleStringProperty implements StyleableProperty<String>
public class SimpleStyleableObjectProperty<T> extends SimpleObjectProperty<T> implements StyleableProperty<T>
Each of these simple classes have a constructor taking an Object which is the property bean and a String which is the property name in combination with a
CssMetaData arg and another taking a
CssMetaData arg and an initial value. For example,
SimpleStyleableBooleanProperty has the following constructors:
In the future,
StyleableProperty may need to incorporate support for attribute selectors and animations.
CssMetaData encapsulates the data needed to lookup a value and apply that value to a
StyleableProperty. This includes the CSS property name, the default property value, and a link back to the corresponding
StyleableProperty. The class is abstract and it is necessary to implement two methods which are invoked from the CSS engine:
StyleableProperty is required to be a
WritableValue. WritableValue is the interface which has the
setValue method. If it isn't possible to
setValue on the property, then it cannot be styled from CSS. The implementation is fairly consistent throughout the existing code, typically:
isSettable should be implemented in a way that does not cause the expansion of the property. The
isSettable check is performed before the CSS value is looked up. If the property is not settable, then no further CSS processing is done for that property (on any given pulse). The
getWritableValue method is invoked only if there is a CSS value to apply. Thus, if the property is not settable or there is no CSS value, the property is not expanded[#1].
The CSS engine does not call
setValue directly on the
WritableValue. Rather, the code calls a set method on the
CssMetaData which, in turn, calls
applyStyle on the
StyleableProperty. This level of indirection allows a
CssMetaData to intercept the value before the calculated style value is applied (in other words, before
setValue is called on the corresponding property). This is used primarily for
Number based properties where the parameterized type is
Number but the actual type might be
Integer and so the value's
intValue() method needs to be invoked.
The parameterization of
CssMetaData is that it requires a
Node is the visible element of the scene graph. The
V parameter is the type of the property's value, for example Boolean.
One major aspect of the private interface has been omitted from the public interface and that is the notion of a Conveter. A Converter takes a value from the parser (a ParsedValue) and converts it to the corresponding java type. So, for example, a ColorConverter would convert a ParsedValue representing a Color into a Color. If possible, the parser performs the conversion. But this is not always possible such as when converting an em size to an absolute pixel value. The Converter was omitted because including it in the API would result in dragging much more of the private implementation than is desired. ParsedValue, for instance, is (in my opinion) not suitable for public API in its current state because it is tightly coupled with the parser and the Converters. I'm not sure, however, that this public API can exist without a Converter member, but I believe I can infer the Converter from the WritableValue. Alternatively, some type indicator could be used or a set of CSS*Property classes (e.g., CSSBooleanProperty) could be created.
Additional Node API.
static List<CssMetaData> getClassCssMetaData()
These methods return a List of CssMetaData supported by the node. The list should include not only the properties of this node, but the properties of the node's super-class(es) as well. This method is called frequently and, by convention, it returns a static list. Since this static list may be used by other node classes, the convention is to provide a public static method that returns the static list: public static List<CssMetaData> getClassCssMetaData(). The getCssMetaData() method is used by the CSS engine to avoid reflection. A typical implementation would be:
The pattern for handling pseudo-class state is to override the invalidated method of the property in order to invoke
pseudoClassStateChanged which sets a flag indicating that the node needs updating by CSS. During a pulse, pseudoClassStateChanged may be called many times, but only if there is a style with the given pseudo-class will the update flag be set. Once the update flag is set, subsequent calls to pseudoClassStateChanged are effectively no-ops.
When CSS processes the update, it gets the node's current pseudo-class state by calling
getPseudoClassStates(). Each node that has pseudo-class state overrides
PseudoClass contains two inner classes, State and States TBD: bad naming?. PseudoClass.State is an immutable object that represents one pseudo-class state. PseudoClass.States is a mutable object that is returned from
getPseudoClassStates() and represents the summation of individual PseudoClass.State values that are in effect.
A pseudo-class state is unique according to the string value of the pseudo-class that might appear in a CSS file. In other words, there is only one PseudoClassState for "hover", even if the "hover" pseudo-class is found in more than one class. Care must be exercised when choosing names for pseudo-classes so they do not conflict with other pseudo-classes.
Typical usage is:
The API is outlined below.
pseudo-class Node API
- property expansion. Consider something like opacityProperty which starts out null. If nothing ever touches it, it stays null. CSS wants to know if opacity can be set before it goes off looking for styles. If it can't be set, then it is skipped. If it can, and no styles for it are found, then opacityProperty should still be null, otherwise every time CSS touches a property it would be expanded - not a good thing. So the isSettable check basically says, the property is settable if it is null or isn't bound. The only time the property is expanded is if there is actually a style for it. Further explanation of