javajavafxjavafx-css

How to add a custom CSS property to custom Label class?


I want to add a custom CSS integer property (in this example I use -fx-foo) to my custom Label. This is my code:

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javafx.application.Application;
import javafx.beans.property.IntegerProperty;
import javafx.css.CssMetaData;
import javafx.css.Styleable;
import javafx.css.StyleableIntegerProperty;
import javafx.css.converter.SizeConverter;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class NewMain extends Application {

    public static class FooLabel extends Label {

        private static final CssMetaData<FooLabel, Number> FOO_PROPERTY = new CssMetaData<FooLabel, Number>("-fx-foo",
                SizeConverter.getInstance(), 10) {

            @Override
            public boolean isSettable(FooLabel label) {
                return true;
            }

            @Override
            public StyleableIntegerProperty getStyleableProperty(FooLabel label) {
                return (StyleableIntegerProperty) label.fooProperty();
            }
        };

        private static final List<CssMetaData<? extends Styleable, ?>> CSS_META_DATA;

        static {
            List<CssMetaData<? extends Styleable, ?>> list = new ArrayList<>(Label.getClassCssMetaData());
            list.add(FOO_PROPERTY);
            CSS_META_DATA = Collections.unmodifiableList(list);
        }

        public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
            return CSS_META_DATA;
        }

        private final StyleableIntegerProperty foo = new StyleableIntegerProperty(10) {

            @Override
            public CssMetaData getCssMetaData() {
                return FOO_PROPERTY;
            }

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

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

        public FooLabel() {
            super();
            foo.addListener((observable, oldValue, newValue) -> {
                System.out.println("NEW VALUE:" + newValue);
            });
        }

        public IntegerProperty fooProperty() {
            return foo;
        }

        public void setFoo(int foo) {
            this.foo.set(foo);
        }

        public int getFoo() {
            return foo.get();
        }
    }

    /**************** MAIN APP  *****************/

    @Override
    public void start(Stage primaryStage) {
        var fooLabel = new FooLabel();
        fooLabel.getStyleClass().add("test");
        fooLabel.setText("abc");
        VBox root = new VBox(fooLabel);
        root.getStylesheets().add(NewMain.class.getResource("test.css").toExternalForm());
        Scene scene = new Scene(root, 100, 100);
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }
}

and CSS:

.test {
    -fx-foo: 100;
    -fx-background-color: yellow;
}

The code is compiled and when it works it doesn't throw any exceptions. The label is yellow. However, the foo property never changes, it seems that -fx-foo is just ignored. Could anyone say how to fix it?


Solution

  • Types that are styleable from CSS implement Styleable. In order for the CSS engine to know which properties are styleable, the Styleable must report its CSS metadata. It does this via its getCssMetaData() method. Note that's an instance method. If you have your own types with their own CSS metadata, then you have to override that method. The returned list should contain the CSS metadata for both the supertypes and your own type if all styleable properties are to continue to work properly.

    That's where the static getClassCssMetaData() methods defined by most styleable classes comes into play. It's not strictly required by the system, and in fact cannot be enforced by the compiler due to the method being static (there's no way in Java to say a type must have a static method). But it's basically part of the expected API and provides at least two functions:

    1. It makes it easy for subtypes to get the metadata of their supertype in a static way without reflection (note since CSS metadata is shared between all instances of a class, it makes sense to define it statically).

    2. It allows tools to inspect the CSS metadata without having to instantiate the class.

    The getCssMetaData() implementation typically delegates to the static method.

    However, you actually want to override Control#getControlCssMetaData() for types that inherit from Control. It serves the same function as Styleable#getCssMetaData(), but Control already provides a final implementation of the latter. That implementation combines the CSS metadata from Control#getControllCssMetaData() and SkinBase#getCssMetaData(), which allows skins to add their own separate styleable properties.

    So, you should just have to add the following to your FooLabel class:

    @Override
    public List<CssMetaData<?, ?>> getControlCssMetaData() {
      return getClassCssMetaData();
    }
    

    Note: The method is protected in Control, but the Labeled class makes it public.


    Full Example

    Source code

    FooLabel.java

    Implemented similarly to how the standard controls implement styleable properties. Though note it makes use of StyleablePropertyFactory for creating the CssMetaData.

    package com.example;
    
    import java.util.List;
    import javafx.beans.property.IntegerProperty;
    import javafx.css.CssMetaData;
    import javafx.css.SimpleStyleableIntegerProperty;
    import javafx.css.StyleableIntegerProperty;
    import javafx.css.StyleablePropertyFactory;
    import javafx.scene.Node;
    import javafx.scene.control.Label;
    
    public class FooLabel extends Label {
    
      private final StyleableIntegerProperty foo = new SimpleStyleableIntegerProperty(Css.FOO, this, "foo");
      public final void setFoo(int foo) { this.foo.set(foo); }
      public final int getFoo() { return foo.get(); }
      public final IntegerProperty fooProperty() { return foo; }
    
      public FooLabel() {
        init();
      }
    
      public FooLabel(String text) {
        super(text);
        init();
      }
    
      public FooLabel(String text, Node graphic) {
        super(text, graphic);
        init();
      }
    
      private void init() {
        getStyleClass().add("foo-label");
      }
    
      public static List<CssMetaData<?, ?>> getClassCssMetaData() {
        return Css.META_DATA;
      }
    
      @Override
      public List<CssMetaData<?, ?>> getControlCssMetaData() {
        return getClassCssMetaData();
      }
    
      private static class Css {
    
        private static final CssMetaData<FooLabel, Number> FOO;
        private static final List<CssMetaData<?, ?>> META_DATA;
    
        static {
          var factory = new StyleablePropertyFactory<FooLabel>(Label.getClassCssMetaData());
          FOO = factory.createSizeCssMetaData("-fx-foo", s -> s.foo);
          META_DATA = List.copyOf(factory.getCssMetaData());
        }
      }
    }
    

    Main.java

    package com.example;
    
    import javafx.application.Application;
    import javafx.scene.Scene;
    import javafx.scene.layout.StackPane;
    import javafx.stage.Stage;
    
    public class Main extends Application {
    
      private static final String STYLESHEET =
          """
          .foo-label {
            -fx-foo: 100;
          }
          """;
    
      @Override
      public void start(Stage primaryStage) throws Exception {
        var label = new FooLabel("Hello, World!");
        label.fooProperty().subscribe(value -> System.out.printf("foo = %d%n", value));
    
        var scene = new Scene(new StackPane(label), 500, 300);
        scene.getStylesheets().add("data:text/css," + STYLESHEET);
    
        primaryStage.setScene(scene);
        primaryStage.show();
      }
    
      public static void main(String[] args) {
        launch(Main.class);
      }
    }
    

    Output

    foo = 0
    foo = 100