...
Say a contributor wished to develop a Watermark control which could overlay a region and present a widget for obtaining with copyright information. The Watermark API has a StringProperty cornerProperty DoubleProperty that controls then angle at which corner of the region the widget is to be the copyright text is displayed. The contributor wishes to make this cornerProperty copyrightAngle styleable through CSS.
Design Goals
The primary goal is to create an API that allows CSS styling to be applied to a JavaFX property. 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.
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.
Architecture
There are two main pieces to the architecture. First is a StyleablePropertyMetaData whose value can be represented syntactically in a .css file. A StyleablePropertyMetaData 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 StyleablePropertyMetaData 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 StyleablePropertyMetaData and a StyleableProperty. A StyleablePropertyMetaData 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<StyleablePropertyMetaData> (particular to the node) and looks up the parsed value of each StyleablePropertyMetaData in turn. If the StyleablePropertyMetaData 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 StyleablePropertyMetaData
- ensuring the StyleablePropertyMetaData is returned in the List<StyleablePropertyMetaData>
API
StyleableProperty
StyleableProperty is an interface that is implemented by various classes that extend from javafx.beans.property properties; for example,
Code Block |
---|
public class StyleableBooleanProperty extends BooleanPropertyBase implements StyleableProperty<Boolean> { ... } |
.
Code Block | ||
---|---|---|
| ||
interface StyleableProperty<T> {
/**
* This method is called from CSS code to set the value of the property.;
*/
void applyStyle(Origin origin, T value);
/**
* Tells the origin of the value of the property. This is needed to
* determine whether or not CSS can override the value.
*/
Origin getOrigin();
/**
* Reflect back the StyleablePropertyMetaData that corresponds to this;
* <code>javafx.beans.property.StyleableProperty</code>
*/
StyleablePropertyMetaData getStyleablePropertyMetaData();
}
|
In implementation, the applyStyle
method delegates to the set method of the javafx.beans.property property
. The getStyleablePropertyMetaData
method is useful for getting from a StyleableProperty
to the corresponding StyleablePropertyMetaData
, which is useful for tooling and unit testing. Example:
Code Block |
---|
private StringProperty corner = new StyleableStringProperty(CORNER, "lower-right") {
// implementation of StringProperty abstract methods assumed
}
public void setCorner(String corner) {
cornerProperty.set(corner);
}
public String getCorner() {
return cornerProperty().get();
}
public final StringProperty cornerProperty() {
return corner;
}
|
Implementing classes:
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 these classes have a constructor taking a StyleablePropertyMetaData
arg and another taking a StyleablePropertyMetaData
arg and an initial value. For example, StyleableBooleanProperty
has the following constructors:
Code Block |
---|
/**
* The constructor of the {@code StyleableBooleanProperty}.
*/
public StyleableBooleanProperty(StyleablePropertyMetaData StyleablePropertyMetaData) {
super();
this.StyleablePropertyMetaData = StyleablePropertyMetaData;
}
/**
* The constructor of the {@code StyleableBooleanProperty}.
*
* @param StyleablePropertyMetaData
* the {@code StyleablePropertyMetaData} that corresponds to this {@code StyleableProperty}
* @param initialValue
* the initial value of the wrapped {@code Object}
*/
public StyleableBooleanProperty(StyleablePropertyMetaData StyleablePropertyMetaData, boolean initialValue) {
super(initialValue);
this.StyleablePropertyMetaData = StyleablePropertyMetaData;
}
|
Issues:
In the future, StyleableProperty
may need to incorporate support for attribute selectors and animations.
StyleablePropertyMetaData
StyleablePropertyMetaData
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:
Code Block |
---|
/**
* Check to see if the corresponding property on the given node is
* settable. This method is called before any styles are looked up for the
* given property. It is abstract so that the code can check if the property
* is settable without expanding the property. Generally, the property is
* settable if it is not null or is not bound.
*
* @param node The node on which the property value is being set
* @return true if the property can be set.
*/
public abstract boolean isSettable(N node);
/**
* Return the corresponding <code>javafx.beans.value.WriteableValue</code> for
* the given Node. Note that calling this method will cause the property
* to be expanded.
* @param node
* @return
*/
public abstract WritableValue<V> getWritableValue(N node); |
The 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:
Code Block |
---|
public abstract boolean isSettable(Watermark node) {
return corner == null || corner.isBound() == false;
}
public abstract WritableValue<String> getWritableValue(Watermark node) {
return cornerProperty();
}
|
Note that isSettable
is 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 there is no need to do the lookup. 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.
The CSS engine does not call setValue
directly on the WritableValue
. Rather, the code calls a set method on the StyleablePropertyMetaData
which, in turn, calls applyStyle
on the StyleableProperty
. This level of indirection allows a StyleablePropertyMetaData
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 StyleablePropertyMetaData
is that it requires a Node
since Node
is the visible element of the scene graph. The V
parameter is the type of the property's value. The corner property of Watermark would be declared as StyleablePropertyMetaData<Watermark, String>.
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' pseud-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.
Code Block |
---|
private DoubleProperty copyrightAngle = new StyleableDoubleProperty(45d) {
/** Link this property with its CssMetaData */
@Override
public CssMetaData getCssMetaData() {
return COPYRIGHT_ANGLE;
}
@Override
public Object getBean() {
return Node.this;
}
@Override
public String getName() {
return "copyrightAngle";
}
};
};
public final void setCopyrightAngle(double value) {
copyrightAngle.set(value);
}
public final double getCopyrightAngle() {
return copyrightAngle.get();
}
public final DoubleProperty copyrightAngleProperty() {
return copyrightAngle;
}
|
The 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.
Code Block |
---|
private static final CssMetaData<Watermark,Number> COPYRIGHT_ANGLE = new CssMetaData("-my-copyright-angle", 45d) {
public abstract boolean isSettable(Watermark node) {
return copyrightAngle == null || copyrightAngle.isBound() == false;
}
public abstract WritableValue<Number> getWritableValue(Watermark node) {
return copyrightAngleProperty();
}
};
|
In order for the CSS engine to know about the styleable properties of Watermark, the methods getClassCssMetaData()
and 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.
Code Block |
---|
private static final List<CssMetaData> CSS_META_DATA;
static {
final List<CssMetaData> metaData = new ArrayList<CssMetaData>(Control.getClassCssMetaData());
Collections.addAll(metaData,
COPYRIGHT_ANGLE
);
CSS_META_DATA = Collections.unmodifiableList(metaData);
}
public static List<CssMetaData> getClassCssMetaData() {
return CSS_META_DATA;
}
@Override public List<CssMetaData> getCssMetaData() {
return getClassCssMetaData();
}
|
At this point, the copyrightAngle can be styled through CSS. For example, to display the copyright from lower left to upper right:
Code Block |
---|
.watermark { -my-copyright-angle: -45; }
|
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.
Code Block |
---|
public final void setViewed(boolean value) {
viewed.set(value);
}
public final boolean isViewed() {
return viewed.get();
}
public final BooleanProperty viewedProperty() {
return viewed;
}
private static final PseudoClass.State VIEWED_PSEUDO_CLASS = PseudoClass.getState("viewed");
private BooleanProperty viewed = new BooleanPropertyBase(false) {
@Override
protected void invalidated() {
pseudoClassStateChanged(VIEWED_PSEUDO_CLASS);
}
@Override
public Object getBean() {
return Node.this;
}
@Override
public String getName() {
return "viewed";
}
};
}
|
CSS calls 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 super.getPseudoClassStates()
.
Code Block |
---|
public PseudoClass.States getPseudoClassStates() {
PseudoClassStates pseudoClassStates = super.getPseudoClassStates();
if (isViewed()) pseudoClassStates = pseudoClassStates.addState(VIEWED_PSEUDO_CLASS);
return pseudoClassStates;
}
|
With this framework in place, the following style can be used to make the watermark more transparent if it has been viewed.
Code Block |
---|
.watermark:viewed { -fx-opacity: 30%; }
|
Design Goals
- 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.
Architecture
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.
API
StyleableProperty
StyleableProperty is an interface that is implemented by various classes that extend from javafx.beans.property properties; for example,
Code Block |
---|
public class StyleableBooleanProperty extends BooleanPropertyBase implements StyleableProperty<Boolean> { ... } |
.
Code Block | ||
---|---|---|
| ||
interface StyleableProperty<T> {
/**
* This method is called from CSS code to set the value of the property.;
*/
void applyStyle(Origin origin, T value);
/**
* Tells the origin of the value of the property. This is needed to
* determine whether or not CSS can override the value.
*/
Origin getOrigin();
/**
* Reflect back the CssMetaData that corresponds to this;
* <code>javafx.beans.property.StyleableProperty</code>
*/
CssMetaData getCssMetaData();
}
|
The 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:
Code Block |
---|
Rectangle rect = new Rectangle(50,50);
CssMetaData fillCssMetaData = ((StyleableProperty)rect.fillProperty()).getCssMetaData();
System.out.println("Use " + fillCssMetaData.getProperty() + " to style Rectangle fill");
|
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:
Code Block |
---|
/**
* The constructor of the {@code StyleableBooleanProperty} |
Code Block |
public abstract class StyleablePropertyMetaData<N extends Node, V> { /** * Check to see if the corresponding property on the given node is * settable. This method is called before any styles are looked up for the * given property. It is abstract so that the code can check if the property * is settable without expanding the property. Generally, the property is * settable if it is not null or is not bound. * * @param node The node on which the property value is being set * @return true if the property can be set. */ public abstract boolean isSettable(N node); /**StyleableBooleanProperty(CssMetaData CssMetaData) { * Return the corresponding <code>javafx.beans.value.WriteableValue</code> for super(); * the given Nodethis.CssMetaData Note= thatCssMetaData; calling this method will cause the property } * to be expanded./** * The @paramconstructor node of the * @return{@code StyleableBooleanProperty}. */ public* abstract@param WritableValue<V>CssMetaData getWritableValue(N node); * /** * Set the value{@code ofCssMetaData} thethat correspondingcorresponds propertyto onthis the given Node.{@code StyleableProperty} * @param nodeinitialValue The node* on which the property value isthe beinginitial set value of *the @paramwrapped value The value to which the property is set */ public void set(N node, V value, Origin origin) { // details omitted, but this method ends up calling applyStyle in the StyleableProperty interface. } private final String property; {@code Object} */ public StyleableBooleanProperty(CssMetaData CssMetaData, boolean initialValue) { super(initialValue); this.CssMetaData = CssMetaData; } |
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:
Code Block |
---|
/** * The constructor @returnof the CSS property name{@code SimpleStyleableBooleanProperty}. */ public finalSimpleStyleableBooleanProperty(Object bean, String getProperty(name, CssMetaData CssMetaData) { return propertysuper(bean, name); } private final V initialValue;this.CssMetaData = CssMetaData; } /** * The constructor initial value of a StyleablePropertyMetaData corresponds to the defaultof the {@code SimpleStyleableBooleanProperty}. * * @param CssMetaData * value of the WritableValue in code. {@code CssMetaData} that corresponds to this {@code StyleableProperty} * @param initialValue * For example, the defaultinitial value of the Shape.fill is Color.BLACK and the * initialValue of Shape.CssMetaData.FILL is also Color.BLACK. * <p> * There may be exceptions to this, however. The initialValue may dependwrapped {@code Object} */ public SimpleStyleableBooleanProperty(Object bean, String name, CssMetaData CssMetaData, boolean initialValue) { super(bean, name, initialValue); this.CssMetaData = CssMetaData; } /** * onThe the stateconstructor of the Node. A ScrollBar has a default orientation of{@code SimpleStyleableBooleanProperty}. */ horizontal. Ifpublic the ScrollBar is vertical, however, this method shouldSimpleStyleableBooleanProperty(CssMetaData CssMetaData) { super(); * return Orientationthis.VERTICAL.CssMetaData Otherwise, a vertical ScrollBar would be= CssMetaData; } /** * incorrectlyThe setconstructor toof a horizontal ScrollBar when the initial value isthe {@code SimpleStyleableBooleanProperty}. * * @param applied.CssMetaData * @return The initial value of the property, possibly null {@code CssMetaData} that corresponds to this {@code StyleableProperty} */ @param publicinitialValue V getInitialValue(N* node) { returnthe initialValue; initial value } of the privatewrapped final List<StyleablePropertyMetaData> subProperties;{@code Object} */** *public SimpleStyleableBooleanProperty(CssMetaData CssMetaData, boolean initialValue) { super(initialValue); this.CssMetaData = CssMetaData; } |
Issues:
In the future, StyleableProperty
may need to incorporate support for attribute selectors and animations.
CssMetaData
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:
Code Block |
---|
/** * Check to see if the corresponding property on the given node is * settable. This method is called before any styles are looked up for the * given property. It is abstract so that the code can check if the property * is settable without expanding the property. Generally, the property is * settable if it is not null or is not bound. *The sub-properties refers to the constituent properties of this property, * if any. For example, "-fx-font-weight" is sub-property of "-fx-font". */ public final List<StyleablePropertyMetaData> getSubProperties() { return subProperties; } private final boolean inherits; /** * If true, the value of this property is the same as * the parent's computed value of this property. * @default false * @see <a href="http://www.w3.org/TR/css3-cascade/#inheritance">CSS Inheritance</a> */ public final boolean isInherits() { return inherits; } /** * Construct a StyleablePropertyMetaData with the given parameters and no sub-properties. * @param property the CSS property * @param initalValuenode theThe defaultnode valueon ofwhich the corresponding property whichvalue mayis bebeing nullset * @param inherits@return true if thisthe property usescan CSSbe inheritanceset. */ @param subPropertiespublic theabstract sub-properties of this property. For example,boolean isSettable(N node); /** * Return the -fx-font property has the sub-properties -fx-font-family,corresponding <code>javafx.beans.value.WriteableValue</code> for * the given Node. Note that calling this method will cause the property * to be expanded. * @param node * @return */ public abstract WritableValue<V> getWritableValue(N node); |
The 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:
Code Block |
---|
public abstract boolean isSettable(Watermark node) {
return corner == null || corner.isBound() == false;
}
public abstract WritableValue<String> getWritableValue(Watermark node) {
return cornerProperty();
}
|
Note that 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.
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
since Node
is the visible element of the scene graph. The V
parameter is the type of the property's value, for example Boolean.
Code Block |
---|
public abstract class CssMetaData<N extends Node, V> {-fx-font-size, -fx-font-weight, and -fx-font-style. */ protected StyleablePropertyMetaData( final String property, final V initialValue, boolean inherits, final List<StyleablePropertyMetaData> subProperties) { this.property = property; this.initialValue = initialValue; this.inherits = inherits; this.subProperties = subProperties != null ? Collections.unmodifiableList(subProperties) : null; if (this.property == null) throw new IllegalArgumentException("property cannot be null"); } /** * Construct a StyleablePropertyMetaData with the given parameters and no sub-properties. * @param property the CSS property * @param initalValue the default value of the corresponding property which may be null * @param inherits true if this property uses CSS inheritance */ protected StyleablePropertyMetaData( final String property, final V initialValue, boolean inherits) { this(property, initialValue, inherits, null); } /** * Construct a StyleablePropertyMetaData with the given parameters, inherit set to * false and no sub-properties. * @param property the CSS property * @param initalValue the default value of the corresponding property which may be null */ protected StyleablePropertyMetaData( final String property, final V initialValue) { this(property, initialValue, false, null); } /** * ConstructCheck ato StyleablePropertyMetaDatasee withif the givencorresponding parameters,property initialValueon is the * null, inherit is set to false, and no sub-properties.given node is * @paramsettable. propertyThis themethod CSSis property called */ protected StyleablePropertyMetaData( final String property) { this(property, false, null); } } |
Issues:
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.
Origin
Code Block |
---|
/**
* Enumeration of the possible source or origin of a stylesheet and styles.
*/
public enum Origin {
/** The stylesheet is a user-agent stylesheet */
USER_AGENT,
/** The value of a property was set by the user through a call to a set method */
USER,
/** The stylesheet is an external file */
AUTHOR,
/** The style is from the Node via setStyle */
INLINE
}
|
Additional Node API.
List<StyleablePropertyMetaData> getStyleablePropertyMetaData()
static List<StyleablePropertyMetaData> getClassStyleablePropertyMetaData()
These methods return a List of StyleablePropertyMetaData 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<StyleablePropertyMetaData> getClassStyleablePropertyMetaData(). The getStyleablePropertyMetaData() method is used by the CSS engine to avoid reflection. A typical implementation would be:
Code Block |
---|
private static class StyleableProperties {
private static final StyleablePropertyMetaData<Watermark, String> CORNER = new StyleablePropertyMetaData<Watermark, String>("-my-corner", "lower-right") {
public abstract boolean isSettable(Watermark node) {
return corner == null || corner.isBound() == false;
}
public abstract WritableValue<String> getWritableValue(Watermark node) {
return cornerProperty();
}
}
private static final List<StyleablePropertyMetaData> META_DATA;
static {
final List<StyleablePropertyMetaData> data = new ArrayList<StyleablePropertyMetaData>();
Collections.addAll(data,
Control.getClassStyleablePropertyMetaData(),
CORNER
);
META_DATA = Collections.unmodifiableList(data);
}
}
public static List<StyleablePropertyMetaData> getClassStyleablePropertyMetaData() {
return StyleableProperties.META_DATA;
}
public static List<StyleablePropertyMetaData> getStyleablePropertyMetaDataMetaData() {
return getClassStyleablePropertyMetaData();
}
|
Pseudo-class
In the current implementation, the pattern for handling pseudo-class state is to override the invalidated method of the property in order to invoke impl_pseudoClassStateChanged
which sets a flag indicating that the node needs updating by CSS. When CSS processes the update, it gets the node's current pseudo-class state by calling impl_getPseudoClassState()
. Each node that has pseudo-class state overrides impl_getPseudoClassState
. A typical example from ButtonBase
, where ARMED_PSEUDOCLASS_STATE is a bit mask:
Code Block |
---|
@Override public long impl_getPseudoClassState() {
long mask = super.impl_getPseudoClassState();
if (isArmed()) mask |= ARMED_PSEUDOCLASS_STATE;
return mask;
}
|
The issue with promoting this implementation to public API is that it is susceptible to coding errors. One might forget to call super.impl_getPsuedoClassState()
or might use the wrong bit mask, either accidentally or purposefully. It should be simple enough to codify this pattern in a *PseudoClass class, as shown in the an example below. Note that this PseudoClass requires a bean object and that the bean must be a Node in order to call impl_pseudoClassStateChanged()
.
Code Block |
---|
public class BooleanPseudoClass extends SimpleBooleanProperty {
public BooleanPseudoClass(String pseudoClass, Object bean, String name) {
super(bean, name);
this.pseudoClass = pseudoClass;
}
public BooleanPseudoClass(String pseudoClass, Object bean, String name, boolean initialValue) {
super(bean, name, initialValue);
this.pseudoClass = pseudoClass;
}
@Override protected void invalidated() {
((Node)getBean()).pseudoClassStateChanged(pseudoClass);
}
private final String pseudoClass;
}
|
But this approach is inadequate. There is still the issue that, if invalidate is overridden, the developer might neglect to call super.invalidate(). Also some pseudo-class states are implemented in read-only properties (Node hover, for example), some are boolean and others are not boolean, and some are also styleable properties (ScrollBar orientation, for example). ScrollBar's orientation pseudo-classes are "horizontal" and "vertical" and an ObjectPseudoClass<Orientation> would still require that invalidated be overridden.
Another possible alternative is to make use of attribute selectors for 3rd party controls while maintaining private implementation of pseudo-classes for the SDK. Attrtibute selectors may not be feasible under current time constraints and performance may be an issue.
Therefore, it seems that the best alternative is to promote the existing private implementation to public API. Although this involves more code and is susceptible to error, the API is proven and promoting it involves less risk.
Pseudo-class state
Wiki Markup |
---|
Currently, pseudo-class state is held in a long. Each bit in the long value corresponds to a specific pseudo-class.
{{private static final long HOVER_PSEUDOCLASS_STATE = PseudoClassStateBitMaskUtilities .getMask("hover");}}
To allow for a larger number of pseudo-classes, a long\[\] will be returned from getPseudoClassState(). Each individual state, such as HOVER_PSEUDOCLASS_STATE will still be a long, but the upper 4 bits hold the index of the mask within the long\[\]. The remaining bits are the bit mask itself. This allows for 2^4 * 60, or 1920, possible pseudo-classes. This technique is currently used for handling multiple style classes. |
before any styles are looked up for the
* given property. It is abstract so that the code can check if the property
* is settable without expanding the property. Generally, the property is
* settable if it is not null or is not bound.
*
* @param node The node on which the property value is being set
* @return true if the property can be set.
*/
public abstract boolean isSettable(N node);
/**
* Return the corresponding <code>javafx.beans.value.WriteableValue</code> for
* the given Node. Note that calling this method will cause the property
* to be expanded.
* @param node
* @return
*/
public abstract WritableValue<V> getWritableValue(N node);
/**
* Set the value of the corresponding property on the given Node.
* @param node The node on which the property value is being set
* @param value The value to which the property is set
*/
public void set(N node, V value, Origin origin) {
// details omitted, but this method ends up calling applyStyle in the StyleableProperty interface.
}
private final String property;
/**
* @return the CSS property name
*/
public final String getProperty() {
return property;
}
private final V initialValue;
/**
* The initial value of a CssMetaData corresponds to the default
* value of the WritableValue in code.
* For example, the default value of Shape.fill is Color.BLACK and the
* initialValue of Shape.CssMetaData.FILL is also Color.BLACK.
* <p>
* There may be exceptions to this, however. The initialValue may depend
* on the state of the Node. A ScrollBar has a default orientation of
* horizontal. If the ScrollBar is vertical, however, this method should
* return Orientation.VERTICAL. Otherwise, a vertical ScrollBar would be
* incorrectly set to a horizontal ScrollBar when the initial value is
* applied.
* @return The initial value of the property, possibly null
*/
public V getInitialValue(N node) {
return initialValue;
}
private final List<CssMetaData> subProperties;
/**
* The sub-properties refers to the constituent properties of this property,
* if any. For example, "-fx-font-weight" is sub-property of "-fx-font".
*/
public final List<CssMetaData> getSubProperties() {
return subProperties;
}
private final boolean inherits;
/**
* If true, the value of this property is the same as
* the parent's computed value of this property.
* @default false
* @see <a href="http://www.w3.org/TR/css3-cascade/#inheritance">CSS Inheritance</a>
*/
public final boolean isInherits() {
return inherits;
}
/**
* Construct a CssMetaData with the given parameters and no sub-properties.
* @param property the CSS property
* @param initalValue the default value of the corresponding property which may be null
* @param inherits true if this property uses CSS inheritance
* @param subProperties the sub-properties of this property. For example,
* the -fx-font property has the sub-properties -fx-font-family,
* -fx-font-size, -fx-font-weight, and -fx-font-style.
*/
protected CssMetaData(
final String property,
final V initialValue,
boolean inherits,
final List<CssMetaData> subProperties) {
this.property = property;
this.initialValue = initialValue;
this.inherits = inherits;
this.subProperties = subProperties != null ? Collections.unmodifiableList(subProperties) : null;
if (this.property == null)
throw new IllegalArgumentException("property cannot be null");
}
/**
* Construct a CssMetaData with the given parameters and no sub-properties.
* @param property the CSS property
* @param initalValue the default value of the corresponding property which may be null
* @param inherits true if this property uses CSS inheritance
*/
protected CssMetaData(
final String property,
final V initialValue,
boolean inherits) {
this(property, initialValue, inherits, null);
}
/**
* Construct a CssMetaData with the given parameters, inherit set to
* false and no sub-properties.
* @param property the CSS property
* @param initalValue the default value of the corresponding property which may be null
*/
protected CssMetaData(
final String property,
final V initialValue) {
this(property, initialValue, false, null);
}
/**
* Construct a CssMetaData with the given parameters, initialValue is
* null, inherit is set to false, and no sub-properties.
* @param property the CSS property
*/
protected CssMetaData(
final String property) {
this(property, false, null);
}
}
|
Issues:
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.
Origin
Code Block |
---|
/**
* Enumeration of the possible source or origin of a stylesheet and styles.
*/
public enum Origin {
/** The stylesheet is a user-agent stylesheet */
USER_AGENT,
/** The value of a property was set by the user through a call to a set method */
USER,
/** The stylesheet is an external file */
AUTHOR,
/** The style is from the Node via setStyle */
INLINE
}
|
Additional Node API.
List<CssMetaData> getCssMetaData()
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:
Code Block |
---|
private static class StyleableProperties {
private static final CssMetaData<Watermark, String> CORNER = new CssMetaData<Watermark, String>("-my-corner", "lower-right") {
public abstract boolean isSettable(Watermark node |
Code Block |
/** * A set of utilities for handling pseudo-class states as long[] bit mask. */ public final class PseudoClassStateBitMaskUtilities { // // The Long value is a bit mask. The upper 4 bits of the mask are used to // hold the index of the mask within the long[] and the remaining bits are // used to hold the mask value. If, for example, "foo" is the 96th entry in // styleClassMask, the upper 4 bits will be 0x01 (foo will be at mask[1]) // and the remaining bits will have the 36th bit set. // // When creating the long[] bit set, you get the value from maskBits, // mask and shift the upper 4 bits to get the index of the string in // the long[], then or the value from maskBits with the mask[index]. // In our example, "foo" will always be at mask[1] // private static final Map<String,Long> maskBits = new HashMap<String,Long>(); // 4 is arbitrary but allows for 2^4 * 60, or 1920 unique strings. private static final int VALUE_BITS = Long.SIZE-4; // 0x0fffffffffffffff private static final long VALUE_MASK = ~(0xfL << VALUE_BITS); /** * Get the bit mask for the give String. The upper 4 bits of the mask are used to * hold the index of the mask within the long[] and the remaining bits are * used to hold the mask value. * @param string The string to mask. * @return The upper 4 bits is an index into the long[] mask representation * of strings. The remaining bits are the bit mask for this string * within the mask[index] */ public static long getMask(String string) { return corner Long== masknull =|| maskBitscorner.getisBound(string) == false; } public abstract ifWritableValue<String> getWritableValue(mask == nullWatermark node) { final int size = maskBits.sizereturn cornerProperty(); } } private static final long element = size / VALUE_BITS; // use top bits for element final int exp = size % VALUE_BITS; // remaining bits for valueList<CssMetaData> META_DATA; static { final List<CssMetaData> data = new ArrayList<CssMetaData>(); Collections.addAll(data, Control.getClassCssMetaData(), CORNER ); META_DATA = mask = Long.valueOf( (element << VALUE_BITS) | (1L << exp) // same as Math.pow(2,exp) ); maskBits.put(styleClass, mask); } return mask.longValue(); } /* * Set a bit in an array of bit masks. * @param maskArray The array of bit masks into which the bit from the mask param will be set. * @param mask The mask of the bit to set. * @return The resulting bit masks array. A new array is allocated if necessary. */ public long[] set(long[] maskArray, long mask) { final long element = (mask & ~VALUE_MASK); Collections.unmodifiableList(data); } } public static List<CssMetaData> getClassCssMetaData() { return StyleableProperties.META_DATA; } public static List<CssMetaData> getCssMetaDataMetaData() { return getClassCssMetaData(); } |
Pseudo-class
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 getPseudoClassStates()
.
PseudoClass
PseudoClass contains two inner classes, State and States 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:
Code Block |
---|
private static final PseudoClassState MAGIC_PSEUDO_CLASS_STATE = PseudoClassState.getState("xyzzy"); @Override public PseudoClass.States getPseudoClassStates() { final int PseudoClass.States indexstates = super.getPseudoClassStates(int)(element; >>> VALUE_BITS); if (isMagic()) states.addState(MAGIC_PSEUDO_CLASS_STATE); // need to grow? return states; } |
The API is outlined below.
Code Block |
---|
package javafx.scene; public final class PseudoClass if (index >= maskArray.length) { /** * There is only one PseudoClass.State finalinstance long[]for tempa =given new long[index+1];pseudoClass. * @return The PseudoClass.State for the given pseudoClass. Will not return System.arraycopy(maskArray, 0, temp, 0, maskArray.length);null. */ public static PseudoClass.State getState(String pseudoClass) { maskArray = temp; } // Use pseudoClass as key to find State in a Map<String,State> // If not found, create it maskArray[index] = maskArray[index] | mask;and add to the map. // return return maskArray; the PseudoClass.State } /** * Clear@return aA bitnew in an array of bit masks.PseudoClass.States instance */ @param maskArray The arraypublic ofstatic bit masks from which the bit from the mask param will be cleared. * @param mask The mask of the bit to clear. PseudoClass.States createStatesInstance() { } public final class State { /** Cannot @returncreate Thean resultinginstance bitof masksState array. except through PseudoClass static methods */ public long[] clear(long[] maskArray, long mask private State() { } final long element =@Override (maskpublic & ~VALUE_MASK); String toString() { // return the String that was used to finalcreate intthis PseudoClassState index = (int)(element >>> VALUE_BITS); pseudo-class, e.g. "hover" } } // mask inpublic maskArray?final ifclass not,States there{ is nothing to clear! /** if (index < maskArray.length) { * Add the state to the current set of states. The result of the longoperation mwill =be ~(maskthe & VALUE_MASK);union of * the existing set of states and maskArray[index] = maskArray[index] & m;the added state. * @param state } The state to add return maskArray;*/ } public staticvoid List<String> getStyleClassStrings(long[] maskArrayaddState(PseudoClass.State state) { if (maskArray == null || maskArray.length == 0) return Collections.EMPTY_LIST;} final Map<Long,String> stringMap = new HashMap<Long,String>(); /** * Remove the for (Map.Entry<String,Long> entry : maskBits.entrySet()) { stringMap.put(entry.getValue(), entry.getKey()); } final List<String> strings = new ArrayList<String>(); for(int index=0; index<maskArray.length; index++) { final long m = maskArray[index]; final long element = (m & ~VALUE_MASK);state to the current states. The result of the operation will be the relative complement of * the removed state in the existing set of states. This implies that removing a state that is not in the * existing set of states had no effect. * @param state The state to remove */ public void removeState(PseudoClass.State state) { for (int exp=0; exp < VALUE_BITS; exp++) { } /** final* longReset keythe =set elementof |states ((1Lto <<the exp) & m);empty set. */ if (keypublic !=void 0clear() { } /** @return The list final String value = stringMap.get(key); of PseudoClass.State that are represented by this States object */ public if (value != null) strings.add(value);List<PseudoClass.State> getStates() { } } /** Cannot create an instance of States except through PseudoClass static methods }*/ private }States() { return strings;} } } |
pseudo-class Node API
Code Block |
---|
/** * Used to notify that a pseudo-class has changed. Typically, this method is called from the invalidated * method of a property that is used as a pseudo-class. * <code> * private static final PseudoClass.State MY_PSEUDO_CLASS_STATE = PseudoClass.getState("my-state"); * * BooleanProperty myPseudoClassState = new BooleanPropertyBase(false) { * * @Override public void invalidated() { * pseudoClassStateChanged(MY_PSEUDO_CLASS_STATE); * } * * @Override public Object getBean() { * return MyControl.this; * } * * @Override public String getName() { * return "myPseudoClassState"; * } * }; * <code> * @param pseudoClass A bit mask that represents the pseudo-classpseudoClassState The PseudoClass.State that has changed. */ protected void pseudoClassStateChanged(longPseudoClass.State pseudoClasspseudoClassState) { // if there are styles that use any of the given pseudoClassespseudoClassState, mark this node as needing css update } /** * The pseudo-class state of a Node is the state pertaining to this class and all of its super classes. This method is * implemented by getting the pseudo-class state of the super class and then or'ing in the pseudo-class state of this class. * <code> * private static final longPseudoClass.State MY_PSEUDO_CLASS_STATE = PseudoClassStateBitMaskUtilities PseudoClass.getMaskgetState("my-pseudo-class-state"); * * @Override public long[] getPseudoClassStatePseudoClass.States getPseudoClassStates() { * long[] statePseudoClass.States states = super.getPseudoClassStategetPseudoClassStates(); * if (isMyPseudoClassState()) { PseudoClassStateBitMaskUtilitiesstates.setaddState(state, MY_PSEUDO_CLASS_STATE); } * return state; * } * <code> * @return long[]PseudoClass.States representing the pseudo-class state of this node and the state of its super classes. * */ public long[] getPseudoClassStatePseudoClass.States getPseudoClassStates() { PseudoClass.States states = PseudoClass.createStatesInstance(); ??[TBD - better to have a instance long[]member stateand =just new long[1];clear it?]?? if(isHover()) PseudoClassStateBitMaskUtilitiesstates.setaddState(state, HOVER_PSEUDOCLASS_STATE); if(isPressed()) PseudoClassStateBitMaskUtilitiesstates.setaddState(state, PRESSED_PSEUDOCLASS_STATE); if(isDisabled()) PseudoClassStateBitMaskUtilitiesstates.setaddState(state, DISABLED_PSEUDOCLASS_STATE); if(isFocused()) PseudoClassStateBitMaskUtilitiesstates.setaddState(state, FOCUSED_PSEUDOCLASS_STATE); if(impl_isShowMnemonics()) PseudoClassStateBitMaskUtilitiesstates.setaddState(state, SHOW_MNEMONICS_PSEUDOCLASS_STATE); return maskstates; } |
...