Using CSS in JavaFX to keep the API clean

  • Post category:Java / javafx / UI

As a notorious skeptic, the usage of CSS for styling in JavaFX to me was something of a “yeah, nice, but we’ll see how it works out.”. There are some doubts in how CSS will work out when complete skins (like Synthetica for Swing) are created, in combination with third party controls. But last week I had a situation that won me over to the “it’s good” side.

A user of one of my controls, CalendarPicker in JFXtras, came to me and told me about their usage of the control. They had integrated it in their application and styled it Windows 8 alike using CSS. But, he said, our graphical designer wants the arrows of the month and year selector to be on either side of the value, and the value centered instead of left aligned. Shown below is the default UI for CalendarPicker and as you can see, it is not as the designer wants:

calendarpicker_usa

The CalendarPicker control is composed of many other controls; there are Labels and ToggleButtons (for the days), and on top are two ListSpinners (also a JFXtras control). ListSpinner is actually the first control I’ve written for JavaFX 2, a pretty long time ago already, and it already has the properties the designer needs in its API; arrow position, direction and value positioning. Some of their effects are shown below.

ListSpinner_styleable

But since the ListSpinner controls are nested in the CalendarPicker, these properties are not available on the API of CalendarPicker. The only Java way of allowing the user to control these properties, is by exposing them in CalendarPicker’s API, and forwarding it to the ListSpinners. So on CalendarPicker there would be properties like:

  • yearSpinnerArrowDirection
  • yearSpinnerArrowPosition
  • yearSpinnerValueAlignment
  • monthSpinnerArrowDirection
  • monthSpinnerArrowPosition
  • monthSpinnerValueAlignement

These methods are of course terrible if you consider that JavaFX tries to hide the actual visualization of a control, in order to keep its Java API clean. And even worse: these properties would be totally meaningless if the CalendarPicker would use a iPhone alike presentation (drums selectors), if it ever gets to run on touch devices.

So this request actually put focus on an existing API design flaw; these properties in ListSpinner’s API are inappropriate, ListSpinner may have different skins, who may not use arrows at all. So these properties should not be in the control’s API.

Styleable properties to the rescue! Styleable properties can be set using CSS, without exposing them via the Java API. That means that the properties can be removed from the ListSpinner control and moved into the skin, but also that they still can be set even if the control is embedded in another control. The idea is to introduce three properties like so:

.CalendarPicker .ListSpinner {
	-fxx-arrow-position:SPLIT;
	-fxx-arrow-DIRECTION:HORIZONTAL;
	-fxx-value-alignment:CENTER;
}

As shown above, these properties are available on ListSpinner and by using the CSS selectors the ListSpinners inside the CalendarPicker can easily be addressed.

Using styleable properties requires three steps:

  1. Create a styleable property.
  2. Create a binding record (called CssMetaData) to bind the CSS id to the property.
  3. Make the CSS engine aware of these properties.

Step 1: create a styleable property (JavaFX 2.2)
The first step is to create a StyleableProperty, there are a number of predefined types, but for our properties an Object version is needed, since we will be using enums.

public ObjectProperty<ArrowPosition> arrowPositionProperty() {
	return this.arrowPositionObjectProperty;
}
final private StyleableObjectProperty<ArrowPosition> arrowPositionObjectProperty = new StyleableObjectProperty<ArrowPosition>(ArrowPosition.TRAILING) {

	@Override
	public StyleableProperty<ListSpinnerCaspianSkin,ArrowPosition> getStyleableProperty() {
		return ARROW_POSITION;
	}

	@Override
	public Object getBean() {
		return ListSpinnerCaspianSkin.this;
	}

	@Override
	public String getName() {
		return "arrowPosition";
	}

	@Override public void invalidated() {
		// code here for reacting to changes
	}
};
public ArrowPosition getArrowPosition() {
	return this.arrowPositionObjectProperty.getValue();
}
public void setArrowPosition(ArrowPosition value) {
	this.arrowPositionObjectProperty.setValue(value);
}
public ListSpinnerCaspianSkin<T> withArrowPosition(ArrowPosition value) {
	setArrowPosition(value);
	return this;
}
public enum ArrowPosition {LEADING, TRAILING, SPLIT}

Note the use of a special styleable property implementation, which does not have the same constructors as normal properties, therefor a number of methods must be overridden.

Step 2: create a CssMetaData record (JavaFX 2.2)

This is actually the most important part; here the identifier in CSS is connected to the property, and the way the CSS value string is converted to the property is defined. JavaFX offers a handy EnumConverter to do this for us.

private static final StyleableProperty<ListSpinnerCaspianSkin,ArrowPosition> ARROW_POSITION
	= new StyleableProperty<ListSpinnerCaspianSkin,ArrowPosition>(
		"-fxx-arrow-position",
		new EnumConverter<ArrowPosition>(ArrowPosition.class),
		ArrowPosition.TRAILING) {

	@Override
	public boolean isSettable(ListSpinnerCaspianSkin owner) {
		return !owner.arrowPositionObjectProperty.isBound();
	}

	@Override
	public WritableValue<ArrowPosition> getWritableValue(ListSpinnerCaspianSkin owner) {
		return owner.arrowPositionProperty();
	}
};

Step 3: make the CSS engine aware of the properties (JavaFX 2.2)

In JavaFX 2.2 the overriding of an internal and deprecated method is required, it comes down to getting the list of styleable properties from the parent, adding our own and returning that list.

@Override @Deprecated
public List<StyleableProperty> impl_getStyleableProperties() {
	if (STYLEABLES == null) {
		final List<StyleableProperty> styleables = new ArrayList<StyleableProperty>(super.impl_getStyleableProperties());
		Collections.addAll(styleables, ARROW_POSITION, ARROW_DIRECTION, VALUE_ALIGNMENT);
		STYLEABLES = Collections.unmodifiableList(styleables);
	}
	return STYLEABLES;
}
private static List<StyleableProperty> STYLEABLES;

So far JavaFX 2.2. In JavaFX 8.0 similar steps are needed, but beside the API now being public, there are some other changes.

Step 1: create a styleable property (JavaFX 8.0)

This is part is identical to the 2.2 code, except that classes are in different packages. This specific implementation uses lazy loading of the property, meaning the getter will return a default hard coded value until someone actually accesses the property.

public final ObjectProperty<ArrowPosition> arrowPositionProperty() {
	if (arrowPosition == null) {
		arrowPosition = new StyleableObjectProperty<ArrowPosition>(ArrowPosition.TRAILING) {

			@Override public CssMetaData<ListSpinner,ArrowPosition> getCssMetaData() {
				return StyleableProperties.ARROW_POSITION;
			}
			@Override public Object getBean() {
				return ListSpinnerCaspianSkin.this;
			}
			@Override public String getName() {
				return "arrowPosition";
			}
			@Override public void invalidated() {
				// code here for reacting to changes
			}
		};
	}
	return arrowPosition;
}
private ObjectProperty<ArrowPosition> arrowPosition = null;
public final void setArrowPosition(ArrowPosition value) {
	arrowPositionProperty().set(value);
}
public final ArrowPosition getArrowPosition() {
	return arrowPosition == null ? ArrowPosition.TRAILING : arrowPosition.get();
}
public final ListSpinnerCaspianSkin<T> withArrowPosition(ArrowPosition value) {
	setArrowPosition(value);
	return this;
}
public enum ArrowPosition {LEADING, TRAILING, SPLIT}

Step 2: create a CssMetaData record (JavaFX 8.0)

One important change is that it is no longer allowed to define styleable properties on skins, because skins no longer implement the Styleable interface. This of course is strange, since it seems very natural to me that properties intended for visualization are defined in a skin and not in the control. After all, there is no guarantee that all skins use the same visual elements, for example the arrows we’re styling in this blog post do not exist in the drum skin of iOS date picker. Luckily there is an acceptable solution: define the styleable properties in the skin, but let them pretend to be in the control. This involves casting the skin of the control, but because the casting happens in the skin itself, it is ok; apparently that skin is being used by the control.

private static class StyleableProperties {
	private static final CssMetaData<ListSpinner, ArrowPosition> ARROW_POSITION
		= new CssMetaData<ListSpinner, ArrowPosition>("-fxx-arrow-position",
			new EnumConverter<ArrowPosition>(ArrowPosition.class),
			ArrowPosition.TRAILING ) {

		@Override public boolean isSettable(ListSpinner n) {
			return !((ListSpinnerCaspianSkin)n.getSkin()).arrowPositionProperty().isBound();
		}
		@Override public StyleableProperty<ArrowPosition> getStyleableProperty(ListSpinner n) {
			return (StyleableProperty<ArrowPosition>)((ListSpinnerCaspianSkin)n.getSkin()).arrowPositionProperty();
		}
	};

	// more CssMetaData definitions here 

	private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
	static {
		final List<CssMetaData<? extends Styleable, ?>> styleables = new ArrayList<CssMetaData<? extends Styleable, ?>>(SkinBase.getClassCssMetaData());
		styleables.add(ARROW_POSITION);
		styleables.add(ARROW_DIRECTION);
		styleables.add(VALUE_ALIGNMENT);
		STYLEABLES = Collections.unmodifiableList(styleables);
	}
}

Step 3: make the CSS engine aware of the properties (JavaFX 8.0)

The way the CssMetaData classes are made available to the CSS engine has changed, no longer overriding an internal method.

/**
 * @return The CssMetaData associated with this class, which may include the
 * CssMetaData of its super classes.
 */
public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
	return StyleableProperties.STYLEABLES;
}

/**
 * This method should delegate to {@link Node#getClassCssMetaData()} so that
 * a Node's CssMetaData can be accessed without the need for reflection.
 * @return The CssMetaData associated with this node, which may include the
 * CssMetaData of its super classes.
 */
public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() {
	return getClassCssMetaData();
}

So, does it work?
Well, yes, it does…

CalendarPicker_CSS

Some final thoughts…

  • First, I really like this way of being able to style controls, doing this the old fashion way via Java API would have been highly upsetting.
  • The fact that styleable properties in 8.0 are not possible on skins anymore seems wrong. The workaround is more than acceptable though, so this is not really an issue.
  • Looking at the code, I wonder why there are separate StyleableXXXProperties classes. It seems to me all the CSS integration information is present in the CssMetaData class, so can’t we just use normal properties and call their setter?

This Post Has 12 Comments

  1. tbeernot

    Hard to tell like this, but post your problem on the JavaFX developer mailing list. David will most certainly be interested in any issues involving the CSS API.

      1. Tom

        I agree that your code seems ok. You could compare it to, for example, Label. It also has a styleable insets property for padding. Copy the padding code over and see if that runs.

  2. wzberger

    Tried something similar to get Insets and failed:
    “WARNING: Failed to set css [CSSProperty {property: -fx-my-insets, converter: InsetsConverter, initalValue: Insets [top=0.0, right=0.0, bottom=0.0, left=0.0], inherits: false, subProperties: []}]”

    1. tbeernot

      I’m getting loads of those warnings too, but they do not interfere with my controls.

      1. wzberger

        A bit later a related ClassCastException is thrown. But for Boolean values it works without any issues – so to me it looks like a bug.

  3. Jonathan Giles

    Styleable properties are still possible from skins. Refer to TreeCellSkin for example.

    1. tbeernot

      Well, they are possible as long as you make the CssMetaData pretend it is doing its thing on the control, otherwise generics will start to complain that the skin does not implement the Skinnable interface.
      The TreeCellSkin does exactly what I do above: CssMetaData on the control and then via the control get the skin and cast it. So in 8.0 the styling is actually done on the control, not the skin.

  4. Bill

    Hello,

    1. How do you change the arrow style from Java code? Not everyone want to use CSS.
    2. How does the user of your library know about what he can change? I mean the API version is self documenting (code completion and JavaDoc) but doesn’t this CSS approach need a separate document (bad)?

    1. tbeernot

      Nothing is without drawbacks 🙂
      1a. you can still set the properties through the Java API directly on the skin.
      1b. use the setStyle(“…”) method on the control
      2. I have added the information in the JavaDoc of the control, but yes, that is putting skin information in the control. This still poses a challenge. But the same goes for a lot of the other CSS styleable properties of JFX controls. Similar to HTML.

Leave a Reply to Jonathan Giles Cancel reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.