diff --git a/app/src/main/java/com/annimon/hotarufx/Main.java b/app/src/main/java/com/annimon/hotarufx/Main.java index 9dd17b5..29bec95 100644 --- a/app/src/main/java/com/annimon/hotarufx/Main.java +++ b/app/src/main/java/com/annimon/hotarufx/Main.java @@ -21,7 +21,8 @@ public class Main extends Application { scene.getStylesheets().addAll( getClass().getResource("/styles/theme-dark.css").toExternalForm(), getClass().getResource("/styles/codearea.css").toExternalForm(), - getClass().getResource("/styles/hotarufx-keywords.css").toExternalForm() + getClass().getResource("/styles/hotarufx-keywords.css").toExternalForm(), + getClass().getResource("/styles/color-picker-box.css").toExternalForm() ); controller = loader.getController(); primaryStage.setScene(scene); diff --git a/app/src/main/java/com/annimon/hotarufx/ui/ColorPickerBox.java b/app/src/main/java/com/annimon/hotarufx/ui/ColorPickerBox.java new file mode 100644 index 0000000..e2505e2 --- /dev/null +++ b/app/src/main/java/com/annimon/hotarufx/ui/ColorPickerBox.java @@ -0,0 +1,263 @@ +package com.annimon.hotarufx.ui; + +import java.util.regex.Pattern; +import javafx.beans.binding.Bindings; +import javafx.beans.binding.ObjectBinding; +import javafx.beans.property.DoubleProperty; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleDoubleProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.event.EventHandler; +import javafx.geometry.Insets; +import javafx.scene.control.Button; +import javafx.scene.control.TextField; +import javafx.scene.input.Clipboard; +import javafx.scene.input.ClipboardContent; +import javafx.scene.input.MouseEvent; +import javafx.scene.layout.Background; +import javafx.scene.layout.BackgroundFill; +import javafx.scene.layout.CornerRadii; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Pane; +import javafx.scene.layout.Priority; +import javafx.scene.layout.Region; +import javafx.scene.layout.StackPane; +import javafx.scene.layout.VBox; +import javafx.scene.paint.Color; +import javafx.scene.paint.CycleMethod; +import javafx.scene.paint.LinearGradient; +import javafx.scene.paint.Stop; +import javafx.util.StringConverter; + +/** + * See https://stackoverflow.com/questions/27171885 + */ +public class ColorPickerBox extends VBox { + + private final ObjectProperty currentColorProperty = new SimpleObjectProperty<>(Color.WHITE); + private final ObjectProperty customColorProperty = new SimpleObjectProperty<>(Color.TRANSPARENT); + + private final DoubleProperty hue = new SimpleDoubleProperty(-1); + private final DoubleProperty sat = new SimpleDoubleProperty(-1); + private final DoubleProperty bright = new SimpleDoubleProperty(-1); + private final DoubleProperty alpha = new SimpleDoubleProperty(100) { + @Override + protected void invalidated() { + final Color c = customColorProperty.get(); + customColorProperty.set(new Color( + c.getRed(), c.getGreen(), c.getBlue(), + clamp(alpha.get() / 100.0) + )); + } + }; + + private final Region colorRectIndicator; + + public ColorPickerBox() { + getStyleClass().add("color-picker-box"); + + customColorProperty().addListener((ov, t, t1) -> colorChanged()); + + /* Hue bar */ + final Pane hueBar = new Pane(); + hueBar.getStyleClass().add("hue-bar"); + hueBar.setBackground(new Background(new BackgroundFill( + createHueGradient(), + CornerRadii.EMPTY, Insets.EMPTY))); + + final Region hueBarIndicator = new Region(); + hueBarIndicator.setId("hue-bar-indicator"); + hueBarIndicator.setMouseTransparent(true); + hueBarIndicator.setCache(true); + + /* Saturation and value rect */ + final Pane colorRect = new StackPane(); + colorRect.getStyleClass().add("color-rect"); + + final Pane colorRectBg = new Pane(); + colorRectBg.backgroundProperty().bind(new ObjectBinding() { + { + bind(hue); + } + @Override + protected Background computeValue() { + return new Background(new BackgroundFill( + Color.hsb(hue.getValue(), 1.0, 1.0), + CornerRadii.EMPTY, Insets.EMPTY)); + } + }); + + final Pane colorRectOverlayWhite = new Pane(); + colorRectOverlayWhite.getStyleClass().add("color-rect"); + colorRectOverlayWhite.setBackground(new Background(new BackgroundFill( + new LinearGradient(0, 0, 1, 0, true, CycleMethod.NO_CYCLE, + new Stop(0, Color.gray(1, 1)), + new Stop(1, Color.gray(1, 0))), + CornerRadii.EMPTY, Insets.EMPTY))); + + final Pane colorRectOverlayBlack = new Pane(); + colorRectOverlayBlack.getStyleClass().add("color-rect"); + colorRectOverlayBlack.setBackground(new Background(new BackgroundFill( + new LinearGradient(0, 0, 0, 1, true, CycleMethod.NO_CYCLE, + new Stop(0, Color.gray(0, 0)), + new Stop(1, Color.gray(0, 1))), + CornerRadii.EMPTY, Insets.EMPTY))); + + final Pane colorRectBlackBorder = new Pane(); + colorRectBlackBorder.setMouseTransparent(true); + colorRectBlackBorder.getStyleClass().addAll("color-rect", "color-rect-border"); + + colorRectIndicator = new Region(); + colorRectIndicator.setId("color-rect-indicator"); + colorRectIndicator.setManaged(false); + colorRectIndicator.setMouseTransparent(true); + colorRectIndicator.setCache(true); + + /* New color rect */ + final Pane newColorRect = new Pane(); + newColorRect.getStyleClass().addAll("color-new-rect"); + newColorRect.setId("new-color"); + newColorRect.backgroundProperty().bind(new ObjectBinding() { + { + bind(customColorProperty); + } + @Override + protected Background computeValue() { + return new Background(new BackgroundFill( + customColorProperty.get(), CornerRadii.EMPTY, Insets.EMPTY)); + } + }); + + /* Color value and copy button */ + final HBox statusPane = new HBox(); + final TextField colorValue = new TextField(); + colorValue.textProperty().bindBidirectional(customColorProperty, new StringConverter() { + final Pattern pattern = Pattern.compile("^#([0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$"); + @Override + public String toString(Color color) { + int r = (int) Math.round(color.getRed() * 255.0); + int g = (int) Math.round(color.getGreen() * 255.0); + int b = (int) Math.round(color.getBlue() * 255.0); + int o = (int) Math.round(color.getOpacity() * 255.0); + String opacity = ""; + if (o < 255) { + opacity = String.format("%02x", o); + } + return String.format("#%02x%02x%02x%s", r, g, b, opacity); + } + @Override + public Color fromString(String string) { + if (pattern.matcher(string).matches()) { + return Color.valueOf(string); + } + return customColorProperty.get(); + } + }); + final Button copyButton = new Button("copy"); + copyButton.setOnAction(e -> { + final Clipboard clipboard = Clipboard.getSystemClipboard(); + final ClipboardContent content = new ClipboardContent(); + content.putString(colorValue.getText()); + clipboard.setContent(content); + }); + + /* Event bindings */ + EventHandler hueBarMouseHandler = event -> { + final double x = event.getX(); + hue.set(clamp(x / colorRect.getWidth()) * 360); + updateHSBColor(); + }; + hueBar.setOnMouseDragged(hueBarMouseHandler); + hueBar.setOnMousePressed(hueBarMouseHandler); + + final EventHandler 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)); + final double currentHue = hue.get(); + updateHSBColor(); + hue.setValue(currentHue); + }; + colorRectOverlayBlack.setOnMouseDragged(rectMouseHandler); + colorRectOverlayBlack.setOnMousePressed(rectMouseHandler); + + /* Layout bindings */ + hueBarIndicator.layoutXProperty().bind( + hue.divide(360).multiply(hueBar.widthProperty())); + colorRectIndicator.layoutXProperty().bind( + sat.divide(100).multiply(colorRect.widthProperty())); + colorRectIndicator.layoutYProperty().bind( + Bindings.subtract(1, bright.divide(100)).multiply(colorRect.heightProperty())); + newColorRect.opacityProperty().bind(alpha.divide(100)); + + /* Adding controls */ + hueBar.getChildren().setAll(hueBarIndicator); + colorRect.getChildren().setAll(colorRectBg, colorRectOverlayWhite, colorRectOverlayBlack, + colorRectBlackBorder, colorRectIndicator); + VBox.setVgrow(colorRect, Priority.SOMETIMES); + HBox.setHgrow(colorValue, Priority.SOMETIMES); + statusPane.getChildren().setAll(colorValue, copyButton); + getChildren().addAll(hueBar, colorRect, newColorRect, statusPane); + + if (currentColorProperty.get() == null) { + currentColorProperty.set(Color.TRANSPARENT); + } + updateValues(); + } + + public void setCurrentColor(Color currentColor) { + this.currentColorProperty.set(currentColor); + updateValues(); + } + + public final ObjectProperty customColorProperty() { + return customColorProperty; + } + + private void updateValues() { + final Color c = currentColorProperty.get(); + hue.set(c.getHue()); + sat.set(c.getSaturation() * 100d); + bright.set(c.getBrightness() * 100); + alpha.set(c.getOpacity() * 100); + updateHSBColor(); + } + + private void colorChanged() { + final Color c = customColorProperty.get(); + hue.set(c.getHue()); + sat.set(c.getSaturation() * 100); + bright.set(c.getBrightness() * 100); + } + + private void updateHSBColor() { + customColorProperty.set(Color.hsb( + hue.get(), + clamp(sat.get() / 100d), + clamp(bright.get() / 100d), + clamp(alpha.get() / 100d) + )); + } + + @Override + protected void layoutChildren() { + super.layoutChildren(); + colorRectIndicator.autosize(); + } + + private static double clamp(double value) { + return (value < 0) ? 0 + : (value > 1) ? 1 : value; + } + + private static LinearGradient createHueGradient() { + final Stop[] stops = new Stop[255]; + for (int x = 0; x < 255; x++) { + final double offset = (1.0 / 255.0) * x; + final int hue = (int)((x / 255.0) * 360); + stops[x] = new Stop(offset, Color.hsb(hue, 1.0, 1.0)); + } + return new LinearGradient(0f, 0f, 1f, 0f, true, CycleMethod.NO_CYCLE, stops); + } +} diff --git a/app/src/main/resources/fxml/Editor.fxml b/app/src/main/resources/fxml/Editor.fxml index 8ced0ca..4eaa723 100644 --- a/app/src/main/resources/fxml/Editor.fxml +++ b/app/src/main/resources/fxml/Editor.fxml @@ -10,6 +10,7 @@ + - - - - - - + + + + + + diff --git a/app/src/main/resources/styles/color-picker-box.css b/app/src/main/resources/styles/color-picker-box.css new file mode 100644 index 0000000..4f1ebc3 --- /dev/null +++ b/app/src/main/resources/styles/color-picker-box.css @@ -0,0 +1,60 @@ +.color-picker-box { + -fx-padding: 1.25em; + -fx-spacing: 0.75em; + -fx-min-width: 20em; + -fx-pref-width: 20em; + -fx-pref-height: 16.666667em; + -fx-max-width: 20em; + -fx-alignment: top-left; + -fx-fill-height: true; +} + +.color-picker-box:focused, +.color-picker-box:selected { + -fx-background-color: transparent; +} + +/* Hue bar, */ +.color-picker-box > .hue-bar { + -fx-min-height: 1.666667em; + -fx-min-width: 16.666667em; + -fx-max-height: 1.666667em; + -fx-border-color: #050505; +} +.color-picker-box > .hue-bar > #hue-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; +} + +/* Saturation and value rect */ +.color-picker-box .color-rect { + -fx-min-width: 16.666667em; + -fx-min-height: 16.666667em; +} +.color-picker-box .color-rect-border { + -fx-border-color: #050505; +} +.color-picker-box #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); +} + +/* New color rect */ +.color-picker-box .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: #050505; +} \ No newline at end of file