Wednesday, September 24, 2014

Debugging JavaFX with Linux/X11

When debugging a JavaFX application under Linux there is the Problem of mouse grabbing, i.e. the application under test grabs the mouse and no other application can receive normal GUI events.

This of course makes debugging nearly impossible. After some research I've found a solution in the issue tracker of JavaFX: You have to set the system property (aka VM arguments) -Dglass.disableGrab=true.

Like this in eclipse:

BTW, according to the Java bug database, for AWT applications the same can be achieved by using -Dsun.awt.disablegrab=true but I haven't tried that.

Important caveat:  If you start your application this way, drag and drop will no longer work in your application.

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.