Display custom color dialog directly- JavaFX ColorPicker

Ruturaj Patil picture Ruturaj Patil · Nov 27, 2014 · Viewed 9.2k times · Source


I need to show a "continuous" color palette for color selection inside a ContextMenu. Similar to CustomColorDialog that pops up on ColorPicker.
Is there a different class for this purpose or is it possible to work around by extending ColorPicker and showing directly CustomColorDialog instead of first showing ColorPicker.

ColorPicker JavaFX 8

TIA

Answer

José Pereda picture José Pereda · Nov 28, 2014

For starters, com.sun.javafx.scene.control.skin.CustomColorDialog is private API, and it's not advisable to use it, as it may change in the future without notice.

Besides, it is a Dialog, what means you can't embed it into a ContextMenu, it has its own window and it's modal.

This is a short example of using this (very big, not customizable) dialog in your application, without using a ColorPicker.

@Override
public void start(Stage primaryStage) {
    Button btn = new Button();
    btn.setText("Open Custom Color Dialog");
    btn.setOnAction(e -> {
        CustomColorDialog dialog = new CustomColorDialog(primaryStage.getOwner());
        dialog.show();
    });

    Scene scene = new Scene(new StackPane(btn), 300, 250);

    primaryStage.setTitle("CustomColorDialog");
    primaryStage.setScene(scene);
    primaryStage.show();
}

You'll get the dialog, but you won't get any possibility to send a custom color or retrieve the selected color, since properties like customColorProperty() are only accesible within the com.sun.javafx.scene.control.skin package.

So we need another way to implement our custom color selector. If you have a look at the source code of CustomColorDialog you'll see that it's relatively a simple control, and most important, almost based on public API: panes, regions and color.

Trying to put all in a ContextMenu could be overkilling, so I've come up with this basic example, where I'll just use the left part of the dialog, displaying the central bar on top. Most of the code is from the class. The CSS styling was also taken from modena.css (under custom-color-dialog CSS selector), but was customized as some of the nodes were rotated 90º.

This is a short version of CustomColorDialog class:

public class MyCustomColorPicker extends VBox {

    private final ObjectProperty<Color> currentColorProperty = 
        new SimpleObjectProperty<>(Color.WHITE);
    private final ObjectProperty<Color> customColorProperty = 
        new SimpleObjectProperty<>(Color.TRANSPARENT);

    private Pane colorRect;
    private final Pane colorBar;
    private final Pane colorRectOverlayOne;
    private final Pane colorRectOverlayTwo;
    private Region colorRectIndicator;
    private final Region colorBarIndicator;
    private Pane newColorRect;

    private DoubleProperty hue = new SimpleDoubleProperty(-1);
    private DoubleProperty sat = new SimpleDoubleProperty(-1);
    private DoubleProperty bright = new SimpleDoubleProperty(-1);

    private DoubleProperty alpha = new SimpleDoubleProperty(100) {
        @Override protected void invalidated() {
            setCustomColor(new Color(getCustomColor().getRed(), getCustomColor().getGreen(), 
                    getCustomColor().getBlue(), clamp(alpha.get() / 100)));
        }
    };

    public MyCustomColorPicker() {

        getStyleClass().add("my-custom-color");

        VBox box = new VBox();

        box.getStyleClass().add("color-rect-pane");
        customColorProperty().addListener((ov, t, t1) -> colorChanged());

        colorRectIndicator = new Region();
        colorRectIndicator.setId("color-rect-indicator");
        colorRectIndicator.setManaged(false);
        colorRectIndicator.setMouseTransparent(true);
        colorRectIndicator.setCache(true);

        final Pane colorRectOpacityContainer = new StackPane();

        colorRect = new StackPane();
        colorRect.getStyleClass().addAll("color-rect", "transparent-pattern");

        Pane colorRectHue = new Pane();
        colorRectHue.backgroundProperty().bind(new ObjectBinding<Background>() {

            {
                bind(hue);
            }

            @Override protected Background computeValue() {
                return new Background(new BackgroundFill(
                        Color.hsb(hue.getValue(), 1.0, 1.0), 
                        CornerRadii.EMPTY, Insets.EMPTY));

            }
        });            

        colorRectOverlayOne = new Pane();
        colorRectOverlayOne.getStyleClass().add("color-rect");
        colorRectOverlayOne.setBackground(new Background(new BackgroundFill(
                new LinearGradient(0, 0, 1, 0, true, CycleMethod.NO_CYCLE, 
                new Stop(0, Color.rgb(255, 255, 255, 1)), 
                new Stop(1, Color.rgb(255, 255, 255, 0))), 
                CornerRadii.EMPTY, Insets.EMPTY)));

        EventHandler<MouseEvent> rectMouseHandler = event -> {
            final double x = event.getX();
            final double y = event.getY();
            sat.set(clamp(x / colorRect.getWidth()) * 100);
            bright.set(100 - (clamp(y / colorRect.getHeight()) * 100));
            updateHSBColor();
        };

        colorRectOverlayTwo = new Pane();
        colorRectOverlayTwo.getStyleClass().addAll("color-rect");
        colorRectOverlayTwo.setBackground(new Background(new BackgroundFill(
                new LinearGradient(0, 0, 0, 1, true, CycleMethod.NO_CYCLE, 
                new Stop(0, Color.rgb(0, 0, 0, 0)), new Stop(1, Color.rgb(0, 0, 0, 1))), 
                CornerRadii.EMPTY, Insets.EMPTY)));
        colorRectOverlayTwo.setOnMouseDragged(rectMouseHandler);
        colorRectOverlayTwo.setOnMousePressed(rectMouseHandler);

        Pane colorRectBlackBorder = new Pane();
        colorRectBlackBorder.setMouseTransparent(true);
        colorRectBlackBorder.getStyleClass().addAll("color-rect", "color-rect-border");

        colorBar = new Pane();
        colorBar.getStyleClass().add("color-bar");
        colorBar.setBackground(new Background(new BackgroundFill(createHueGradient(), 
                CornerRadii.EMPTY, Insets.EMPTY)));

        colorBarIndicator = new Region();
        colorBarIndicator.setId("color-bar-indicator");
        colorBarIndicator.setMouseTransparent(true);
        colorBarIndicator.setCache(true);

        colorRectIndicator.layoutXProperty().bind(
            sat.divide(100).multiply(colorRect.widthProperty()));
        colorRectIndicator.layoutYProperty().bind(
            Bindings.subtract(1, bright.divide(100)).multiply(colorRect.heightProperty()));
        colorBarIndicator.layoutXProperty().bind(
            hue.divide(360).multiply(colorBar.widthProperty()));
        colorRectOpacityContainer.opacityProperty().bind(alpha.divide(100));

        EventHandler<MouseEvent> barMouseHandler = event -> {
            final double x = event.getX();
            hue.set(clamp(x / colorRect.getWidth()) * 360);
            updateHSBColor();
        };

        colorBar.setOnMouseDragged(barMouseHandler);
        colorBar.setOnMousePressed(barMouseHandler);

        newColorRect = new Pane();
        newColorRect.getStyleClass().add("color-new-rect");
        newColorRect.setId("new-color");
        newColorRect.backgroundProperty().bind(new ObjectBinding<Background>() {
            {
                bind(customColorProperty);
            }
            @Override protected Background computeValue() {
                return new Background(new BackgroundFill(customColorProperty.get(), CornerRadii.EMPTY, Insets.EMPTY));
            }
        });

        colorBar.getChildren().setAll(colorBarIndicator);
        colorRectOpacityContainer.getChildren().setAll(colorRectHue, colorRectOverlayOne, colorRectOverlayTwo);
        colorRect.getChildren().setAll(colorRectOpacityContainer, colorRectBlackBorder, colorRectIndicator);
        VBox.setVgrow(colorRect, Priority.SOMETIMES);
        box.getChildren().addAll(colorBar, colorRect, newColorRect);

        getChildren().add(box);

        if (currentColorProperty.get() == null) {
            currentColorProperty.set(Color.TRANSPARENT);
        }
        updateValues();

    }

    private void updateValues() {
        hue.set(getCurrentColor().getHue());
        sat.set(getCurrentColor().getSaturation()*100);
        bright.set(getCurrentColor().getBrightness()*100);
        alpha.set(getCurrentColor().getOpacity()*100);
        setCustomColor(Color.hsb(hue.get(), clamp(sat.get() / 100), 
                clamp(bright.get() / 100), clamp(alpha.get()/100)));
    }

    private void colorChanged() {
        hue.set(getCustomColor().getHue());
        sat.set(getCustomColor().getSaturation() * 100);
        bright.set(getCustomColor().getBrightness() * 100);
    }

    private void updateHSBColor() {
        Color newColor = Color.hsb(hue.get(), clamp(sat.get() / 100), 
                        clamp(bright.get() / 100), clamp(alpha.get() / 100));
        setCustomColor(newColor);
    }

    @Override 
    protected void layoutChildren() {
        super.layoutChildren();            
        colorRectIndicator.autosize();
    }

    static double clamp(double value) {
        return value < 0 ? 0 : value > 1 ? 1 : value;
    }

    private static LinearGradient createHueGradient() {
        double offset;
        Stop[] stops = new Stop[255];
        for (int x = 0; x < 255; x++) {
            offset = (double)((1.0 / 255) * x);
            int h = (int)((x / 255.0) * 360);
            stops[x] = new Stop(offset, Color.hsb(h, 1.0, 1.0));
        }
        return new LinearGradient(0f, 0f, 1f, 0f, true, CycleMethod.NO_CYCLE, stops);
    }

    public void setCurrentColor(Color currentColor) {
        this.currentColorProperty.set(currentColor);
        updateValues();
    }

    Color getCurrentColor() {
        return currentColorProperty.get();
    }

    final ObjectProperty<Color> customColorProperty() {
        return customColorProperty;
    }

    void setCustomColor(Color color) {
        customColorProperty.set(color);
    }

    Color getCustomColor() {
        return customColorProperty.get();
    }
}

This is the color.css file:

.context-menu{
    -fx-background-color: derive(#ececec,26.4%);
}
.menu-item:focused {
    -fx-background-color: transparent;
}

/* CUSTOM COLOR */

.my-custom-color {
    -fx-background-color: derive(#ececec,26.4%);
    -fx-padding: 1.25em;
    -fx-spacing: 1.25em;
    -fx-min-width: 20em;
    -fx-pref-width: 20em;
    -fx-max-width: 20em;
}

.my-custom-color:focused,
.my-custom-color:selected {
    -fx-background-color: transparent;
}

.my-custom-color > .color-rect-pane {
    -fx-spacing: 0.75em;
    -fx-pref-height: 16.666667em;
    -fx-alignment: top-left;
    -fx-fill-height: true;
}

.my-custom-color .color-rect-pane .color-rect {
    -fx-min-width: 16.666667em;
    -fx-min-height: 16.666667em;
}

.my-custom-color .color-rect-pane .color-rect-border {
    -fx-border-color: derive(#ececec, -20%);
}

.my-custom-color > .color-rect-pane #color-rect-indicator {
    -fx-background-color: null;
    -fx-border-color: white;
    -fx-border-radius: 0.4166667em;
    -fx-translate-x: -0.4166667em;
    -fx-translate-y: -0.4166667em;
    -fx-pref-width: 0.833333em;
    -fx-pref-height: 0.833333em;
    -fx-effect: dropshadow(three-pass-box, black, 2, 0.0, 0, 1);
}

.my-custom-color > .color-rect-pane > .color-bar {
    -fx-min-height: 1.666667em;
    -fx-min-width: 16.666667em;
    -fx-max-height: 1.666667em;
    -fx-border-color: derive(#ececec, -20%);
}

.my-custom-color > .color-rect-pane > .color-bar > #color-bar-indicator {
    -fx-border-radius: 0.333333em;
    -fx-border-color: white;
    -fx-effect: dropshadow(three-pass-box, black, 2, 0.0, 0, 1);
    -fx-pref-height: 2em;
    -fx-pref-width: 0.833333em;
    -fx-translate-y: -0.1666667em;
    -fx-translate-x: -0.4166667em;
}

.my-custom-color .transparent-pattern {
    -fx-background-image: url("pattern-transparent.png"); 
    -fx-background-repeat: repeat;
    -fx-background-size: auto;
}

.my-custom-color .color-new-rect {
    -fx-min-width: 10.666667em;
    -fx-min-height: 1.75em;
    -fx-pref-width: 10.666667em;
    -fx-pref-height: 1.75em;
    -fx-border-color: derive(#ececec, -20%);
}

The image can be found here.

And finally, our application class.

public class CustomColorContextMenu extends Application {

    private final ObjectProperty<Color> sceneColorProperty = 
        new SimpleObjectProperty<>(Color.WHITE);

    @Override
    public void start(Stage primaryStage) {

        Rectangle rect = new Rectangle(400,400);
        rect.fillProperty().bind(sceneColorProperty);

        Scene scene = new Scene(new StackPane(rect), 400, 400);
        scene.getStylesheets().add(getClass().getResource("color.css").toExternalForm());
        scene.setOnMouseClicked(e->{
            if(e.getButton().equals(MouseButton.SECONDARY)){
                MyCustomColorPicker myCustomColorPicker = new MyCustomColorPicker();
                myCustomColorPicker.setCurrentColor(sceneColorProperty.get());

                CustomMenuItem itemColor = new CustomMenuItem(myCustomColorPicker);
                itemColor.setHideOnClick(false);
                sceneColorProperty.bind(myCustomColorPicker.customColorProperty());
                ContextMenu contextMenu = new ContextMenu(itemColor);
                contextMenu.setOnHiding(t->sceneColorProperty.unbind());
                contextMenu.show(scene.getWindow(),e.getScreenX(),e.getScreenY());
            }
        });

        primaryStage.setTitle("Custom Color Selector");
        primaryStage.setScene(scene);
        primaryStage.show();
    }

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

}

Note the use of CustomMenuItem to allow clicking on the color selectors without closing the context menu. To close it just click anywhere outside the popup window.

This is how it looks like:

Custom Color Selector

Based on this custom dialog, you can improve it and add the functionality you may need.