Wednesday, September 17, 2014

JavaFX bindings and tooltips

In this post, I will show two things:
  • How to use JavaFX bindings to set a tooltip
  • How to display tooltips if the widget is disbled
This is our GUI:

Pretty simple so far. Here's the code:

package prv.rli.codetest;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.layout.GridPane;
import javafx.stage.Stage;

public class TooltipOnDisabledButton extends Application {

    @Override
    public void start(Stage primaryStage) throws Exception {
        GridPane grid = new GridPane();
        grid.setHgap(20);

        Button button = new Button("Mystery");
        grid.add(button, 0, 0);
       
        CheckBox checkbox = new CheckBox("enabled");
        grid.add(checkbox, 1, 0);
       
        Scene scene = new Scene(grid);
        primaryStage.setScene(scene);       
        primaryStage.show();
    }
   
    public static void main(String[] args) {
        launch(args);
    }
}


Next, I want the "Mystery" button to be enabled/disabled if the checkbox is checked/unchecked.

Thanks to JavaFX bindings this can be done pretty easily, by adding this line right before the scene is created:

button.disableProperty().bind(checkbox.selectedProperty().not());

What this means is that the disableProperty of the button is now kept in sync with the (negated) selectedProperty of the checkbox.

And now, I also want the Button to display a Tooltip which will show the user how to enable/disable the button. This could be done by installing a ChangeListener to the selectedProperty() of the checkbox, but since we want to show (off) bindings, let's create a binding that will take a BooleanProperty and produce a Tooltip.

The whole thing then looks like this:

package prv.rli.codetest;

import javafx.application.Application;
import javafx.beans.binding.ObjectBinding;
import javafx.beans.property.BooleanProperty;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.GridPane;
import javafx.stage.Stage;

public class TooltipOnDisabledButton extends Application {

    @Override
    public void start(Stage primaryStage) throws Exception {
        GridPane grid = new GridPane();
        grid.setHgap(20);

        Button button = new Button("Mystery");
        grid.add(button, 0, 0);
       
        CheckBox checkbox = new CheckBox("enabled");
        grid.add(checkbox, 1, 0);
       
        button.disableProperty().bind(checkbox.selectedProperty().not());
        button.tooltipProperty().bind(new TooltipBinding(checkbox.selectedProperty()));
       
        Scene scene = new Scene(grid);
        primaryStage.setScene(scene);       
        primaryStage.show();
    }
   
    private static class TooltipBinding extends ObjectBinding<Tooltip> {
           
        BooleanProperty enabled;
       
        public TooltipBinding(BooleanProperty enabled) {
            bind(enabled);
            this.enabled = enabled;
        }

        @Override
        protected Tooltip computeValue() {
            if (enabled.get()) {
                return new Tooltip("To disable this button, uncheck the box to right.");
            }
            else {
                return new Tooltip("To enable this button, check the box to right.");
            }
        }
    }
   
    public static void main(String[] args) {
        launch(args);
    }
}


The TooltipBinding produces a new Tooltip (we could also just create the two tooltips and return them, saving the garbage collector some work) whenever the bound property is changed. 

And it works like a charm -- well almost. When the button is disabled, no tooltip is displayed. The reason for this is documented in [1] and [2]: Disabled nodes are not sent any mouse events.
To circumvent this, we must wrap the button:
        Button button = new Button("Mystery");
        SplitPane wrapper = new SplitPane();
        wrapper.getItems().add(button);
        grid.add(wrapper, 0, 0);

...
        wrapper.tooltipProperty().bind(new TooltipBinding(checkbox.selectedProperty()));

Note that the wrapper has to be a Control in order to have a tooltipProperty(). The problem with this solution is that we now have an additional border (from the SplitPane) around our button. This can easily be amended by


        wrapper.setStyle("-fx-background-color: transparent");

It seems unnatural howeve to introduce a SplitPane as a Tooltip-Wrapper. A simple pane would be the more natural choice. A Pane however has no tooltip property, therefore we must use Tooltip.install() and install a single tooltip whose textProperty() is bound:

package prv.rli.codetest;

import javafx.application.Application;
import javafx.beans.binding.ObjectBinding;
import javafx.beans.property.BooleanProperty;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Pane;
import javafx.stage.Stage;

public class TooltipOnDisabledButton extends Application {

    @Override
    public void start(Stage primaryStage) throws Exception {
        GridPane grid = new GridPane();
        grid.setHgap(20);

        Button button = new Button("Mystery");
        Pane wrapper = new Pane();
        wrapper.getChildren().add(button);
        grid.add(wrapper, 0, 0);
       
        CheckBox checkbox = new CheckBox("enabled");
        grid.add(checkbox, 1, 0);
       
        button.disableProperty().bind(checkbox.selectedProperty().not());
        Tooltip tooltip = new Tooltip();
        tooltip.textProperty().bind(new TooltipBinding(checkbox.selectedProperty()));
        Tooltip.install(wrapper, tooltip);
       
        Scene scene = new Scene(grid);
        primaryStage.setScene(scene);       
        primaryStage.show();
    }
   
    private static class TooltipBinding extends ObjectBinding<String> {
           
        BooleanProperty enabled;
       
        public TooltipBinding(BooleanProperty enabled) {
            bind(enabled);
            this.enabled = enabled;
        }

        @Override
        protected String computeValue() {
            if (enabled.get()) {
                return "To disable this button, uncheck the box to the right.";
            }
            else {
                return "To enable this button, check the box to the right.";
            }
        }
    }
   
    public static void main(String[] args) {
        launch(args);
    }
}


Note that the TooltipBinding now generates a String instead of a whole tooltip.

2 comments:

  1. Instead of using the TooltipBinding class, you could use the Bindings.when().then().otherwise() approach:


    tooltip.textProperty().bind(
    Bindings.when(checkbox.selectedProperty())
    .then("To disable this button, uncheck the box to the right.")
    .otherwise("To enable this button, check the box to the right."));

    ReplyDelete
  2. how to disableBinding through BooleanProperty TRUE OR FALSE

    ReplyDelete