diff --git a/sampler/src/main/java/atlantafx/sampler/page/general/ColorBlock.java b/sampler/src/main/java/atlantafx/sampler/page/general/ColorBlock.java index 786d664..59e594a 100644 --- a/sampler/src/main/java/atlantafx/sampler/page/general/ColorBlock.java +++ b/sampler/src/main/java/atlantafx/sampler/page/general/ColorBlock.java @@ -1,7 +1,9 @@ +/* SPDX-License-Identifier: MIT */ package atlantafx.sampler.page.general; import atlantafx.base.theme.Styles; import atlantafx.sampler.util.Containers; +import javafx.beans.property.ReadOnlyObjectProperty; import javafx.geometry.Insets; import javafx.scene.control.Label; import javafx.scene.layout.AnchorPane; @@ -23,6 +25,7 @@ class ColorBlock extends VBox { private final String fgColorName; private final String bgColorName; private final String borderColorName; + private final ReadOnlyObjectProperty bgBaseColor; private final AnchorPane colorBox; private final Text fgText; @@ -32,10 +35,14 @@ class ColorBlock extends VBox { private Consumer actionHandler; - public ColorBlock(String fgColorName, String bgColorName, String borderColorName) { + public ColorBlock(String fgColorName, + String bgColorName, + String borderColorName, + ReadOnlyObjectProperty bgBaseColor) { this.fgColorName = validateColorName(fgColorName); this.bgColorName = validateColorName(bgColorName); this.borderColorName = validateColorName(borderColorName); + this.bgBaseColor = bgBaseColor; fgText = new Text(); fgText.setStyle("-fx-fill:" + fgColorName + ";"); @@ -58,12 +65,16 @@ class ColorBlock extends VBox { colorBox.getStyleClass().add("box"); colorBox.getChildren().setAll(fgText, wsagLabel, expandIcon); colorBox.setOnMouseEntered(e -> { - toggleHover(true); var bgFill = getBgColor(); + + // this happens when css isn't updated yet + if (bgFill == null) { return; } + + toggleHover(true); // doesn't play quite well with transparency, because we not calc // actual underlying background color to flatten bgFill - expandIcon.setFill(getColorLuminance(flattenColor(Color.WHITE, bgFill)) < LUMINANCE_THRESHOLD ? - Color.WHITE : Color.BLACK + expandIcon.setFill(getColorLuminance(flattenColor(bgBaseColor.get(), bgFill)) < LUMINANCE_THRESHOLD ? + Color.WHITE : Color.BLACK ); }); colorBox.setOnMouseExited(e -> toggleHover(false)); @@ -94,6 +105,12 @@ class ColorBlock extends VBox { wsagLabel.setOpacity(state ? 0.5 : 1); } + private Text description(String text) { + var t = new Text(text); + t.getStyleClass().addAll("description", Styles.TEXT_SMALL); + return t; + } + public void update() { var fgFill = getFgColor(); var bgFill = getBgColor(); @@ -105,7 +122,7 @@ class ColorBlock extends VBox { return; } - double contrastRatio = 1 / getContrastRatioOpacityAware(bgFill, fgFill); + double contrastRatio = 1 / getContrastRatioOpacityAware(bgFill, fgFill, bgBaseColor.get()); colorBox.pseudoClassStateChanged(PASSED, contrastRatio >= 4.5); wsagIcon.setIconCode(contrastRatio >= 4.5 ? Material2AL.CHECK : Material2AL.CLOSE); @@ -119,9 +136,8 @@ class ColorBlock extends VBox { } public Color getBgColor() { - return colorBox.getBackground() != null & !colorBox.getBackground().isEmpty() ? - (Color) colorBox.getBackground().getFills().get(0).getFill() : - null; + return colorBox.getBackground() != null && !colorBox.getBackground().isEmpty() ? + (Color) colorBox.getBackground().getFills().get(0).getFill() : null; } public String getFgColorName() { @@ -139,10 +155,4 @@ class ColorBlock extends VBox { public void setOnAction(Consumer actionHandler) { this.actionHandler = actionHandler; } - - private Text description(String text) { - var t = new Text(text); - t.getStyleClass().addAll("description", Styles.TEXT_SMALL); - return t; - } -} \ No newline at end of file +} diff --git a/sampler/src/main/java/atlantafx/sampler/page/general/ColorContrastChecker.java b/sampler/src/main/java/atlantafx/sampler/page/general/ColorContrastChecker.java index bab6100..d92cfc0 100644 --- a/sampler/src/main/java/atlantafx/sampler/page/general/ColorContrastChecker.java +++ b/sampler/src/main/java/atlantafx/sampler/page/general/ColorContrastChecker.java @@ -1,3 +1,4 @@ +/* SPDX-License-Identifier: MIT */ package atlantafx.sampler.page.general; import atlantafx.base.controls.CustomTextField; @@ -12,11 +13,14 @@ import javafx.collections.FXCollections; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import javafx.css.PseudoClass; +import javafx.event.Event; import javafx.geometry.Insets; import javafx.geometry.Pos; +import javafx.scene.control.Button; import javafx.scene.control.ContentDisplay; import javafx.scene.control.Label; import javafx.scene.control.Slider; +import javafx.scene.input.ContextMenuEvent; import javafx.scene.layout.GridPane; import javafx.scene.layout.HBox; import javafx.scene.layout.VBox; @@ -36,6 +40,7 @@ public class ColorContrastChecker extends GridPane { static final PseudoClass PASSED = PseudoClass.getPseudoClass("passed"); static final float[] COLOR_WHITE = new float[] { 255f, 255f, 255f, 1f }; static final float[] COLOR_BLACK = new float[] { 0f, 0f, 0f, 1f }; + static final double CONTRAST_RATIO_THRESHOLD = 1.5; static final double LUMINANCE_THRESHOLD = 0.55; private static final int SLIDER_WIDTH = 300; @@ -45,11 +50,8 @@ public class ColorContrastChecker extends GridPane { private final ObservableHSLAColor bgColor = new ObservableHSLAColor(Color.WHITE); private final ObservableHSLAColor fgColor = new ObservableHSLAColor(Color.BLACK); - private final DoubleBinding contrastRatio = Bindings.createDoubleBinding( - () -> 1 / getContrastRatioOpacityAware(bgColor.getColor(), fgColor.getColor()), - bgColor.colorProperty(), - fgColor.colorProperty() - ); + private final ReadOnlyObjectProperty bgBaseColor; + private final DoubleBinding contrastRatio; private Label bgColorNameLabel; private Label fgColorNameLabel; @@ -62,8 +64,17 @@ public class ColorContrastChecker extends GridPane { private Slider fgLightnessSlider; private Slider fgAlphaSlider; - public ColorContrastChecker() { + public ColorContrastChecker(ReadOnlyObjectProperty bgBaseColor) { super(); + + this.bgBaseColor = bgBaseColor; + this.contrastRatio = Bindings.createDoubleBinding( + () -> 1 / getContrastRatioOpacityAware(bgColor.getColor(), fgColor.getColor(), bgBaseColor.get()), + bgColor.colorProperty(), + fgColor.colorProperty(), + bgBaseColor + ); + createView(); } @@ -155,6 +166,7 @@ public class ColorContrastChecker extends GridPane { bgTextField.textProperty().bind(Bindings.createStringBinding( () -> bgColor.getColorHexWithAlpha().substring(1), bgColor.colorProperty() )); + bgTextField.addEventFilter(ContextMenuEvent.CONTEXT_MENU_REQUESTED, Event::consume); fgColorNameLabel = new Label("Foreground Color"); fgColorNameLabel.setPadding(new Insets(-15, 0, 0, 0)); @@ -166,6 +178,7 @@ public class ColorContrastChecker extends GridPane { fgTextField.textProperty().bind(Bindings.createStringBinding( () -> fgColor.getColorHexWithAlpha().substring(1), fgColor.colorProperty() )); + fgTextField.addEventFilter(ContextMenuEvent.CONTEXT_MENU_REQUESTED, Event::consume); bgHueSlider = slider(1, 360, 1, 1); bgHueSlider.valueProperty().addListener((obs, old, val) -> { @@ -243,6 +256,17 @@ public class ColorContrastChecker extends GridPane { // ~ + var flattenButton = new Button("Flatten"); + flattenButton.setOnAction(e -> { + double[] flatBg = flattenColor(bgBaseColor.get(), bgColor.getColor()); + setBackground(Color.color(flatBg[0], flatBg[1], flatBg[2])); + + double[] flatFg = flattenColor(bgBaseColor.get(), fgColor.getColor()); + setForeground(Color.color(flatFg[0], flatFg[1], flatFg[2])); + }); + + // ~ + getStyleClass().add("contrast-checker"); // column 0 @@ -259,6 +283,8 @@ public class ColorContrastChecker extends GridPane { add(bgAlphaLabel, 0, 10); add(bgAlphaSlider, 0, 11); + add(flattenButton, 0, 12); + // column 1 add(wsagBox, 1, 0); add(new Label("Foreground Color"), 1, 1); @@ -287,14 +313,28 @@ public class ColorContrastChecker extends GridPane { float[] fg = fgColor.getRGBAColor(); // use fallback color if contrast ratio is too low - if (contrastRatio.get() <= LUMINANCE_THRESHOLD) { - fg = getColorLuminance(flattenColor(Color.WHITE, bgColor.getColor())) < 0.55 ? COLOR_WHITE : COLOR_BLACK; + if (contrastRatio.get() <= CONTRAST_RATIO_THRESHOLD) { + fg = getColorLuminance(flattenColor(bgBaseColor.get(), bgColor.getColor())) < LUMINANCE_THRESHOLD ? + COLOR_WHITE : + COLOR_BLACK; } - setStyle(String.format("-color-contrast-checker-bg:rgba(%.0f,%.0f,%.0f,%.2f);-color-contrast-checker-fg:rgba(%.0f,%.0f,%.0f,%.2f);", - bg[0], bg[1], bg[2], bg[3], - fg[0], fg[1], fg[2], fg[3] - )); + // flat colors are necessary for controls that use reverse styling (bg color over fg color), + // it won't be readable if we not remove transparency first + double[] bgFlat = flattenColor(bgBaseColor.get(), bgColor.getColor()); + double[] fgFlat = flattenColor(bgBaseColor.get(), fgColor.getColor()); + + var style = String.format("-color-contrast-checker-bg:rgba(%.0f,%.0f,%.0f,%.2f);" + + "-color-contrast-checker-fg:rgba(%.0f,%.0f,%.0f,%.2f);" + + "-color-contrast-checker-bg-flat:%s;" + + "-color-contrast-checker-fg-flat:%s;", + bg[0], bg[1], bg[2], bg[3], + fg[0], fg[1], fg[2], fg[3], + JColor.color((float) bgFlat[0], (float) bgFlat[1], (float) bgFlat[2]).getColorHex(), + JColor.color((float) fgFlat[0], (float) fgFlat[1], (float) fgFlat[2]).getColorHex() + ); + + setStyle(style); } private void setBackground(Color color) { @@ -357,9 +397,9 @@ public class ColorContrastChecker extends GridPane { return slider; } - static double getContrastRatioOpacityAware(Color bgColor, Color fgColor) { - double luminance1 = getColorLuminance(flattenColor(Color.WHITE, bgColor)); - double luminance2 = getColorLuminance(flattenColor(Color.WHITE, fgColor)); + static double getContrastRatioOpacityAware(Color bgColor, Color fgColor, Color bgBaseColor) { + double luminance1 = getColorLuminance(flattenColor(bgBaseColor, bgColor)); + double luminance2 = getColorLuminance(flattenColor(bgBaseColor, fgColor)); return getContrastRatio(luminance1, luminance2); } @@ -371,11 +411,11 @@ public class ColorContrastChecker extends GridPane { private final ReadOnlyObjectWrapper color = new ReadOnlyObjectWrapper<>() { }; public ObservableHSLAColor(Color initialColor) { - color.set(initialColor); values.addListener((ListChangeListener) c -> { float[] rgb = getRGBAArithmeticColor(); color.set(Color.color(rgb[0], rgb[1], rgb[2], getAlpha())); }); + setColor(initialColor); } public Color getColor() { diff --git a/sampler/src/main/java/atlantafx/sampler/page/general/ColorPalette.java b/sampler/src/main/java/atlantafx/sampler/page/general/ColorPalette.java index e635f09..c6d50a1 100644 --- a/sampler/src/main/java/atlantafx/sampler/page/general/ColorPalette.java +++ b/sampler/src/main/java/atlantafx/sampler/page/general/ColorPalette.java @@ -1,8 +1,12 @@ +/* SPDX-License-Identifier: MIT */ package atlantafx.sampler.page.general; import atlantafx.base.controls.Spacer; import atlantafx.base.theme.Styles; import javafx.application.Platform; +import javafx.beans.property.ReadOnlyBooleanProperty; +import javafx.beans.property.ReadOnlyBooleanWrapper; +import javafx.beans.property.ReadOnlyObjectWrapper; import javafx.geometry.Pos; import javafx.scene.control.Button; import javafx.scene.control.Label; @@ -10,6 +14,7 @@ import javafx.scene.layout.GridPane; import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; import javafx.scene.layout.VBox; +import javafx.scene.paint.Color; import org.kordamp.ikonli.feather.Feather; import org.kordamp.ikonli.javafx.FontIcon; @@ -22,14 +27,35 @@ import java.util.function.Consumer; class ColorPalette extends VBox { - private final List blocks = new ArrayList<>(); - private Label headerLabel; private Button backBtn; private GridPane colorGrid; private ColorContrastChecker contrastChecker; private VBox contrastCheckerArea; + private final List blocks = new ArrayList<>(); + private final Consumer colorBlockActionHandler = colorBlock -> { + ColorContrastChecker c = getOrCreateContrastChecker(); + c.setValues(colorBlock.getFgColorName(), + colorBlock.getFgColor(), + colorBlock.getBgColorName(), + colorBlock.getBgColor() + ); + + if (contrastCheckerArea.getChildren().isEmpty()) { + contrastCheckerArea.getChildren().setAll(c); + } + + showContrastChecker(); + }; + + private final ReadOnlyBooleanWrapper contrastCheckerActive = new ReadOnlyBooleanWrapper(false); + private final ReadOnlyObjectWrapper bgBaseColor = new ReadOnlyObjectWrapper<>(Color.WHITE); + + public ReadOnlyBooleanProperty contrastCheckerActiveProperty() { + return contrastCheckerActive.getReadOnlyProperty(); + } + public ColorPalette() { super(); createView(); @@ -43,22 +69,22 @@ class ColorPalette extends VBox { backBtn.getStyleClass().add(Styles.FLAT); backBtn.setVisible(false); backBtn.setManaged(false); - backBtn.setOnAction(e -> { - headerLabel.setText("Color Palette"); - backBtn.setVisible(false); - backBtn.setManaged(false); - getChildren().set(1, colorGrid); - }); + backBtn.setOnAction(e -> showColorPalette()); var headerBox = new HBox(); headerBox.getChildren().setAll(headerLabel, new Spacer(), backBtn); headerBox.setAlignment(Pos.CENTER_LEFT); + headerBox.getStyleClass().add("header"); contrastCheckerArea = new VBox(); contrastCheckerArea.getStyleClass().add("contrast-checker-area"); colorGrid = colorGrid(); + backgroundProperty().addListener((obs, old, val) -> bgBaseColor.set( + val != null && !val.getFills().isEmpty() ? (Color) val.getFills().get(0).getFill() : Color.WHITE + )); + getChildren().setAll(headerBox, colorGrid); setId("color-palette"); } @@ -100,39 +126,34 @@ class ColorPalette extends VBox { } private ColorBlock colorBlock(String fgColor, String bgColor, String borderColor) { - var actionHandler = new Consumer() { - @Override - public void accept(ColorBlock colorBlock) { - ColorContrastChecker c = getOrCreateContrastChecker(); - c.setValues(colorBlock.getFgColorName(), - colorBlock.getFgColor(), - colorBlock.getBgColorName(), - colorBlock.getBgColor() - ); - - if (contrastCheckerArea.getChildren().isEmpty()) { - contrastCheckerArea.getChildren().setAll(c); - } - - headerLabel.setText("Contrast Checker"); - backBtn.setVisible(true); - backBtn.setManaged(true); - getChildren().set(1, contrastCheckerArea); - } - }; - - var block = new ColorBlock(fgColor, bgColor, borderColor); - block.setOnAction(actionHandler); + var block = new ColorBlock(fgColor, bgColor, borderColor, bgBaseColor.getReadOnlyProperty()); + block.setOnAction(colorBlockActionHandler); blocks.add(block); return block; } private ColorContrastChecker getOrCreateContrastChecker() { - if (contrastChecker == null) { contrastChecker = new ColorContrastChecker(); } + if (contrastChecker == null) { contrastChecker = new ColorContrastChecker(bgBaseColor.getReadOnlyProperty()); } VBox.setVgrow(contrastChecker, Priority.ALWAYS); return contrastChecker; } + private void showColorPalette() { + headerLabel.setText("Color Palette"); + backBtn.setVisible(false); + backBtn.setManaged(false); + getChildren().set(1, colorGrid); + contrastCheckerActive.set(false); + } + + private void showContrastChecker() { + headerLabel.setText("Contrast Checker"); + backBtn.setVisible(true); + backBtn.setManaged(true); + getChildren().set(1, contrastCheckerArea); + contrastCheckerActive.set(true); + } + // To calculate contrast ratio, we have to obtain all components colors first. // Unfortunately, JavaFX doesn't provide an API to observe when stylesheet changes has been applied. // The timer is introduced to defer widget update to a time when scene changes supposedly will be finished. diff --git a/sampler/src/main/java/atlantafx/sampler/page/general/ThemePage.java b/sampler/src/main/java/atlantafx/sampler/page/general/ThemePage.java index 988c21d..3dac181 100755 --- a/sampler/src/main/java/atlantafx/sampler/page/general/ThemePage.java +++ b/sampler/src/main/java/atlantafx/sampler/page/general/ThemePage.java @@ -43,6 +43,7 @@ public class ThemePage extends AbstractPage { private GridPane optionsGrid() { ChoiceBox themeSelector = themeSelector(); themeSelector.setPrefWidth(200); + themeSelector.disableProperty().bind(colorPalette.contrastCheckerActiveProperty()); // ~ diff --git a/sampler/src/main/resources/assets/styles/index.css b/sampler/src/main/resources/assets/styles/index.css index 886de02..6f2a28b 100755 --- a/sampler/src/main/resources/assets/styles/index.css +++ b/sampler/src/main/resources/assets/styles/index.css @@ -101,6 +101,11 @@ #color-palette { -fx-spacing: 20px; + /* mandatory, base bg color for flatten feature */ + -fx-background-color: -color-bg-default; +} +#color-palette > .header { + -fx-pref-height: 40px; } #color-palette > .grid { -fx-vgap: 20px; @@ -150,6 +155,23 @@ -fx-border-color: -color-contrast-checker-fg; -fx-border-width: 0 0 1 0; } +#color-palette > .contrast-checker-area > .contrast-checker .button { + -color-button-bg: -color-contrast-checker-fg-flat; + -color-button-fg: -color-contrast-checker-bg-flat; + -color-button-border: -color-contrast-checker-bg-flat; + + -color-button-bg-hover: -color-contrast-checker-fg-flat; + -color-button-fg-hover: -color-contrast-checker-bg-flat; + -color-button-border-hover: -color-contrast-checker-bg-flat; + + -color-button-bg-focused: -color-contrast-checker-fg-flat; + -color-button-fg-focused: -color-contrast-checker-bg-flat; + -color-button-border-focused: -color-contrast-checker-bg-flat; + + -color-button-bg-pressed: -color-contrast-checker-bg-flat; + -color-button-fg-pressed: -color-contrast-checker-fg-flat; + -color-button-border-pressed: -color-contrast-checker-fg-flat; +} #color-palette > .contrast-checker-area > .contrast-checker .ikonli-font-icon { -fx-icon-color: -color-contrast-checker-fg; -fx-fill: -color-contrast-checker-fg;