Friday, May 24, 2013

CSS == Confusing Style Sheets ?

Hi there,

Finally I found some time to play around with CSS Pseudo classes and so called StyleableProperties in JavaFX. Because there is already a good description about this topics available on the web (OpenJFX Wiki) this post is more about examples of those topics than about the details. So to keep it short...

CSS PSEUDO CLASS
You might have seen something like follows when reading CSS files

.style-class {
    ...
}

.style-class:pseudo {
    ...
}

In the case above pseudo is a so called CSS pseudo class. The nice thing about these pseudo classes is that one could trigger them by using a setter on a property in JavaFX. Means if one calls the method setPseudo(true) in JavaFX, the component that uses the .style-class will now use the .style-class:pseudo for styling. This is really useful because you as a developer don't have to take care about switching the style on the component manually but JavaFX will take care about this.
As a simple example lets create a custom control by extending Region and create a CSS pseudo class for that control. This the code for the control.

public class CSSPseudo extends Region {
  private static final PseudoClass DEMO_PSEUDO_CLASS
    PseudoClass.getPseudoClass("demo");
  private BooleanProperty demo;

  public CSSPseudo() {      
    getStylesheets().add(getClass()
      .getResource("csspseudo.css").toExternalForm());
    getStyleClass().add("region");
    setPrefSize(50, 50);
  }

  public final boolean isDemo() {
    return null == demo ? false : demo.get();
  }
  public final void setDemo(final boolean ON) {
    demoProperty().set(ON);
  }
  public final BooleanProperty demoProperty() {
    if (null == demo) {
      demo = new BooleanPropertyBase(false) {
        @Override protected void invalidated() { 
          pseudoClassStateChanged(DEMO_PSEUDO_CLASS, get()); 
        }
        @Override public Object getBean() { return this; }
        @Override public String getName() { return "demo"; }
      };
    }
    return demo;
  }

}

As you can see in the code the BooleanProperty looks in principle like a standard JavaFX BooleanProperty except the demoProperty() method. In this method the magic happens that if the property is invalid the style of the control will change to :demo in the CSS file.
The CSS file than looks like follows

.region {
    -fx-background-color: red;
}
.region:demo {
    -fx-background-color: green;

}

To test the behavior lets create a little Application and toggle the demo property between true and false which should lead to change of the Regions color between red and green. Here is the Application code

public class CSSDemo extends Application {
  private boolean        toggle;
  private long           lastTimerCall;
  private AnimationTimer timer;
  private CSSPseudo      control;

  @Override public void init() {
    toggle = true;
    lastTimerCall = System.nanoTime();
    timer = new AnimationTimer() {
      @Override public void handle(long now) {
        if (now > lastTimerCall + 1_000_000_000l) {
          toggle ^= true;
          control.setDemo(toggle);
          lastTimerCall = now;
        }
      }
    };
    control = new CSSPseudo();
  }

  @Override public void start(Stage stage) {
    StackPane pane = new StackPane();
    pane.setPadding(new Insets(10, 10, 10, 10));
    pane.getChildren().add(control);

    Scene scene = new Scene(pane);

    stage.setScene(scene);
    stage.show();

    timer.start();
  }

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

If you start this application you should see a simple window with a rectangle that changes it's color between red and green every second.




Well and that's all it takes to switch a CSS style by using a JavaFX property...neat isn't it?

STYLEABLE PROPERTY
So now we now how to change a CSS style by using JavaFX properties but would it not be nice if one could set a CSS style (e.g. by loading a specific CSS file) and dependent on the CSS style a property in the code will set to the value given by the CSS style ?
And that's exactly what the StyleableProperties are for.
Means we would like to define the value of a JavaFX property by using a CSS variable.
Therefor it takes a bit more code...

First of all we now create a custom control by extending Control which means we also have to create a Skin for our control. So first of all we need the control class

public class CssStyleable extends Control {
  public static final Color     DEFAULT_CSS_COLOR = Color.RED;
  private ObjectProperty<Paint> cssColor;

  // ******************** Constructors **************************************
  public CssStyleable() {
    getStyleClass().add("css-styleable");
  }

  // ******************** Methods *******************************************
  public final Paint getCssColor() {
    return null == cssColor ? DEFAULT_CSS_COLOR : cssColor.get();
  }
  public final void setCssColor(Paint cssColor) {
    cssColorProperty().set(cssColor);
  }
  public final ObjectProperty<Paint> cssColorProperty() {
    if (null == cssColor) {
      cssColor = new StyleableObjectProperty<Paint>(DEFAULT_CSS_COLOR) {
        @Override public CssMetaData getCssMetaData() { 
          return StyleableProperties.CSS_COLOR
        }
        @Override public Object getBean() { return CssStyleable.this; }
        @Override public String getName() { return "cssColor"; }
      };
    }
    return cssColor;
  }

  // ******************** Style related *************************************
  @Override protected String getUserAgentStylesheet() {
    return getClass().getResource("cssstyleable.css").toExternalForm();
  }

  private static class StyleableProperties {
    private static final CssMetaData<CssStyleable, Paint> CSS_COLOR =
      new CssMetaData<CssStyleable, Paint>("-css-color"
        PaintConverter.getInstance(), DEFAULT_CSS_COLOR) {

      @Override public boolean isSettable(CssStyleable node) {
        return null == node.cssColor || !node.cssColor.isBound();
      }

      @Override public StyleableProperty<Paint> getStyleableProperty(
        CssStyleable node) {
        return (StyleableProperty) node.cssColorProperty();
      }

      @Override public Color getInitialValue(CssStyleable node) {
        return node.DEFAULT_CSS_COLOR;
      }
    };

  private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
  static {
    final List<CssMetaData<? extends Styleable, ?>> styleables = new ArrayList<>
      (Control.getClassCssMetaData());
       Collections.addAll(styleables, CSS_COLOR);
       STYLEABLES = Collections.unmodifiableList(styleables);
    }
  }

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

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

}

The next thing we need is the Skin class for our control which looks like this

public class CssStyleableSkin extends SkinBase<CssStyleable> implements Skin<CssStyleable> {
  private static final double PREFERRED_SIZE = 64;
  private Pane                pane;
  private Region              region;

  // ******************** Constructors **************************************
  public CssStyleableSkin(final CssStyleable CONTROL) {
    super(CONTROL);
    pane = new Pane();

    getSkinnable().setPrefSize(PREFERRED_SIZE, PREFERRED_SIZE);
    initGraphics();
    registerListeners();
  }

  private void initGraphics() {
    region = new Region();
    region.getStyleClass().setAll("region");
    region.setPrefSize(PREFERRED_SIZE, PREFERRED_SIZE);
    pane.getChildren().setAll(region);
    getChildren().setAll(pane);
  }

  private void registerListeners() {
    getSkinnable().cssColorProperty().addListener(observable -> { 
      handleControlPropertyChanged("CSS_COLOR"); } );
  }

  // ******************** Methods *******************************************
  protected void handleControlPropertyChanged(final String PROPERTY) {
    if ("CSS_COLOR".equals(PROPERTY)) {
      System.out.println("CSS color property changed to "
                         getSkinnable().getCssColor());
    }
  }

}

As you could see in the code we attach an InvalidationListener to the cssColorProperty() of the CssStyleable control. That means as soon as this property gets invalid (by being changed to another value) the InvalidationListener will print the new color on the console.

Now we need the CSS file (cssstyleable.css) that will be used for our control. In this case it's really small and looks like follows

.css-styleable {
    -fx-skin  : "CssStyleableSkin";
    -css-color: red;
}

.css-styleable .region {
    -fx-background-color: -css-color;

}

As you can see it only defines the Skin file that should be loaded for the control and a the CSS variable named -css-color which is set to red;

With all the files in place we now just need a little application that demonstrates the StyleableProperty. Therefor we will create an app that simply toggles the CSS style of the Region between red and green by calling region.setStyle()

KEEP IN MIND that this CSS inlining should be avoided in an application due to performance but for this demo it's ok to use it.

So the code for the demo Application looks like this

public class CSSDemo extends Application {
  private boolean        toggle;
  private long           lastTimerCall;
  private AnimationTimer timer;
  private CssStyleable   control;


  @Override public void init() {
    toggle = true;
    lastTimerCall = System.nanoTime();
    timer = new AnimationTimer() {
      @Override public void handle(long now) {
        if (now > lastTimerCall + 1_000_000_000l) {
          toggle ^= true;
          control.setStyle(toggle ? "-css-color: red;" : "-css-color: green;");
          lastTimerCall = now;
        }
      }
    };
    control = new CssStyleable();
  }

  @Override public void start(Stage stage) {
    StackPane pane = new StackPane();
    pane.setPadding(new Insets(10, 10, 10, 10));
    pane.getChildren().add(control);

    Scene scene = new Scene(pane);

    stage.setScene(scene);
    stage.show();

    timer.start();
  }

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

}

If you now start that application you should see the same as in the example before, a simple rectangle that changes it's color every second between red and green. In addition you should see something like follows on the the console

CSS color property changed to 0x008000ff
CSS color property changed to 0xff0000ff

CSS color property changed to 0x008000ff

And this indicates that the InvalidationListener in the Skin was triggered because the cssColorProperty() in the CssStyleable class was changed. So you could change a CSS style and the appropriate StyleableProperty in your code will be triggered so that you can react on that change. 

Like I mentioned at the beginning, this post won't explain all the details because there is a good explanation available but it will give you an easy example for the CSS pseudo class and StyleableProperty that you could try by yourself and use it for your controls etc.

And to be honest, I just needed to write it down somewhere so that I could take a look at it when I need it... :)

Keep coding...

No comments:

Post a Comment