4

My application allows users to use custom CSS themes to style the interface. I have several pre-built "themes" available to choose from that are very simple, with only 3 properties.

Sample CSS:

.root{
    -fx-background: #325c81;
    -fx-default-button: #77a3ca;
    -fx-base: #a7c4dd;
}

The application has 3 ColorPicker controls that need to allow users to select a color for each of those properties and save back to the CSS file.

I have no problem with actually writing the CSS file, but I cannot find a way to parse the .css file in order to set the values of the ColorPicker controls with the values from the .css file.

Basic Program Flow

1) User selects a premade theme from ComboBox:

cboPresetTheme.valueProperty().addListener((observable, priorTheme, newTheme) -> {
                Utility.applyTheme(cboPresetTheme.getScene(), newTheme);
            });

2) The associated .css file is loaded and applied to the current Scene:

public static void applyTheme(Scene scene, Theme theme) {
    scene.getStylesheets().clear();

    File css = new File("themes/" + theme.getFileName());
    File fontFile = new File("themes/Font.css");

    scene.getStylesheets().addAll(
            css.toURI().toString(),
            fontFile.toURI().toString());
}

3) The 3 ColorPicker controls are updated with the values from the applied StyleSheet:

cpBackground.setValue(Color.valueOf(cssFileBackground));
cpBase.setValue(Color.valueOf(cssFileBase));
cpDefaultButton.setValue(Color.valueOf(cssFileDefaultButton));

While I have no problem with steps 1 & 2, I do not know how to process step 3.

I have looked at other CSS Parser libraries (thank you, Google) but they seem more geared toward stand CSS and don't support FX properties. The StackExchange question edit or parse FX-CSS file programmatically appears to be asking the same question but it was never successfully answered.

One answer suggests using CSS Parser to accomplish this, but as there is little to know documentation (and what is there is beyond my current comprehension level), I don't know where to begin.

I understand there may not be a standard API currently available to accomplish this, but I was hoping there may be a simple library or solution out there that I have been unable to find.

Community
  • 1
  • 1
Zephyr
  • 9,885
  • 4
  • 28
  • 63

1 Answers1

9

There are several ways you can tackle the conversion of a CSS declaration into a Color.

Style an auxiliar node

This is quite simple, but effective: The idea is that you could just style the background color of a node with the same css, and then set the colorPicker value with that color.

The only thing you need to take into account in this case is that the node is styled only when is added to a scene.

So you have to add the node to the scene. Adding a node with 0x0 size won't cause any issue, but maybe you don't want it to be there, so you can use an auxiliar scene.

public class CSSParsingApp extends Application {

    @Override
    public void start(Stage primaryStage) {
        ColorPicker cpBackground = new ColorPicker(retrieveColor("value1"));
        ColorPicker cpBase = new ColorPicker(retrieveColor("value2"));
        ColorPicker cpDefaultButton = new ColorPicker(retrieveColor("value3"));

        VBox root = new VBox(10, cpBackground, cpDefaultButton, cpBase);
        root.setAlignment(Pos.CENTER);

        Scene scene = new Scene(root, 300, 250);
        scene.getStylesheets().add(getClass().getResource("style.css").toExternalForm());

        primaryStage.setScene(scene);
        primaryStage.show();
    }

    private Color retrieveColor(String value) {
        Pane pane = new Pane();
        pane.getStyleClass().add(value);

        Scene sceneAux = new Scene(pane);
        sceneAux.getStylesheets().add(getClass().getResource("style.css").toExternalForm());
        pane.applyCss();
        return (Color) pane.getBackground().getFills().get(0).getFill();
    }

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

}

where style.css is:

.root {
    -fx-background: #325c81;
    -fx-default-button: #77a3ca;
    -fx-base: #a7c4dd;
}

.value1 {
    -fx-background-color: -fx-background;
}
.value2 {
    -fx-background-color: -fx-default-button;
}
.value3 {
    -fx-background-color: -fx-base;
}

Use StylableProperties

A similar, more elegant solution is found here. It uses StylableProperties to create a node, that you can style with a custom -named-color property, and then adds this helper node to the main scene.

Basically it is the same idea as the one above, maybe more clean, as you don't need to modify your css file.

Using CssToColorHelper, your code will be like this:

public class CSSParsingApp extends Application {

    private CssToColorHelper helper = new CssToColorHelper();

    @Override
    public void start(Stage primaryStage) {
        ColorPicker cpBackground = new ColorPicker();
        ColorPicker cpBase = new ColorPicker();
        ColorPicker cpDefaultButton = new ColorPicker();  

        VBox root = new VBox(10, cpBackground, cpDefaultButton, cpBase, helper);
        root.setAlignment(Pos.CENTER);

        Scene scene = new Scene(root, 300, 250);
        scene.getStylesheets().add(getClass().getResource("style.css").toExternalForm());

        cpBackground.setValue(getNamedColor("-fx-background"));
        cpDefaultButton.setValue(getNamedColor("-fx-default-button"));
        cpBase.setValue(getNamedColor("-fx-base"));

        primaryStage.setScene(scene);
        primaryStage.show();
    }

    private Color getNamedColor(String name) {
        helper.setStyle("-named-color: " + name + ";");
        helper.applyCss();

        return helper.getNamedColor();      
    }
    /**
     * @param args the command line arguments
     */
    public static void main(String[] args) {
        launch(args);
    }
}

where style.css is your css file:

.root {
    -fx-background: #325c81;
    -fx-default-button: #77a3ca;
    -fx-base: #a7c4dd;
}

Use JavaFX CssParser

If you are looking for a CSS parser, why don't you just use the one included in JavaFX, the one you actually use to apply styling to your app?

It is under javafx.css.CssParser, and it has been a part of the public API since JavaFX 9.

With it you can parse the CSS file and retrieve any parsed value easily.

 public class CSSParsingApp extends Application {

    @Override
    public void start(Stage primaryStage) {
        ColorPicker cpBackground = new ColorPicker();
        ColorPicker cpBase = new ColorPicker();
        ColorPicker cpDefaultButton = new ColorPicker();  

        VBox root = new VBox(10, cpBackground, cpDefaultButton, cpBase);
        root.setAlignment(Pos.CENTER);

        Scene scene = new Scene(root, 300, 250);
        scene.getStylesheets().add(getClass().getResource("style.css").toExternalForm());

        cpBackground.setValue(parseColor("-fx-background"));
        cpDefaultButton.setValue(parseColor("-fx-default-button"));
        cpBase.setValue(parseColor("-fx-base"));

        primaryStage.setScene(scene);
        primaryStage.show();
    }

    private Color parseColor(String property) {
        CssParser parser = new CssParser();
        try {
            Stylesheet css = parser.parse(getClass().getResource("style.css").toURI().toURL());
            final Rule rootRule = css.getRules().get(0); // .root
            return (Color) rootRule.getDeclarations().stream()
                .filter(d -> d.getProperty().equals(property))
                .findFirst()
                .map(d -> ColorConverter.getInstance().convert(d.getParsedValue(), null))
                .get();
        } catch (URISyntaxException | IOException ex) { }
        return Color.WHITE;
    }

    /**
     * @param args the command line arguments
     */
    public static void main(String[] args) {
        launch(args);
    }
}

where style.css is your css file:

.root {
    -fx-background: #325c81;
    -fx-default-button: #77a3ca;
    -fx-base: #a7c4dd;
}
Johan Kaving
  • 4,870
  • 1
  • 27
  • 21
José Pereda
  • 44,311
  • 7
  • 104
  • 132
  • Using the built-in CSSParser looks promising, thank you! However, I'm getting a `NullPointerException` on `Stylesheet css = parser.parse(Utility.class.getResource(cssFile).toURI().toURL());' My `cssFile` is being passed as a String to the `.css` file in question. – Zephyr Mar 05 '17 at 18:14
  • Make sure you have a valid `cssFile` under the same path as the `Utility` class. Also place an `ex.printStackTrace()` in the catch, so you can find out the reason of that exception. – José Pereda Mar 05 '17 at 18:17
  • I'm assuming it has something to do with `Utility.class` being null (the String checks out fine). I am putting the parser in a `Static` method so I can't use `getClass()`. – Zephyr Mar 05 '17 at 18:17
  • The `cssFile` string contains the full relative path to the file; will that not work? And the `e.printStackTrace()` only leads me to that line. Is there a way to find out which part of the line is causing the exception? – Zephyr Mar 05 '17 at 18:21
  • I suggest you use just the name of the css file, and place it under the same package path as `Utility` class. Or use an absolute path, and use `"/" + cssFile` as path to get the resource. – José Pereda Mar 05 '17 at 18:24
  • I can't actually place the `.css` into the package with `Utility` as the StyleSheets are distributed as external files. – Zephyr Mar 05 '17 at 18:27
  • Why don't you use the same approach you already have? Use your `css` file and then call `parser.parse(css.toURI().toURL())`. – José Pereda Mar 05 '17 at 18:30
  • I actually just caught that too :) Will give it a shot, thank you! – Zephyr Mar 05 '17 at 18:31
  • That did it. Thanks again, Jose! – Zephyr Mar 05 '17 at 18:37