I try to create a custom css style property for my component, with multiple color values, like the -fx-background-color. But I'm stuck, despite I define my CSS property similar to the -fx-background-color property, only the first color value is parsed from the CSS declaration. This is the CSS declaration:
.default-chart-theme {
-jfc-default-paint-sequence: red,white,green,blue,yellow,orange,gray;
}
And this is the CssMetadata declaration in the java class:
public final CssMetaData<StyleableChartViewer, Paint[]> DEFAULT_PAINT_SEQUENCE = new CssMetaData<>("-jfc-default-paint-sequence", PaintConverter.SequenceConverter.getInstance(), new Paint[] { Color.RED } )
{
@Override
public boolean isSettable(StyleableChartViewer styleable)
{
return !cssDefaultPaintSequence.isBound();
}
@Override
public StyleableProperty<Paint[]> getStyleableProperty(StyleableChartViewer styleable)
{
return cssDefaultPaintSequence;
}
};
public final SimpleStyleableObjectProperty<Paint[]> cssDefaultPaintSequence =
new SimpleStyleableObjectProperty<>(DEFAULT_PAINT_SEQUENCE, this, "cssDefaultPaintSequence", new Paint[] { Color.RED } );
in the getCssMetaData I also return this property and it is also parsed, but instead of a paint sequence, it is parsed only as a single color value.
I also get a warning when processing the css property:
WARNING: Caught 'java.lang.ClassCastException: class javafx.scene.paint.Color cannot be cast to class [Ljavafx.css.ParsedValue; (javafx.scene.paint.Color and [Ljavafx.css.ParsedValue; are in unnamed module of loader 'app')' while converting value for '-jfc-default-paint-sequence' from rule '*.default-chart-theme' in stylesheet
Any advice how to create such a CSS property is welcome. I tried to google and also tried to get some info using Perplexity about this issue, but I did not find anything useful to overcome this issue.
Thanks!
One solution is to create your own StyleConverter
implementation and quote the colors list in the CSS. By quoting the colors list, the converter will receive the value as a single string which can then be processed any way you want. Unfortunately, one significant downside to this approach is that variables (i.e., looked-up colors) won't be resolved and there's no way for you to resolve them from inside a style converter.
There may be a better approach. If there is, hopefully someone else will post an answer.
This example is only capable of parsing Color
, not any arbitrary Paint
. That's why this is only a "partial solution", though it should be possible to modify it to parse gradients and image patterns similar to how it works natively (not necessarily a trivial modification). In other words, it should be possible to make this approach as flexible as properties like -fx-background-color
, though without any variables being resolved.
Main.java
package com.example;
import java.util.Arrays;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.stage.Stage;
public class Main extends Application {
private static final String STYLESHEET =
"""
.colors-region {
-fx-colors: 'red,blue,green';
}
""";
@Override
public void start(Stage primaryStage) {
var region = new ColorsRegion();
region.colorsProperty().subscribe(val -> System.out.println(Arrays.toString(val)));
var scene = new Scene(region, 500, 300);
scene.getStylesheets().add("data:text/css," + STYLESHEET);
primaryStage.setScene(scene);
primaryStage.show();
}
}
ColorSequenceConverter.java
package com.example;
import java.util.stream.Stream;
import javafx.css.ParsedValue;
import javafx.css.StyleConverter;
import javafx.scene.paint.Color;
import javafx.scene.text.Font;
public class ColorSequenceConverter extends StyleConverter<String, Color[]> {
@Override
public Color[] convert(ParsedValue<String, Color[]> value, Font font) {
var string = value.getValue();
return Stream.of(string.split(",")).map(Color::web).toArray(Color[]::new);
}
}
ColorsRegion.java
package com.example;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javafx.beans.property.ObjectProperty;
import javafx.css.CssMetaData;
import javafx.css.SimpleStyleableObjectProperty;
import javafx.css.StyleableObjectProperty;
import javafx.css.StyleableProperty;
import javafx.geometry.HPos;
import javafx.geometry.VPos;
import javafx.scene.layout.Background;
import javafx.scene.layout.Region;
import javafx.scene.paint.Color;
public class ColorsRegion extends Region {
public ColorsRegion() {
getStyleClass().add("colors-region");
}
@Override
protected void layoutChildren() {
if (getChildren().isEmpty()) return;
double x = getInsets().getLeft();
double y = getInsets().getTop();
double w = getWidth() - getInsets().getRight() - x;
double h = getHeight() - getInsets().getBottom() - y;
double ry = y;
double rh = h / getChildren().size();
for (var child : getChildren()) {
layoutInArea(child, x, ry, w, rh, -1, HPos.CENTER, VPos.CENTER);
ry += rh;
}
}
private void updateRegions() {
getChildren().clear();
var colors = getColors();
if (colors != null) {
for (var color : colors) {
var region = new Region();
region.setBackground(Background.fill(color));
getChildren().add(region);
}
}
}
/* **************************************************************************
* *
* Properties *
* *
****************************************************************************/
// -- colors property
private final StyleableObjectProperty<Color[]> colors =
new SimpleStyleableObjectProperty<>(Css.COLORS, this, "colors") {
@Override
protected void invalidated() {
updateRegions();
}
};
public final void setColors(Color[] colors) {
this.colors.set(colors);
}
public final Color[] getColors() {
return colors.get();
}
public final ObjectProperty<Color[]> colorsProperty() {
return colors;
}
/* **************************************************************************
* *
* CSS Handling *
* *
****************************************************************************/
public static List<CssMetaData<?, ?>> getClassCssMetaData() {
return Css.META_DATA;
}
@Override
public List<CssMetaData<?, ?>> getCssMetaData() {
return getClassCssMetaData();
}
private static class Css {
private static final CssMetaData<ColorsRegion, Color[]> COLORS =
new CssMetaData<>("-fx-colors", new ColorSequenceConverter()) {
@Override
public boolean isSettable(ColorsRegion styleable) {
return !styleable.colors.isBound();
}
@Override
public StyleableProperty<Color[]> getStyleableProperty(ColorsRegion styleable) {
return styleable.colors;
}
};
private static final List<CssMetaData<?, ?>> META_DATA;
static {
var regionMetadata = Region.getClassCssMetaData();
var metadata = new ArrayList<CssMetaData<?, ?>>(regionMetadata.size() + 1);
metadata.addAll(Region.getClassCssMetaData());
metadata.add(COLORS);
META_DATA = Collections.unmodifiableList(metadata);
}
}
}
A StyleConverter
is responsible for taking a ParsedValue
and converting it into the type used by the JavaFX property. But a ParsedValue
represents the value as it was already parsed by the CSS parser. In other words, the value has already been parsed into an intermediate object. Additionally, a ParsedValue
may have its own StyleConverter
associated with it. That converter, if I'm not mistaken, always takes precedence over the one associated with a styleable property's CssMetaData
.
Which intermediate object the value is parsed into and which converter, if any, is associated with the parsed value is not customizable. The behavior is hard-coded within the parser. Examining the linked code, you can see what is going wrong in your attempt.
Your CSS property, -jfc-default-paint-sequence
, is not one of the specially-handled properties.
The value of your property is not a number, duration, hash, function, or URL. It's also not a known constant or a previously established variable. This means the value is treated as a simple string/possible color. However, only the first "term" is considered in this case. In your example, the first "term" is red
. All other "terms" (the other colors) are ignored.
The first "term" of your example's value is red
, which can be parsed into a Color
(specifically; not just any Paint
type). Therefore, the value is parsed into a Color
and returned wrapped in a single ParsedValue<Color, Color>
. This ParsedValue
has no associated StyleConverter
.
The PaintConverter.SequenceConverter
expects a ParsedValue<ParsedValue<?, Paint>[], Paint[]>
. So, a ClassCastException
occurs when trying to get the value of the ParsedValue
because it expects a ParsedValue<?, Paint>[]
but instead gets a Color
.
Since this behavior is hard-coded you unfortunately can't change it. It wouldn't really matter in this case if the parser handled arrays of paints, but it doesn't (interestingly, the parser does handle arrays of numbers for sizes).
Now, if the value cannot be parsed into a Color
then you get a ParsedValue<String, String>
with no StyleConverter
. That's what the "partial solution" above is taking advantage of. By quoting the value the CSS parser treats the entire thing as a single "term". That "term" cannot be parsed into a color. This results in the CSS metadata's StyleConverter
basically having full control over how the value is parsed.