Display contrast checker in overlay dialog

This commit is contained in:
mkpaz 2022-09-01 14:11:02 +04:00
parent 3ab95551f3
commit 9b0b0bf44c
22 changed files with 780 additions and 368 deletions

@ -109,12 +109,6 @@
<directory>src/main/java/atlantafx/sampler/page</directory> <directory>src/main/java/atlantafx/sampler/page</directory>
<targetPath>atlantafx/sampler/page</targetPath> <targetPath>atlantafx/sampler/page</targetPath>
<filtering>false</filtering> <filtering>false</filtering>
<excludes>
<exclude>**/AbstractPage.java</exclude>
<exclude>**/CodeViewer.java</exclude>
<exclude>**/Page.java</exclude>
<exclude>**/SampleBlock.java</exclude>
</excludes>
</resource> </resource>
<!-- copy icons --> <!-- copy icons -->
<resource> <resource>

@ -1,61 +1,14 @@
/* SPDX-License-Identifier: MIT */ /* SPDX-License-Identifier: MIT */
package atlantafx.sampler.layout; package atlantafx.sampler.layout;
import atlantafx.sampler.page.Page;
import atlantafx.sampler.page.components.OverviewPage;
import javafx.animation.FadeTransition;
import javafx.application.Platform;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane; import javafx.scene.layout.StackPane;
import javafx.util.Duration;
import java.util.Objects; public class ApplicationWindow extends StackPane {
import static javafx.scene.layout.Priority.ALWAYS;
public class ApplicationWindow extends BorderPane {
public ApplicationWindow() { public ApplicationWindow() {
var sidebar = new Sidebar(); getChildren().setAll(
sidebar.setMinWidth(200); new Overlay(),
new MainLayer()
final var pageContainer = new StackPane(); );
HBox.setHgrow(pageContainer, ALWAYS);
sidebar.setOnSelect(pageClass -> {
try {
final Page prevPage = (!pageContainer.getChildren().isEmpty() && pageContainer.getChildren().get(0) instanceof Page page) ? page : null;
final Page nextPage = pageClass.getDeclaredConstructor().newInstance();
// startup, no animation
if (getScene() == null) {
pageContainer.getChildren().add(nextPage.getView());
return;
}
Objects.requireNonNull(prevPage);
// reset previous page, e.g. to free resources
prevPage.reset();
// animate switching between pages
pageContainer.getChildren().add(nextPage.getView());
FadeTransition transition = new FadeTransition(Duration.millis(300), nextPage.getView());
transition.setFromValue(0.0);
transition.setToValue(1.0);
transition.setOnFinished(t -> pageContainer.getChildren().remove(prevPage.getView()));
transition.play();
} catch (Exception e) {
throw new RuntimeException(e);
}
});
// ~
setLeft(sidebar);
setCenter(pageContainer);
sidebar.select(OverviewPage.class);
Platform.runLater(sidebar::requestFocus);
} }
} }

@ -0,0 +1,66 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.layout;
import atlantafx.sampler.page.Page;
import atlantafx.sampler.page.components.OverviewPage;
import javafx.animation.FadeTransition;
import javafx.application.Platform;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.util.Duration;
import java.util.Objects;
import static javafx.scene.layout.Priority.ALWAYS;
class MainLayer extends BorderPane {
public MainLayer() {
super();
createView();
}
private void createView() {
var sidebar = new Sidebar();
sidebar.setMinWidth(200);
final var pageContainer = new StackPane();
HBox.setHgrow(pageContainer, ALWAYS);
sidebar.setOnSelect(pageClass -> {
try {
final Page prevPage = (!pageContainer.getChildren().isEmpty() && pageContainer.getChildren().get(0) instanceof Page page) ? page : null;
final Page nextPage = pageClass.getDeclaredConstructor().newInstance();
// startup, no animation
if (getScene() == null) {
pageContainer.getChildren().add(nextPage.getView());
return;
}
Objects.requireNonNull(prevPage);
// reset previous page, e.g. to free resources
prevPage.reset();
// animate switching between pages
pageContainer.getChildren().add(nextPage.getView());
FadeTransition transition = new FadeTransition(Duration.millis(300), nextPage.getView());
transition.setFromValue(0.0);
transition.setToValue(1.0);
transition.setOnFinished(t -> pageContainer.getChildren().remove(prevPage.getView()));
transition.play();
} catch (Exception e) {
throw new RuntimeException(e);
}
});
// ~
setLeft(sidebar);
setCenter(pageContainer);
sidebar.select(OverviewPage.class);
Platform.runLater(sidebar::requestFocus);
}
}

@ -0,0 +1,156 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.layout;
import atlantafx.sampler.util.Animations;
import atlantafx.sampler.util.Containers;
import atlantafx.sampler.util.NodeUtils;
import javafx.animation.Timeline;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.ReadOnlyBooleanWrapper;
import javafx.event.Event;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.ScrollPane;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseButton;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.util.Duration;
import java.util.Objects;
import java.util.function.Consumer;
public class Overlay extends StackPane {
public static final String STYLE_CLASS = "overlay";
private ScrollPane scrollPane;
private AnchorPane edgeContentWrapper;
private StackPane centerContentWrapper;
private final ReadOnlyBooleanWrapper onFrontProperty = new ReadOnlyBooleanWrapper(false);
private final Timeline fadeInTransition = Animations.fadeIn(this, Duration.millis(100));
private final Timeline fadeOutTransition = Animations.fadeOut(this, Duration.millis(200));
private HPos currentContentPos;
public Overlay() {
createView();
}
private void createView() {
edgeContentWrapper = new AnchorPane();
edgeContentWrapper.getStyleClass().add("scrollable-content");
centerContentWrapper = new StackPane();
centerContentWrapper.getStyleClass().add("scrollable-content");
centerContentWrapper.setAlignment(Pos.CENTER);
scrollPane = new ScrollPane();
Containers.setScrollConstraints(scrollPane,
ScrollPane.ScrollBarPolicy.AS_NEEDED, true,
ScrollPane.ScrollBarPolicy.NEVER, true
);
scrollPane.setMaxHeight(10_000); // scroll pane won't work without height specified
// ~
Consumer<Event> hideAndConsume = e -> {
removeContent();
toBack();
e.consume();
};
// hide overlay by pressing ESC (only works when overlay or one of its children has focus,
// that's why we requesting it in the toFront())
addEventHandler(KeyEvent.KEY_PRESSED, e -> {
if (e.getCode() == KeyCode.ESCAPE) { hideAndConsume.accept(e); }
});
// hide overlay by clicking outside content area
setOnMouseClicked(e -> {
Pane content = getContent();
Node eventSource = e.getPickResult().getIntersectedNode();
if (e.getButton() == MouseButton.PRIMARY && content != null && !NodeUtils.isDescendant(content, eventSource)) {
hideAndConsume.accept(e);
}
});
getChildren().add(scrollPane);
getStyleClass().add(STYLE_CLASS);
}
public Pane getContent() {
return NodeUtils.getChildByIndex(getContentWrapper(), 0, Pane.class);
}
private Pane getContentWrapper() {
return currentContentPos == HPos.CENTER ? centerContentWrapper : edgeContentWrapper;
}
public void setContent(Pane content, HPos pos) {
Objects.requireNonNull(content);
Objects.requireNonNull(pos);
// clear old content
if (pos != currentContentPos) {
removeContent();
currentContentPos = pos;
}
switch (pos) {
case LEFT -> {
edgeContentWrapper.getChildren().setAll(content);
Containers.setAnchors(content, new Insets(0, -1, 0, 0));
}
case RIGHT -> {
edgeContentWrapper.getChildren().setAll(content);
Containers.setAnchors(content, new Insets(0, 0, 0, -1));
}
case CENTER -> {
centerContentWrapper.getChildren().setAll(content);
StackPane.setAlignment(content, Pos.CENTER);
}
}
scrollPane.setContent(getContentWrapper());
}
public void removeContent() {
getContentWrapper().getChildren().clear();
}
public boolean contains(Pane content) {
return content != null &&
getContentWrapper().getChildren().size() > 0 &&
getContentWrapper().getChildren().get(0).equals(content);
}
@Override
public void toFront() {
if (onFrontProperty.get()) { return; }
super.toFront();
fadeInTransition.playFromStart();
onFrontProperty.set(true);
}
@Override
public void toBack() {
if (!onFrontProperty.get()) { return; }
super.toBack();
fadeOutTransition.playFromStart();
onFrontProperty.set(false);
}
public ReadOnlyBooleanProperty onFrontProperty() {
return onFrontProperty.getReadOnlyProperty();
}
public boolean isOnFront() {
return onFrontProperty().get();
}
}

@ -30,7 +30,7 @@ import java.util.function.Consumer;
import static javafx.scene.layout.Priority.ALWAYS; import static javafx.scene.layout.Priority.ALWAYS;
public class Sidebar extends VBox { class Sidebar extends VBox {
private static final PseudoClass SELECTED = PseudoClass.getPseudoClass("selected"); private static final PseudoClass SELECTED = PseudoClass.getPseudoClass("selected");

@ -4,6 +4,7 @@ package atlantafx.sampler.page;
import atlantafx.base.controls.Popover; import atlantafx.base.controls.Popover;
import atlantafx.base.controls.Spacer; import atlantafx.base.controls.Spacer;
import atlantafx.base.theme.Styles; import atlantafx.base.theme.Styles;
import atlantafx.sampler.layout.Overlay;
import atlantafx.sampler.theme.ThemeManager; import atlantafx.sampler.theme.ThemeManager;
import javafx.event.ActionEvent; import javafx.event.ActionEvent;
import javafx.event.EventHandler; import javafx.event.EventHandler;
@ -48,6 +49,7 @@ public abstract class AbstractPage extends BorderPane implements Page {
protected StackPane codeViewerWrapper; protected StackPane codeViewerWrapper;
protected CodeViewer codeViewer; protected CodeViewer codeViewer;
protected VBox userContent; protected VBox userContent;
protected Overlay overlay;
protected boolean isRendered = false; protected boolean isRendered = false;
protected AbstractPage() { protected AbstractPage() {
@ -129,9 +131,16 @@ public abstract class AbstractPage extends BorderPane implements Page {
// Some properties can only be obtained after node placed // Some properties can only be obtained after node placed
// to the scene graph and here is the place do this. // to the scene graph and here is the place do this.
protected void onRendered() { } protected void onRendered() {
this.overlay = lookupOverlay();
}
private void showThemeConfigPopover() { protected Overlay lookupOverlay() {
return getScene() != null && getScene().lookup("." + Overlay.STYLE_CLASS) instanceof Overlay overlay ?
overlay : null;
}
protected void showThemeConfigPopover() {
if (quickConfigPopover == null) { if (quickConfigPopover == null) {
var content = new QuickConfigMenu(); var content = new QuickConfigMenu();
content.setExitHandler(() -> quickConfigPopover.hide()); content.setExitHandler(() -> quickConfigPopover.hide());

@ -0,0 +1,113 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.page;
import atlantafx.base.controls.Spacer;
import atlantafx.sampler.util.Containers;
import javafx.geometry.Pos;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import org.kordamp.ikonli.javafx.FontIcon;
import org.kordamp.ikonli.material2.Material2AL;
import java.util.Objects;
import static atlantafx.base.theme.Styles.*;
public abstract class OverlayDialog<T extends Region> extends VBox {
protected static final int CONTENT_CHILD_INDEX = 1;
protected Label titleLabel;
protected Button topCloseBtn;
protected HBox headerBox;
protected Button bottomCloseBtn;
protected HBox footerBox;
protected Runnable onCloseCallback;
public OverlayDialog() {
createView();
}
protected void createView() {
titleLabel = new Label();
titleLabel.getStyleClass().addAll(TITLE_4, "title");
topCloseBtn = new Button("", new FontIcon(Material2AL.CLOSE));
topCloseBtn.getStyleClass().addAll(BUTTON_ICON, BUTTON_CIRCLE, FLAT, "close-button");
topCloseBtn.setOnAction(e -> close());
headerBox = new HBox(10);
headerBox.getStyleClass().add("header");
headerBox.setAlignment(Pos.CENTER_LEFT);
headerBox.getChildren().setAll(
titleLabel,
new Spacer(),
topCloseBtn
);
VBox.setVgrow(headerBox, Priority.NEVER);
bottomCloseBtn = new Button("Close");
bottomCloseBtn.getStyleClass().add("form-action");
bottomCloseBtn.setOnAction(e -> close());
bottomCloseBtn.setCancelButton(true);
footerBox = new HBox(10);
footerBox.getStyleClass().add("footer");
footerBox.setAlignment(Pos.CENTER_RIGHT);
footerBox.getChildren().setAll(
new Spacer(),
bottomCloseBtn
);
VBox.setVgrow(footerBox, Priority.NEVER);
// IMPORTANT: this guarantees client will use correct width and height
Containers.usePrefWidth(this);
Containers.usePrefHeight(this);
// if you're updating this line, update setContent() method as well
getChildren().setAll(headerBox, footerBox);
getStyleClass().add("overlay-dialog");
}
protected void setContent(T content) {
Objects.requireNonNull(content);
VBox.setVgrow(content, Priority.ALWAYS);
// content have to be placed exactly between header and footer
if (getChildren().size() == 2) {
// add new content
getChildren().add(CONTENT_CHILD_INDEX, content);
} else if (getChildren().size() == 3) {
// overwrite existing content
getChildren().set(CONTENT_CHILD_INDEX, content);
} else {
throw new UnsupportedOperationException("Content cannot be placed because of unexpected children size. " +
"You should override 'OverlayDialog#setContent()' and place it manually.");
}
}
protected void setTitle(String title) {
titleLabel.setText(title);
}
public void close() {
if (onCloseCallback != null) {
onCloseCallback.run();
}
}
public Runnable getOnCloseRequest() {
return onCloseCallback;
}
public void setOnCloseRequest(Runnable handler) {
this.onCloseCallback = handler;
}
}

@ -16,7 +16,7 @@ import org.kordamp.ikonli.material2.Material2AL;
import java.util.function.Consumer; import java.util.function.Consumer;
import static atlantafx.sampler.page.general.ColorContrastChecker.*; import static atlantafx.sampler.page.general.ContrastChecker.*;
import static atlantafx.sampler.util.JColorUtils.flattenColor; import static atlantafx.sampler.util.JColorUtils.flattenColor;
import static atlantafx.sampler.util.JColorUtils.getColorLuminance; import static atlantafx.sampler.util.JColorUtils.getColorLuminance;
@ -71,8 +71,8 @@ class ColorBlock extends VBox {
if (bgFill == null) { return; } if (bgFill == null) { return; }
toggleHover(true); toggleHover(true);
expandIcon.setFill( expandIcon.setFill(getColorLuminance(flattenColor(bgBaseColor.get(), bgFill)) < LUMINANCE_THRESHOLD ?
getColorLuminance(flattenColor(bgBaseColor.get(), bgFill)) < LUMINANCE_THRESHOLD ? Color.WHITE : Color.BLACK Color.WHITE : Color.BLACK
); );
}); });
colorBox.setOnMouseExited(e -> toggleHover(false)); colorBox.setOnMouseExited(e -> toggleHover(false));

@ -4,82 +4,42 @@ package atlantafx.sampler.page.general;
import atlantafx.base.controls.Spacer; import atlantafx.base.controls.Spacer;
import atlantafx.base.theme.Styles; import atlantafx.base.theme.Styles;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.beans.property.ReadOnlyBooleanProperty; import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.ReadOnlyBooleanWrapper;
import javafx.beans.property.ReadOnlyObjectWrapper; import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.scene.control.Button;
import javafx.scene.control.Label; import javafx.scene.control.Label;
import javafx.scene.layout.GridPane; import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox; import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox; import javafx.scene.layout.VBox;
import javafx.scene.paint.Color; import javafx.scene.paint.Color;
import org.kordamp.ikonli.feather.Feather;
import org.kordamp.ikonli.javafx.FontIcon;
import java.time.Duration; import java.time.Duration;
import java.util.ArrayList; import java.util.*;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import java.util.function.Consumer; import java.util.function.Consumer;
class ColorPalette extends VBox { class ColorPalette extends VBox {
private Label headerLabel;
private Button backBtn;
private GridPane colorGrid;
private ColorContrastChecker contrastChecker;
private VBox contrastCheckerArea;
private final List<ColorBlock> blocks = new ArrayList<>(); private final List<ColorBlock> blocks = new ArrayList<>();
private final Consumer<ColorBlock> 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<Color> bgBaseColor = new ReadOnlyObjectWrapper<>(Color.WHITE); private final ReadOnlyObjectWrapper<Color> bgBaseColor = new ReadOnlyObjectWrapper<>(Color.WHITE);
private final Consumer<ColorBlock> colorBlockActionHandler;
public ReadOnlyBooleanProperty contrastCheckerActiveProperty() { public ColorPalette(Consumer<ColorBlock> blockClickedHandler) {
return contrastCheckerActive.getReadOnlyProperty();
}
public ColorPalette() {
super(); super();
this.colorBlockActionHandler = Objects.requireNonNull(blockClickedHandler);
createView(); createView();
} }
private void createView() { private void createView() {
headerLabel = new Label("Color Palette"); var headerLabel = new Label("Color Palette");
headerLabel.getStyleClass().add(Styles.TITLE_4); headerLabel.getStyleClass().add(Styles.TITLE_4);
backBtn = new Button("Back", new FontIcon(Feather.CHEVRONS_LEFT));
backBtn.getStyleClass().add(Styles.FLAT);
backBtn.setVisible(false);
backBtn.setManaged(false);
backBtn.setOnAction(e -> showColorPalette());
var headerBox = new HBox(); var headerBox = new HBox();
headerBox.getChildren().setAll(headerLabel, new Spacer(), backBtn); headerBox.getChildren().setAll(headerLabel, new Spacer());
headerBox.setAlignment(Pos.CENTER_LEFT); headerBox.setAlignment(Pos.CENTER_LEFT);
headerBox.getStyleClass().add("header"); headerBox.getStyleClass().add("header");
contrastCheckerArea = new VBox(); var colorGrid = colorGrid();
contrastCheckerArea.getStyleClass().add("contrast-checker-area");
colorGrid = colorGrid();
backgroundProperty().addListener((obs, old, val) -> bgBaseColor.set( backgroundProperty().addListener((obs, old, val) -> bgBaseColor.set(
val != null && !val.getFills().isEmpty() ? (Color) val.getFills().get(0).getFill() : Color.WHITE val != null && !val.getFills().isEmpty() ? (Color) val.getFills().get(0).getFill() : Color.WHITE
@ -132,28 +92,6 @@ class ColorPalette extends VBox {
return block; return block;
} }
private ColorContrastChecker getOrCreateContrastChecker() {
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. // 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. // 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. // The timer is introduced to defer widget update to a time when scene changes supposedly will be finished.
@ -165,4 +103,8 @@ class ColorPalette extends VBox {
} }
}, delay.toMillis()); }, delay.toMillis());
} }
public ReadOnlyObjectProperty<Color> bgBaseColorProperty() {
return bgBaseColor.getReadOnlyProperty();
}
} }

@ -2,6 +2,7 @@
package atlantafx.sampler.page.general; package atlantafx.sampler.page.general;
import atlantafx.base.controls.CustomTextField; import atlantafx.base.controls.CustomTextField;
import atlantafx.base.controls.Spacer;
import atlantafx.base.theme.Styles; import atlantafx.base.theme.Styles;
import atlantafx.sampler.theme.ThemeManager; import atlantafx.sampler.theme.ThemeManager;
import atlantafx.sampler.util.JColor; import atlantafx.sampler.util.JColor;
@ -33,16 +34,15 @@ import org.kordamp.ikonli.material2.Material2AL;
import java.util.Objects; import java.util.Objects;
import static atlantafx.sampler.page.general.ColorBlock.validateColorName; import static atlantafx.sampler.page.general.ColorBlock.validateColorName;
import static atlantafx.sampler.util.JColorUtils.*; import static atlantafx.sampler.util.JColorUtils.flattenColor;
import static atlantafx.sampler.util.JColorUtils.getColorLuminance;
// Inspired by the https://colourcontrast.cc/ // Inspired by the https://colourcontrast.cc/
public class ColorContrastChecker extends GridPane { public class ContrastChecker extends GridPane {
static final PseudoClass PASSED = PseudoClass.getPseudoClass("passed"); public static final double CONTRAST_RATIO_THRESHOLD = 1.5;
static final float[] COLOR_WHITE = new float[] { 255f, 255f, 255f, 1f }; public static final double LUMINANCE_THRESHOLD = 0.55;
static final float[] COLOR_BLACK = new float[] { 0f, 0f, 0f, 1f }; public static final PseudoClass PASSED = PseudoClass.getPseudoClass("passed");
static final double CONTRAST_RATIO_THRESHOLD = 1.5;
static final double LUMINANCE_THRESHOLD = 0.55;
private static final int SLIDER_WIDTH = 300; private static final int SLIDER_WIDTH = 300;
@ -65,7 +65,7 @@ public class ColorContrastChecker extends GridPane {
private Slider fgLightnessSlider; private Slider fgLightnessSlider;
private Slider fgAlphaSlider; private Slider fgAlphaSlider;
public ColorContrastChecker(ReadOnlyObjectProperty<Color> bgBaseColor) { public ContrastChecker(ReadOnlyObjectProperty<Color> bgBaseColor) {
super(); super();
this.bgBaseColor = bgBaseColor; this.bgBaseColor = bgBaseColor;
@ -98,6 +98,19 @@ public class ColorContrastChecker extends GridPane {
public Color getFgColor() { return fgColor.colorProperty().get(); } public Color getFgColor() { return fgColor.colorProperty().get(); }
public ReadOnlyObjectProperty<Color> bgColorProperty() { return bgColor.colorProperty(); }
public ReadOnlyObjectProperty<Color> fgColorProperty() { return fgColor.colorProperty(); }
// Returns fg color that guaranteed to be visible on the current bg.
public Color getSafeFgColor() {
if (contrastRatio.get() <= CONTRAST_RATIO_THRESHOLD) {
return getColorLuminance(flattenColor(bgBaseColor.get(), bgColor.getColor())) < LUMINANCE_THRESHOLD ? Color.WHITE : Color.BLACK;
} else {
return fgColor.getColor();
}
}
private void createView() { private void createView() {
var textLabel = new Label("Aa"); var textLabel = new Label("Aa");
textLabel.getStyleClass().add("text"); textLabel.getStyleClass().add("text");
@ -274,15 +287,16 @@ public class ColorContrastChecker extends GridPane {
tm.reloadCustomCSS(); tm.reloadCustomCSS();
}); });
var controlsBox = new HBox(20, flattenBtn, applyBtn); var controlsBox = new HBox(20, new Spacer(), flattenBtn, applyBtn);
controlsBox.setAlignment(Pos.CENTER_LEFT); controlsBox.setAlignment(Pos.CENTER_LEFT);
controlsBox.setPadding(new Insets(10, 0, 0, 0));
// ~ // ~
getStyleClass().add("contrast-checker"); getStyleClass().add("contrast-checker");
// column 0 // column 0
add(fontBox, 0, 0); add(new HBox(fontBox, new Spacer(), wsagBox), 0, 0, REMAINING, 1);
add(new Label("Background Color"), 0, 1); add(new Label("Background Color"), 0, 1);
add(bgColorNameLabel, 0, 2); add(bgColorNameLabel, 0, 2);
add(bgTextField, 0, 3); add(bgTextField, 0, 3);
@ -298,7 +312,6 @@ public class ColorContrastChecker extends GridPane {
add(controlsBox, 0, 12, REMAINING, 1); add(controlsBox, 0, 12, REMAINING, 1);
// column 1 // column 1
add(wsagBox, 1, 0);
add(new Label("Foreground Color"), 1, 1); add(new Label("Foreground Color"), 1, 1);
add(fgColorNameLabel, 1, 2); add(fgColorNameLabel, 1, 2);
add(fgTextField, 1, 3); add(fgTextField, 1, 3);
@ -321,40 +334,14 @@ public class ColorContrastChecker extends GridPane {
} }
private void updateStyle() { private void updateStyle() {
float[] bg = bgColor.getRGBAColor(); setStyle(String.format("-color-contrast-checker-bg:%s;-color-contrast-checker-fg:%s;",
float[] fg = fgColor.getRGBAColor(); JColorUtils.toHexWithAlpha(bgColor.getColor()),
JColorUtils.toHexWithAlpha(getSafeFgColor())
// use fallback color if contrast ratio is too low ));
if (contrastRatio.get() <= CONTRAST_RATIO_THRESHOLD) {
fg = getColorLuminance(flattenColor(bgBaseColor.get(), bgColor.getColor())) < LUMINANCE_THRESHOLD ?
COLOR_WHITE :
COLOR_BLACK;
}
// 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) { private void setBackground(Color color) {
float[] hsl = JColorUtils.toHSL( float[] hsl = JColorUtils.toHSL(color);
(float) color.getRed(),
(float) color.getGreen(),
(float) color.getBlue()
);
bgHueSlider.setValue(hsl[0]); bgHueSlider.setValue(hsl[0]);
bgSaturationSlider.setValue(hsl[1]); bgSaturationSlider.setValue(hsl[1]);
bgLightnessSlider.setValue(hsl[2]); bgLightnessSlider.setValue(hsl[2]);
@ -362,11 +349,7 @@ public class ColorContrastChecker extends GridPane {
} }
private void setForeground(Color color) { private void setForeground(Color color) {
float[] hsl = JColorUtils.toHSL( float[] hsl = JColorUtils.toHSL(color);
(float) color.getRed(),
(float) color.getGreen(),
(float) color.getBlue()
);
fgHueSlider.setValue(hsl[0]); fgHueSlider.setValue(hsl[0]);
fgSaturationSlider.setValue(hsl[1]); fgSaturationSlider.setValue(hsl[1]);
fgLightnessSlider.setValue(hsl[2]); fgLightnessSlider.setValue(hsl[2]);
@ -412,7 +395,7 @@ public class ColorContrastChecker extends GridPane {
static double getContrastRatioOpacityAware(Color bgColor, Color fgColor, Color bgBaseColor) { static double getContrastRatioOpacityAware(Color bgColor, Color fgColor, Color bgBaseColor) {
double luminance1 = getColorLuminance(flattenColor(bgBaseColor, bgColor)); double luminance1 = getColorLuminance(flattenColor(bgBaseColor, bgColor));
double luminance2 = getColorLuminance(flattenColor(bgBaseColor, fgColor)); double luminance2 = getColorLuminance(flattenColor(bgBaseColor, fgColor));
return getContrastRatio(luminance1, luminance2); return JColorUtils.getContrastRatio(luminance1, luminance2);
} }
/////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////
@ -435,11 +418,7 @@ public class ColorContrastChecker extends GridPane {
} }
public void setColor(Color color) { public void setColor(Color color) {
float[] hsl = JColorUtils.toHSL( float[] hsl = JColorUtils.toHSL(color);
(float) color.getRed(),
(float) color.getGreen(),
(float) color.getBlue()
);
values.setAll(hsl[0], hsl[1], hsl[2], (float) color.getOpacity()); values.setAll(hsl[0], hsl[1], hsl[2], (float) color.getOpacity());
} }
@ -490,17 +469,6 @@ public class ColorContrastChecker extends GridPane {
}; };
} }
public float[] getRGBAColor() {
float[] hsl = new float[] { getHue(), getSaturation(), getLightness() };
var color = JColor.color(hsl, getAlpha());
return new float[] {
color.getRed(),
color.getGreen(),
color.getBlue(),
getAlpha()
};
}
public String getColorHexWithAlpha() { public String getColorHexWithAlpha() {
float[] hsl = new float[] { getHue(), getSaturation(), getLightness() }; float[] hsl = new float[] { getHue(), getSaturation(), getLightness() };
return JColor.color(hsl, getAlpha()).getColorHexWithAlpha(); return JColor.color(hsl, getAlpha()).getColorHexWithAlpha();

@ -0,0 +1,36 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.page.general;
import atlantafx.sampler.page.OverlayDialog;
import atlantafx.sampler.util.JColorUtils;
import atlantafx.sampler.util.NodeUtils;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.scene.paint.Color;
class ContrastCheckerDialog extends OverlayDialog<ContrastChecker> {
private final ContrastChecker contrastChecker;
public ContrastCheckerDialog(ReadOnlyObjectProperty<Color> bgBaseColor) {
this.contrastChecker = new ContrastChecker(bgBaseColor);
contrastChecker.bgColorProperty().addListener((obs, old, val) -> updateStyle());
contrastChecker.fgColorProperty().addListener((obs, old, val) -> updateStyle());
getStyleClass().add("contrast-checker-dialog");
setTitle("Contrast Checker");
setContent(contrastChecker);
NodeUtils.toggleVisibility(footerBox, false);
}
private void updateStyle() {
setStyle(String.format("-color-dialog-bg:%s;-color-dialog-fg:%s;",
JColorUtils.toHexWithAlpha(contrastChecker.getBgColor()),
JColorUtils.toHexWithAlpha(contrastChecker.getSafeFgColor())
));
}
public ContrastChecker getContent() {
return contrastChecker;
}
}

@ -5,6 +5,8 @@ import atlantafx.base.theme.Theme;
import atlantafx.sampler.page.AbstractPage; import atlantafx.sampler.page.AbstractPage;
import atlantafx.sampler.theme.ThemeEvent.EventType; import atlantafx.sampler.theme.ThemeEvent.EventType;
import atlantafx.sampler.theme.ThemeManager; import atlantafx.sampler.theme.ThemeManager;
import atlantafx.sampler.util.NodeUtils;
import javafx.geometry.HPos;
import javafx.scene.control.ChoiceBox; import javafx.scene.control.ChoiceBox;
import javafx.scene.control.Label; import javafx.scene.control.Label;
import javafx.scene.layout.GridPane; import javafx.scene.layout.GridPane;
@ -12,12 +14,26 @@ import javafx.util.StringConverter;
import java.time.Duration; import java.time.Duration;
import java.util.Objects; import java.util.Objects;
import java.util.function.Consumer;
public class ThemePage extends AbstractPage { public class ThemePage extends AbstractPage {
public static final String NAME = "Theme"; public static final String NAME = "Theme";
private final ColorPalette colorPalette = new ColorPalette(); private final Consumer<ColorBlock> colorBlockActionHandler = colorBlock -> {
ContrastCheckerDialog dialog = getOrCreateContrastCheckerDialog();
dialog.getContent().setValues(colorBlock.getFgColorName(),
colorBlock.getFgColor(),
colorBlock.getBgColorName(),
colorBlock.getBgColor()
);
overlay.setContent(dialog, HPos.CENTER);
overlay.toFront();
};
private final ColorPalette colorPalette = new ColorPalette(colorBlockActionHandler);
private ContrastCheckerDialog contrastCheckerDialog;
@Override @Override
public String getName() { return NAME; } public String getName() { return NAME; }
@ -46,16 +62,13 @@ public class ThemePage extends AbstractPage {
); );
// if you want to enable quick menu don't forget that // if you want to enable quick menu don't forget that
// theme selection choice box have to be updated accordingly // theme selection choice box have to be updated accordingly
quickConfigBtn.setVisible(false); NodeUtils.toggleVisibility(quickConfigBtn, false);
quickConfigBtn.setManaged(false); NodeUtils.toggleVisibility(sourceCodeToggleBtn, false);
sourceCodeToggleBtn.setVisible(false);
sourceCodeToggleBtn.setManaged(false);
} }
private GridPane optionsGrid() { private GridPane optionsGrid() {
ChoiceBox<Theme> themeSelector = themeSelector(); ChoiceBox<Theme> themeSelector = themeSelector();
themeSelector.setPrefWidth(200); themeSelector.setPrefWidth(200);
themeSelector.disableProperty().bind(colorPalette.contrastCheckerActiveProperty());
// ~ // ~
@ -106,4 +119,17 @@ public class ThemePage extends AbstractPage {
return selector; return selector;
} }
private ContrastCheckerDialog getOrCreateContrastCheckerDialog() {
if (contrastCheckerDialog == null) {
contrastCheckerDialog = new ContrastCheckerDialog(colorPalette.bgBaseColorProperty());
}
contrastCheckerDialog.setOnCloseRequest(() -> {
overlay.removeContent();
overlay.toBack();
});
return contrastCheckerDialog;
}
} }

@ -5,6 +5,7 @@ import atlantafx.sampler.page.AbstractPage;
import atlantafx.sampler.page.SampleBlock; import atlantafx.sampler.page.SampleBlock;
import atlantafx.sampler.theme.ThemeEvent.EventType; import atlantafx.sampler.theme.ThemeEvent.EventType;
import atlantafx.sampler.theme.ThemeManager; import atlantafx.sampler.theme.ThemeManager;
import atlantafx.sampler.util.NodeUtils;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.collections.FXCollections; import javafx.collections.FXCollections;
import javafx.geometry.Pos; import javafx.geometry.Pos;
@ -74,10 +75,8 @@ public class TypographyPage extends AbstractPage {
); );
// if you want to enable quick menu don't forget that // if you want to enable quick menu don't forget that
// font size spinner value have to be updated accordingly // font size spinner value have to be updated accordingly
quickConfigBtn.setVisible(false); NodeUtils.toggleVisibility(quickConfigBtn, false);
quickConfigBtn.setManaged(false); NodeUtils.toggleVisibility(sourceCodeToggleBtn, false);
sourceCodeToggleBtn.setVisible(false);
sourceCodeToggleBtn.setManaged(false);
} }
private ComboBox<String> fontFamilyChooser() { private ComboBox<String> fontFamilyChooser() {

@ -0,0 +1,63 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.util;
import javafx.animation.Interpolator;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.scene.Node;
import javafx.util.Duration;
public final class Animations {
public static final Interpolator EASE = Interpolator.SPLINE(0.25, 0.1, 0.25, 1);
public static Timeline fadeIn(Node node, Duration duration) {
return new Timeline(
new KeyFrame(Duration.millis(0), new KeyValue(node.opacityProperty(), 0, EASE)),
new KeyFrame(duration, new KeyValue(node.opacityProperty(), 1, EASE))
);
}
public static Timeline fadeOut(Node node, Duration duration) {
return new Timeline(
new KeyFrame(Duration.millis(0), new KeyValue(node.opacityProperty(), 1, EASE)),
new KeyFrame(duration, new KeyValue(node.opacityProperty(), 0, EASE))
);
}
public static Timeline zoomIn(Node node, Duration duration) {
return new Timeline(
new KeyFrame(Duration.millis(0),
new KeyValue(node.opacityProperty(), 0, EASE),
new KeyValue(node.scaleXProperty(), 0.3, EASE),
new KeyValue(node.scaleYProperty(), 0.3, EASE),
new KeyValue(node.scaleZProperty(), 0.3, EASE)
),
new KeyFrame(duration,
new KeyValue(node.opacityProperty(), 1, EASE),
new KeyValue(node.scaleXProperty(), 1, EASE),
new KeyValue(node.scaleYProperty(), 1, EASE),
new KeyValue(node.scaleZProperty(), 1, EASE)
)
);
}
public static Timeline zoomOut(Node node, Duration duration) {
return new Timeline(
new KeyFrame(Duration.millis(0),
new KeyValue(node.opacityProperty(), 1, EASE),
new KeyValue(node.scaleXProperty(), 1, EASE),
new KeyValue(node.scaleYProperty(), 1, EASE),
new KeyValue(node.scaleZProperty(), 0.3, EASE)
),
new KeyFrame(duration.divide(2),
new KeyValue(node.opacityProperty(), 0, EASE),
new KeyValue(node.scaleXProperty(), 0.3, EASE),
new KeyValue(node.scaleYProperty(), 0.3, EASE),
new KeyValue(node.scaleZProperty(), 0.3, EASE)
),
new KeyFrame(duration, new KeyValue(node.opacityProperty(), 0, EASE))
);
}
}

@ -7,6 +7,7 @@ import javafx.scene.control.ScrollPane;
import javafx.scene.layout.AnchorPane; import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.ColumnConstraints; import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.Priority; import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import static javafx.scene.layout.Region.USE_COMPUTED_SIZE; import static javafx.scene.layout.Region.USE_COMPUTED_SIZE;
import static javafx.scene.layout.Region.USE_PREF_SIZE; import static javafx.scene.layout.Region.USE_PREF_SIZE;
@ -41,4 +42,14 @@ public final class Containers {
constraints.setHgrow(hgrow); constraints.setHgrow(hgrow);
return constraints; return constraints;
} }
public static void usePrefWidth(Region region) {
region.setMinWidth(USE_PREF_SIZE);
region.setMaxWidth(USE_PREF_SIZE);
}
public static void usePrefHeight(Region region) {
region.setMinHeight(USE_PREF_SIZE);
region.setMaxHeight(USE_PREF_SIZE);
}
} }

@ -54,7 +54,6 @@ public class JColorUtils {
* @param red red hex color in format RR or R * @param red red hex color in format RR or R
* @param green green hex color in format GG or G * @param green green hex color in format GG or G
* @param blue blue hex color in format BB or B * @param blue blue hex color in format BB or B
*
* @return hex color in format #RRGGBB * @return hex color in format #RRGGBB
*/ */
public static String toColor(String red, String green, String blue) { public static String toColor(String red, String green, String blue) {
@ -67,7 +66,6 @@ public class JColorUtils {
* @param red red hex color in format RR or R * @param red red hex color in format RR or R
* @param green green hex color in format GG or G * @param green green hex color in format GG or G
* @param blue blue hex color in format BB or B * @param blue blue hex color in format BB or B
*
* @return hex color in format #RGB or #RRGGBB * @return hex color in format #RGB or #RRGGBB
*/ */
public static String toColorShorthand(String red, String green, public static String toColorShorthand(String red, String green,
@ -82,7 +80,6 @@ public class JColorUtils {
* @param red red hex color in format RR or R * @param red red hex color in format RR or R
* @param green green hex color in format GG or G * @param green green hex color in format GG or G
* @param blue blue hex color in format BB or B * @param blue blue hex color in format BB or B
*
* @return hex color in format #AARRGGBB * @return hex color in format #AARRGGBB
*/ */
public static String toColorWithAlpha(String red, String green, public static String toColorWithAlpha(String red, String green,
@ -102,7 +99,6 @@ public class JColorUtils {
* @param red red hex color in format RR or R * @param red red hex color in format RR or R
* @param green green hex color in format GG or G * @param green green hex color in format GG or G
* @param blue blue hex color in format BB or B * @param blue blue hex color in format BB or B
*
* @return hex color in format #ARGB or #AARRGGBB * @return hex color in format #ARGB or #AARRGGBB
*/ */
public static String toColorShorthandWithAlpha(String red, String green, public static String toColorShorthandWithAlpha(String red, String green,
@ -117,7 +113,6 @@ public class JColorUtils {
* @param green green hex color in format GG or G * @param green green hex color in format GG or G
* @param blue blue hex color in format BB or B * @param blue blue hex color in format BB or B
* @param alpha alpha hex color in format AA or A, null to not include alpha * @param alpha alpha hex color in format AA or A, null to not include alpha
*
* @return hex color in format #AARRGGBB or #RRGGBB * @return hex color in format #AARRGGBB or #RRGGBB
*/ */
public static String toColorWithAlpha(String red, String green, String blue, public static String toColorWithAlpha(String red, String green, String blue,
@ -144,7 +139,6 @@ public class JColorUtils {
* @param green green hex color in format GG or G * @param green green hex color in format GG or G
* @param blue blue hex color in format BB or B * @param blue blue hex color in format BB or B
* @param alpha alpha hex color in format AA or A, null to not include alpha * @param alpha alpha hex color in format AA or A, null to not include alpha
*
* @return hex color in format #ARGB, #RGB, #AARRGGBB, or #RRGGBB * @return hex color in format #ARGB, #RGB, #AARRGGBB, or #RRGGBB
*/ */
public static String toColorShorthandWithAlpha(String red, String green, public static String toColorShorthandWithAlpha(String red, String green,
@ -158,7 +152,6 @@ public class JColorUtils {
* @param red red integer color inclusively between 0 and 255 * @param red red integer color inclusively between 0 and 255
* @param green green integer color inclusively between 0 and 255 * @param green green integer color inclusively between 0 and 255
* @param blue blue integer color inclusively between 0 and 255 * @param blue blue integer color inclusively between 0 and 255
*
* @return integer color * @return integer color
*/ */
public static int toColor(int red, int green, int blue) { public static int toColor(int red, int green, int blue) {
@ -172,7 +165,6 @@ public class JColorUtils {
* @param red red integer color inclusively between 0 and 255 * @param red red integer color inclusively between 0 and 255
* @param green green integer color inclusively between 0 and 255 * @param green green integer color inclusively between 0 and 255
* @param blue blue integer color inclusively between 0 and 255 * @param blue blue integer color inclusively between 0 and 255
*
* @return integer color * @return integer color
*/ */
public static int toColorWithAlpha(int red, int green, int blue) { public static int toColorWithAlpha(int red, int green, int blue) {
@ -187,7 +179,6 @@ public class JColorUtils {
* @param blue blue integer color inclusively between 0 and 255 * @param blue blue integer color inclusively between 0 and 255
* @param alpha alpha integer color inclusively between 0 and 255, -1 to not * @param alpha alpha integer color inclusively between 0 and 255, -1 to not
* include alpha * include alpha
*
* @return integer color * @return integer color
*/ */
public static int toColorWithAlpha(int red, int green, int blue, public static int toColorWithAlpha(int red, int green, int blue,
@ -207,7 +198,6 @@ public class JColorUtils {
* Convert the RGB integer to a hex single color * Convert the RGB integer to a hex single color
* *
* @param color integer color inclusively between 0 and 255 * @param color integer color inclusively between 0 and 255
*
* @return hex single color in format FF * @return hex single color in format FF
*/ */
public static String toHex(int color) { public static String toHex(int color) {
@ -223,7 +213,6 @@ public class JColorUtils {
* Convert the arithmetic RGB float to a hex single color * Convert the arithmetic RGB float to a hex single color
* *
* @param color float color inclusively between 0.0 and 1.0 * @param color float color inclusively between 0.0 and 1.0
*
* @return hex single color in format FF * @return hex single color in format FF
*/ */
public static String toHex(float color) { public static String toHex(float color) {
@ -234,7 +223,6 @@ public class JColorUtils {
* Convert the hex single color to a RGB integer * Convert the hex single color to a RGB integer
* *
* @param color hex single color in format FF or F * @param color hex single color in format FF or F
*
* @return integer color inclusively between 0 and 255 * @return integer color inclusively between 0 and 255
*/ */
public static int toRGB(String color) { public static int toRGB(String color) {
@ -249,7 +237,6 @@ public class JColorUtils {
* Convert the arithmetic RGB float to a RGB integer * Convert the arithmetic RGB float to a RGB integer
* *
* @param color float color inclusively between 0.0 and 1.0 * @param color float color inclusively between 0.0 and 1.0
*
* @return integer color inclusively between 0 and 255 * @return integer color inclusively between 0 and 255
*/ */
public static int toRGB(float color) { public static int toRGB(float color) {
@ -261,7 +248,6 @@ public class JColorUtils {
* Convert the hex single color to an arithmetic RGB float * Convert the hex single color to an arithmetic RGB float
* *
* @param color hex single color in format FF or F * @param color hex single color in format FF or F
*
* @return float color inclusively between 0.0 and 1.0 * @return float color inclusively between 0.0 and 1.0
*/ */
public static float toArithmeticRGB(String color) { public static float toArithmeticRGB(String color) {
@ -272,7 +258,6 @@ public class JColorUtils {
* Convert the RGB integer to an arithmetic RGB float * Convert the RGB integer to an arithmetic RGB float
* *
* @param color integer color inclusively between 0 and 255 * @param color integer color inclusively between 0 and 255
*
* @return float color inclusively between 0.0 and 1.0 * @return float color inclusively between 0.0 and 1.0
*/ */
public static float toArithmeticRGB(int color) { public static float toArithmeticRGB(int color) {
@ -287,7 +272,6 @@ public class JColorUtils {
* @param red red color inclusively between 0.0 and 1.0 * @param red red color inclusively between 0.0 and 1.0
* @param green green color inclusively between 0.0 and 1.0 * @param green green color inclusively between 0.0 and 1.0
* @param blue blue color inclusively between 0.0 and 1.0 * @param blue blue color inclusively between 0.0 and 1.0
*
* @return HSL array where: 0 = hue, 1 = saturation, 2 = lightness * @return HSL array where: 0 = hue, 1 = saturation, 2 = lightness
*/ */
public static float[] toHSL(float red, float green, float blue) { public static float[] toHSL(float red, float green, float blue) {
@ -342,7 +326,6 @@ public class JColorUtils {
* @param red red color inclusively between 0 and 255 * @param red red color inclusively between 0 and 255
* @param green green color inclusively between 0 and 255 * @param green green color inclusively between 0 and 255
* @param blue blue color inclusively between 0 and 255 * @param blue blue color inclusively between 0 and 255
*
* @return HSL array where: 0 = hue, 1 = saturation, 2 = lightness * @return HSL array where: 0 = hue, 1 = saturation, 2 = lightness
*/ */
public static float[] toHSL(int red, int green, int blue) { public static float[] toHSL(int red, int green, int blue) {
@ -357,7 +340,6 @@ public class JColorUtils {
* @param hue hue value inclusively between 0.0 and 360.0 * @param hue hue value inclusively between 0.0 and 360.0
* @param saturation saturation inclusively between 0.0 and 1.0 * @param saturation saturation inclusively between 0.0 and 1.0
* @param lightness lightness inclusively between 0.0 and 1.0 * @param lightness lightness inclusively between 0.0 and 1.0
*
* @return arithmetic RGB array where: 0 = red, 1 = green, 2 = blue * @return arithmetic RGB array where: 0 = red, 1 = green, 2 = blue
*/ */
public static float[] toArithmeticRGB(float hue, float saturation, public static float[] toArithmeticRGB(float hue, float saturation,
@ -389,7 +371,6 @@ public class JColorUtils {
* @param hue hue value inclusively between 0.0 and 360.0 * @param hue hue value inclusively between 0.0 and 360.0
* @param saturation saturation inclusively between 0.0 and 1.0 * @param saturation saturation inclusively between 0.0 and 1.0
* @param lightness lightness inclusively between 0.0 and 1.0 * @param lightness lightness inclusively between 0.0 and 1.0
*
* @return RGB integer array where: 0 = red, 1 = green, 2 = blue * @return RGB integer array where: 0 = red, 1 = green, 2 = blue
*/ */
public static int[] toRGB(float hue, float saturation, float lightness) { public static int[] toRGB(float hue, float saturation, float lightness) {
@ -403,7 +384,6 @@ public class JColorUtils {
* @param t1 t1 * @param t1 t1
* @param t2 t2 * @param t2 t2
* @param hue hue * @param hue hue
*
* @return arithmetic RGB value * @return arithmetic RGB value
*/ */
private static float hslConvert(float t1, float t2, float hue) { private static float hslConvert(float t1, float t2, float hue) {
@ -430,7 +410,6 @@ public class JColorUtils {
* Get the hex red color from the hex string * Get the hex red color from the hex string
* *
* @param hex hex color * @param hex hex color
*
* @return hex red color in format RR * @return hex red color in format RR
*/ */
public static String getRed(String hex) { public static String getRed(String hex) {
@ -441,7 +420,6 @@ public class JColorUtils {
* Get the hex green color from the hex string * Get the hex green color from the hex string
* *
* @param hex hex color * @param hex hex color
*
* @return hex green color in format GG * @return hex green color in format GG
*/ */
public static String getGreen(String hex) { public static String getGreen(String hex) {
@ -452,7 +430,6 @@ public class JColorUtils {
* Get the hex blue color from the hex string * Get the hex blue color from the hex string
* *
* @param hex hex color * @param hex hex color
*
* @return hex blue color in format BB * @return hex blue color in format BB
*/ */
public static String getBlue(String hex) { public static String getBlue(String hex) {
@ -463,7 +440,6 @@ public class JColorUtils {
* Get the hex alpha color from the hex string if it exists * Get the hex alpha color from the hex string if it exists
* *
* @param hex hex color * @param hex hex color
*
* @return hex alpha color in format AA or null * @return hex alpha color in format AA or null
*/ */
public static String getAlpha(String hex) { public static String getAlpha(String hex) {
@ -475,7 +451,6 @@ public class JColorUtils {
* *
* @param hex hex color * @param hex hex color
* @param colorIndex red=0, green=1, blue=2, alpha=-1 * @param colorIndex red=0, green=1, blue=2, alpha=-1
*
* @return hex single color in format FF or null * @return hex single color in format FF or null
*/ */
private static String getHexSingle(String hex, int colorIndex) { private static String getHexSingle(String hex, int colorIndex) {
@ -512,7 +487,6 @@ public class JColorUtils {
* Get the red color from color integer * Get the red color from color integer
* *
* @param color color integer * @param color color integer
*
* @return red color * @return red color
*/ */
public static int getRed(int color) { public static int getRed(int color) {
@ -523,7 +497,6 @@ public class JColorUtils {
* Get the green color from color integer * Get the green color from color integer
* *
* @param color color integer * @param color color integer
*
* @return green color * @return green color
*/ */
public static int getGreen(int color) { public static int getGreen(int color) {
@ -534,7 +507,6 @@ public class JColorUtils {
* Get the blue color from color integer * Get the blue color from color integer
* *
* @param color color integer * @param color color integer
*
* @return blue color * @return blue color
*/ */
public static int getBlue(int color) { public static int getBlue(int color) {
@ -545,7 +517,6 @@ public class JColorUtils {
* Get the alpha color from color integer * Get the alpha color from color integer
* *
* @param color color integer * @param color color integer
*
* @return alpha color * @return alpha color
*/ */
public static int getAlpha(int color) { public static int getAlpha(int color) {
@ -556,7 +527,6 @@ public class JColorUtils {
* Shorthand the hex color if possible * Shorthand the hex color if possible
* *
* @param color hex color * @param color hex color
*
* @return shorthand hex color or original value * @return shorthand hex color or original value
*/ */
public static String shorthandHex(String color) { public static String shorthandHex(String color) {
@ -589,7 +559,6 @@ public class JColorUtils {
* Expand the hex if it is in shorthand * Expand the hex if it is in shorthand
* *
* @param color hex color * @param color hex color
*
* @return expanded hex color or original value * @return expanded hex color or original value
*/ */
public static String expandShorthandHex(String color) { public static String expandShorthandHex(String color) {
@ -615,7 +584,6 @@ public class JColorUtils {
* Shorthand the hex single color if possible * Shorthand the hex single color if possible
* *
* @param color hex single color * @param color hex single color
*
* @return shorthand hex color or original value * @return shorthand hex color or original value
*/ */
public static String shorthandHexSingle(String color) { public static String shorthandHexSingle(String color) {
@ -632,7 +600,6 @@ public class JColorUtils {
* Expand the hex single if it is in shorthand * Expand the hex single if it is in shorthand
* *
* @param color hex single color * @param color hex single color
*
* @return expanded hex color or original value * @return expanded hex color or original value
*/ */
public static String expandShorthandHexSingle(String color) { public static String expandShorthandHexSingle(String color) {
@ -647,7 +614,6 @@ public class JColorUtils {
* Check if the hex color value is valid * Check if the hex color value is valid
* *
* @param color hex color * @param color hex color
*
* @return true if valid * @return true if valid
*/ */
public static boolean isValidHex(String color) { public static boolean isValidHex(String color) {
@ -671,7 +637,6 @@ public class JColorUtils {
* Check if the hex single color value is valid * Check if the hex single color value is valid
* *
* @param color hex single color * @param color hex single color
*
* @return true if valid * @return true if valid
*/ */
public static boolean isValidHexSingle(String color) { public static boolean isValidHexSingle(String color) {
@ -694,7 +659,6 @@ public class JColorUtils {
* Check if the RGB integer color is valid, inclusively between 0 and 255 * Check if the RGB integer color is valid, inclusively between 0 and 255
* *
* @param color decimal color * @param color decimal color
*
* @return true if valid * @return true if valid
*/ */
public static boolean isValidRGB(int color) { public static boolean isValidRGB(int color) {
@ -719,7 +683,6 @@ public class JColorUtils {
* and 1.0 * and 1.0
* *
* @param color decimal color * @param color decimal color
*
* @return true if valid * @return true if valid
*/ */
public static boolean isValidArithmeticRGB(float color) { public static boolean isValidArithmeticRGB(float color) {
@ -745,7 +708,6 @@ public class JColorUtils {
* 360.0 * 360.0
* *
* @param hue hue value * @param hue hue value
*
* @return true if valid * @return true if valid
*/ */
public static boolean isValidHue(float hue) { public static boolean isValidHue(float hue) {
@ -770,7 +732,6 @@ public class JColorUtils {
* and 1.0 * and 1.0
* *
* @param saturation saturation value * @param saturation saturation value
*
* @return true if valid * @return true if valid
*/ */
public static boolean isValidSaturation(float saturation) { public static boolean isValidSaturation(float saturation) {
@ -796,7 +757,6 @@ public class JColorUtils {
* and 1.0 * and 1.0
* *
* @param lightness lightness value * @param lightness lightness value
*
* @return true if valid * @return true if valid
*/ */
public static boolean isValidLightness(float lightness) { public static boolean isValidLightness(float lightness) {
@ -899,4 +859,21 @@ public class JColorUtils {
fgColor.getBlue(), fgColor.getBlue(),
}; };
} }
public static float[] toHSL(Color color) {
return JColorUtils.toHSL(
(float) color.getRed(),
(float) color.getGreen(),
(float) color.getBlue()
);
}
public static String toHexWithAlpha(Color color) {
return JColor.color(
(float) color.getRed(),
(float) color.getGreen(),
(float) color.getBlue(),
(float) color.getOpacity()
).getColorHexShorthandWithAlpha();
}
} }

@ -0,0 +1,38 @@
/* SPDX-License-Identifier: MIT */
package atlantafx.sampler.util;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import java.util.List;
public final class NodeUtils {
public static void toggleVisibility(Node node, boolean on) {
node.setVisible(on);
node.setManaged(on);
}
public static boolean isDoubleClick(MouseEvent e) {
return e.getButton().equals(MouseButton.PRIMARY) && e.getClickCount() == 2;
}
public static <T> T getChildByIndex(Parent parent, int index, Class<T> contentType) {
List<Node> children = parent.getChildrenUnmodifiable();
if (index < 0 || index >= children.size()) { return null; }
Node node = children.get(index);
return contentType.isInstance(node) ? contentType.cast(node) : null;
}
public static boolean isDescendant(Node ancestor, Node descendant) {
if (ancestor == null) { return true; }
while (descendant != null) {
if (descendant == ancestor) { return true; }
descendant = descendant.getParent();
}
return false;
}
}

@ -3,3 +3,4 @@
@use "sidebar"; @use "sidebar";
@use "page"; @use "page";
@use "components"; @use "components";
@use "overlay";

@ -0,0 +1,31 @@
.overlay {
-fx-background-color: transparent;
>.scroll-pane {
>.viewport {
>* {
>.scrollable-content {
-fx-background-color: rgba(0, 0, 0, 0.5);
}
}
}
}
}
.overlay-dialog {
-fx-background-color: -color-bg-default;
-fx-background-radius: 5px;
-fx-border-radius: 5px;
-fx-border-width: 1px;
-fx-border-color: -color-border-default;
>.header {
-fx-padding: 10px 5px 5px 20px;
}
>.footer {
-fx-border-width: 1 0 0 0;
-fx-border-color: -color-border-default;
-fx-padding: 10;
}
}

@ -45,88 +45,4 @@ $color-wsag-fg: white;
} }
} }
} }
>.contrast-checker-area {
-fx-padding: 0 0 0 -20px;
>.contrast-checker {
-fx-background-color: -color-contrast-checker-bg;
-fx-hgap: 40px;
-fx-vgap: 20px;
-fx-padding: 20px 20px 40px 20px;
.label {
-fx-text-fill: -color-contrast-checker-fg;
}
.text-field {
-fx-background-insets: 0;
-fx-background-color: transparent;
-fx-background-radius: 0;
-fx-text-fill: -color-contrast-checker-fg;
-fx-border-color: -color-contrast-checker-fg;
-fx-border-width: 0 0 1 0;
}
.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;
}
.ikonli-font-icon {
-fx-icon-color: -color-contrast-checker-fg;
-fx-fill: -color-contrast-checker-fg;
}
.slider {
>.thumb {
-fx-background-color: -color-contrast-checker-fg;
}
>.track {
-fx-background-color: transparent, -color-contrast-checker-fg;
-fx-opacity: 0.5;
}
}
.font-box {
>.text {
-fx-font-size: 4em;
}
>.ratio {
-fx-font-size: 2em;
}
}
>.wsag-box>*>.wsag-label {
-fx-padding: 0.5em 1em 0.5em 1em;
-fx-background-color: $color-wsag-bg-failed;
-fx-background-radius: 6px;
-fx-text-fill: $color-wsag-fg;
&:passed {
-fx-background-color: $color-wsag-bg-passed;
}
>.ikonli-font-icon {
-fx-fill: $color-wsag-fg;
-fx-icon-color: $color-wsag-fg;
}
}
}
}
} }

@ -0,0 +1,112 @@
@use "color-palette" as palette;
.contrast-checker {
-fx-background-color: -color-contrast-checker-bg;
-fx-hgap: 40px;
-fx-vgap: 20px;
-fx-padding: 20px;
.label {
-fx-text-fill: -color-contrast-checker-fg;
}
.text-field {
-fx-background-insets: 0;
-fx-background-color: transparent;
-fx-background-radius: 0;
-fx-text-fill: -color-contrast-checker-fg;
-fx-border-color: -color-contrast-checker-fg;
-fx-border-width: 0 0 1 0;
}
.button {
-color-button-bg: transparent;
-color-button-fg: -color-contrast-checker-fg;
-color-button-border: transparent;
-color-button-bg-hover: transparent;
-color-button-fg-hover: -color-contrast-checker-fg;
-color-button-border-hover: transparent;
-color-button-bg-focused: transparent;
-color-button-fg-focused: -color-contrast-checker-fg;
-color-button-border-focused: transparent;
-color-button-bg-pressed: transparent;
-color-button-fg-pressed: -color-contrast-checker-fg;
-color-button-border-pressed: transparent;
-fx-border-width: 1px;
-fx-border-color: -color-contrast-checker-fg;
-fx-border-radius: 4px;
&:armed,
&:focused:armed {
-fx-border-color: transparent;
}
}
.ikonli-font-icon {
-fx-icon-color: -color-contrast-checker-fg;
-fx-fill: -color-contrast-checker-fg;
}
.slider {
>.thumb {
-fx-background-color: -color-contrast-checker-fg;
}
>.track {
-fx-background-color: transparent, -color-contrast-checker-fg;
-fx-opacity: 0.5;
}
}
.font-box {
-fx-padding: 0 40px 0 0;
>.text {
-fx-font-size: 4em;
}
>.ratio {
-fx-font-size: 2em;
}
}
.wsag-box>*>.wsag-label {
-fx-padding: 0.5em 1em 0.5em 1em;
-fx-background-color: palette.$color-wsag-bg-failed;
-fx-background-radius: 4px;
-fx-text-fill: palette.$color-wsag-fg;
&:passed {
-fx-background-color: palette.$color-wsag-bg-passed;
}
>.ikonli-font-icon {
-fx-fill: palette.$color-wsag-fg;
-fx-icon-color: palette.$color-wsag-fg;
}
}
}
.contrast-checker-dialog {
-color-dialog-bg: white;
-color-dialog-fg: black;
>.header {
-fx-background-color: -color-dialog-bg;
>.title {
-fx-text-fill: -color-dialog-fg;
}
>.close-button {
-color-button-fg: -color-dialog-fg;
-color-button-bg-hover: derive(-color-dialog-bg, 10%);
}
}
-fx-border-color: -color-dialog-bg;
}

@ -1,4 +1,5 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@use "color-palette"; @use "color-palette";
@use "contrast-checker";
@use "quick-config-menu"; @use "quick-config-menu";