diff --git a/sampler/pom.xml b/sampler/pom.xml index 41a3d9f..3d4659a 100755 --- a/sampler/pom.xml +++ b/sampler/pom.xml @@ -109,12 +109,6 @@ src/main/java/atlantafx/sampler/page atlantafx/sampler/page false - - **/AbstractPage.java - **/CodeViewer.java - **/Page.java - **/SampleBlock.java - diff --git a/sampler/src/main/java/atlantafx/sampler/layout/ApplicationWindow.java b/sampler/src/main/java/atlantafx/sampler/layout/ApplicationWindow.java index 9bfe80c..458988d 100755 --- a/sampler/src/main/java/atlantafx/sampler/layout/ApplicationWindow.java +++ b/sampler/src/main/java/atlantafx/sampler/layout/ApplicationWindow.java @@ -1,61 +1,14 @@ /* 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; - -public class ApplicationWindow extends BorderPane { +public class ApplicationWindow extends StackPane { public ApplicationWindow() { - 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); + getChildren().setAll( + new Overlay(), + new MainLayer() + ); } } diff --git a/sampler/src/main/java/atlantafx/sampler/layout/MainLayer.java b/sampler/src/main/java/atlantafx/sampler/layout/MainLayer.java new file mode 100644 index 0000000..fd5f31c --- /dev/null +++ b/sampler/src/main/java/atlantafx/sampler/layout/MainLayer.java @@ -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); + } +} diff --git a/sampler/src/main/java/atlantafx/sampler/layout/Overlay.java b/sampler/src/main/java/atlantafx/sampler/layout/Overlay.java new file mode 100644 index 0000000..d1f2aeb --- /dev/null +++ b/sampler/src/main/java/atlantafx/sampler/layout/Overlay.java @@ -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 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(); + } +} diff --git a/sampler/src/main/java/atlantafx/sampler/layout/Sidebar.java b/sampler/src/main/java/atlantafx/sampler/layout/Sidebar.java index beb5c51..b325539 100644 --- a/sampler/src/main/java/atlantafx/sampler/layout/Sidebar.java +++ b/sampler/src/main/java/atlantafx/sampler/layout/Sidebar.java @@ -30,7 +30,7 @@ import java.util.function.Consumer; import static javafx.scene.layout.Priority.ALWAYS; -public class Sidebar extends VBox { +class Sidebar extends VBox { private static final PseudoClass SELECTED = PseudoClass.getPseudoClass("selected"); diff --git a/sampler/src/main/java/atlantafx/sampler/page/AbstractPage.java b/sampler/src/main/java/atlantafx/sampler/page/AbstractPage.java index 84242f0..af13bbd 100755 --- a/sampler/src/main/java/atlantafx/sampler/page/AbstractPage.java +++ b/sampler/src/main/java/atlantafx/sampler/page/AbstractPage.java @@ -4,6 +4,7 @@ package atlantafx.sampler.page; import atlantafx.base.controls.Popover; import atlantafx.base.controls.Spacer; import atlantafx.base.theme.Styles; +import atlantafx.sampler.layout.Overlay; import atlantafx.sampler.theme.ThemeManager; import javafx.event.ActionEvent; import javafx.event.EventHandler; @@ -48,6 +49,7 @@ public abstract class AbstractPage extends BorderPane implements Page { protected StackPane codeViewerWrapper; protected CodeViewer codeViewer; protected VBox userContent; + protected Overlay overlay; protected boolean isRendered = false; protected AbstractPage() { @@ -96,8 +98,8 @@ public abstract class AbstractPage extends BorderPane implements Page { var scrollPane = new ScrollPane(userContentWrapper); setScrollConstraints(scrollPane, - ScrollPane.ScrollBarPolicy.AS_NEEDED, true, - ScrollPane.ScrollBarPolicy.AS_NEEDED, true + ScrollPane.ScrollBarPolicy.AS_NEEDED, true, + ScrollPane.ScrollBarPolicy.AS_NEEDED, true ); scrollPane.setMaxHeight(10_000); @@ -129,9 +131,16 @@ public abstract class AbstractPage extends BorderPane implements Page { // Some properties can only be obtained after node placed // 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) { var content = new QuickConfigMenu(); content.setExitHandler(() -> quickConfigPopover.hide()); diff --git a/sampler/src/main/java/atlantafx/sampler/page/OverlayDialog.java b/sampler/src/main/java/atlantafx/sampler/page/OverlayDialog.java new file mode 100644 index 0000000..64f2d05 --- /dev/null +++ b/sampler/src/main/java/atlantafx/sampler/page/OverlayDialog.java @@ -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 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; + } +} 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 0195b5e..0f83cc2 100644 --- a/sampler/src/main/java/atlantafx/sampler/page/general/ColorBlock.java +++ b/sampler/src/main/java/atlantafx/sampler/page/general/ColorBlock.java @@ -16,7 +16,7 @@ import org.kordamp.ikonli.material2.Material2AL; 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.getColorLuminance; @@ -71,8 +71,8 @@ class ColorBlock extends VBox { if (bgFill == null) { return; } toggleHover(true); - expandIcon.setFill( - getColorLuminance(flattenColor(bgBaseColor.get(), 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)); 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 c6d50a1..349c470 100644 --- a/sampler/src/main/java/atlantafx/sampler/page/general/ColorPalette.java +++ b/sampler/src/main/java/atlantafx/sampler/page/general/ColorPalette.java @@ -4,82 +4,42 @@ 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.ReadOnlyObjectProperty; import javafx.beans.property.ReadOnlyObjectWrapper; import javafx.geometry.Pos; -import javafx.scene.control.Button; import javafx.scene.control.Label; 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; import java.time.Duration; -import java.util.ArrayList; -import java.util.List; -import java.util.Timer; -import java.util.TimerTask; +import java.util.*; import java.util.function.Consumer; class ColorPalette extends VBox { - 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); + private final Consumer colorBlockActionHandler; - public ReadOnlyBooleanProperty contrastCheckerActiveProperty() { - return contrastCheckerActive.getReadOnlyProperty(); - } - - public ColorPalette() { + public ColorPalette(Consumer blockClickedHandler) { super(); + + this.colorBlockActionHandler = Objects.requireNonNull(blockClickedHandler); createView(); } private void createView() { - headerLabel = new Label("Color Palette"); + var headerLabel = new Label("Color Palette"); 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(); - headerBox.getChildren().setAll(headerLabel, new Spacer(), backBtn); + headerBox.getChildren().setAll(headerLabel, new Spacer()); headerBox.setAlignment(Pos.CENTER_LEFT); headerBox.getStyleClass().add("header"); - contrastCheckerArea = new VBox(); - contrastCheckerArea.getStyleClass().add("contrast-checker-area"); - - colorGrid = colorGrid(); + var colorGrid = colorGrid(); backgroundProperty().addListener((obs, old, val) -> bgBaseColor.set( val != null && !val.getFills().isEmpty() ? (Color) val.getFills().get(0).getFill() : Color.WHITE @@ -132,28 +92,6 @@ class ColorPalette extends VBox { 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. // 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. @@ -165,4 +103,8 @@ class ColorPalette extends VBox { } }, delay.toMillis()); } + + public ReadOnlyObjectProperty bgBaseColorProperty() { + return bgBaseColor.getReadOnlyProperty(); + } } diff --git a/sampler/src/main/java/atlantafx/sampler/page/general/ColorContrastChecker.java b/sampler/src/main/java/atlantafx/sampler/page/general/ContrastChecker.java similarity index 84% rename from sampler/src/main/java/atlantafx/sampler/page/general/ColorContrastChecker.java rename to sampler/src/main/java/atlantafx/sampler/page/general/ContrastChecker.java index 9c7cb25..7f4e4b3 100644 --- a/sampler/src/main/java/atlantafx/sampler/page/general/ColorContrastChecker.java +++ b/sampler/src/main/java/atlantafx/sampler/page/general/ContrastChecker.java @@ -2,6 +2,7 @@ package atlantafx.sampler.page.general; import atlantafx.base.controls.CustomTextField; +import atlantafx.base.controls.Spacer; import atlantafx.base.theme.Styles; import atlantafx.sampler.theme.ThemeManager; import atlantafx.sampler.util.JColor; @@ -33,16 +34,15 @@ import org.kordamp.ikonli.material2.Material2AL; import java.util.Objects; 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/ -public class ColorContrastChecker extends GridPane { +public class ContrastChecker 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; + public static final double CONTRAST_RATIO_THRESHOLD = 1.5; + public static final double LUMINANCE_THRESHOLD = 0.55; + public static final PseudoClass PASSED = PseudoClass.getPseudoClass("passed"); private static final int SLIDER_WIDTH = 300; @@ -65,7 +65,7 @@ public class ColorContrastChecker extends GridPane { private Slider fgLightnessSlider; private Slider fgAlphaSlider; - public ColorContrastChecker(ReadOnlyObjectProperty bgBaseColor) { + public ContrastChecker(ReadOnlyObjectProperty bgBaseColor) { super(); this.bgBaseColor = bgBaseColor; @@ -98,6 +98,19 @@ public class ColorContrastChecker extends GridPane { public Color getFgColor() { return fgColor.colorProperty().get(); } + public ReadOnlyObjectProperty bgColorProperty() { return bgColor.colorProperty(); } + + public ReadOnlyObjectProperty 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() { var textLabel = new Label("Aa"); textLabel.getStyleClass().add("text"); @@ -268,21 +281,22 @@ public class ColorContrastChecker extends GridPane { var applyBtn = new Button("Apply"); applyBtn.setOnAction(e -> { - var tm = ThemeManager.getInstance(); + var tm = ThemeManager.getInstance(); tm.setColor(getBgColorName(), bgColor.getColor()); tm.setColor(getFgColorName(), fgColor.getColor()); tm.reloadCustomCSS(); }); - var controlsBox = new HBox(20, flattenBtn, applyBtn); + var controlsBox = new HBox(20, new Spacer(), flattenBtn, applyBtn); controlsBox.setAlignment(Pos.CENTER_LEFT); + controlsBox.setPadding(new Insets(10, 0, 0, 0)); // ~ getStyleClass().add("contrast-checker"); // 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(bgColorNameLabel, 0, 2); add(bgTextField, 0, 3); @@ -298,7 +312,6 @@ public class ColorContrastChecker extends GridPane { add(controlsBox, 0, 12, REMAINING, 1); // column 1 - add(wsagBox, 1, 0); add(new Label("Foreground Color"), 1, 1); add(fgColorNameLabel, 1, 2); add(fgTextField, 1, 3); @@ -321,40 +334,14 @@ public class ColorContrastChecker extends GridPane { } private void updateStyle() { - float[] bg = bgColor.getRGBAColor(); - float[] fg = fgColor.getRGBAColor(); - - // 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); + setStyle(String.format("-color-contrast-checker-bg:%s;-color-contrast-checker-fg:%s;", + JColorUtils.toHexWithAlpha(bgColor.getColor()), + JColorUtils.toHexWithAlpha(getSafeFgColor()) + )); } private void setBackground(Color color) { - float[] hsl = JColorUtils.toHSL( - (float) color.getRed(), - (float) color.getGreen(), - (float) color.getBlue() - ); + float[] hsl = JColorUtils.toHSL(color); bgHueSlider.setValue(hsl[0]); bgSaturationSlider.setValue(hsl[1]); bgLightnessSlider.setValue(hsl[2]); @@ -362,11 +349,7 @@ public class ColorContrastChecker extends GridPane { } private void setForeground(Color color) { - float[] hsl = JColorUtils.toHSL( - (float) color.getRed(), - (float) color.getGreen(), - (float) color.getBlue() - ); + float[] hsl = JColorUtils.toHSL(color); fgHueSlider.setValue(hsl[0]); fgSaturationSlider.setValue(hsl[1]); fgLightnessSlider.setValue(hsl[2]); @@ -412,7 +395,7 @@ public class ColorContrastChecker extends GridPane { 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); + return JColorUtils.getContrastRatio(luminance1, luminance2); } /////////////////////////////////////////////////////////////////////////// @@ -435,11 +418,7 @@ public class ColorContrastChecker extends GridPane { } public void setColor(Color color) { - float[] hsl = JColorUtils.toHSL( - (float) color.getRed(), - (float) color.getGreen(), - (float) color.getBlue() - ); + float[] hsl = JColorUtils.toHSL(color); 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() { float[] hsl = new float[] { getHue(), getSaturation(), getLightness() }; return JColor.color(hsl, getAlpha()).getColorHexWithAlpha(); diff --git a/sampler/src/main/java/atlantafx/sampler/page/general/ContrastCheckerDialog.java b/sampler/src/main/java/atlantafx/sampler/page/general/ContrastCheckerDialog.java new file mode 100644 index 0000000..c75a59c --- /dev/null +++ b/sampler/src/main/java/atlantafx/sampler/page/general/ContrastCheckerDialog.java @@ -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 { + + private final ContrastChecker contrastChecker; + + public ContrastCheckerDialog(ReadOnlyObjectProperty 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; + } +} 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 b05e0c4..4dc1729 100755 --- a/sampler/src/main/java/atlantafx/sampler/page/general/ThemePage.java +++ b/sampler/src/main/java/atlantafx/sampler/page/general/ThemePage.java @@ -5,6 +5,8 @@ import atlantafx.base.theme.Theme; import atlantafx.sampler.page.AbstractPage; import atlantafx.sampler.theme.ThemeEvent.EventType; import atlantafx.sampler.theme.ThemeManager; +import atlantafx.sampler.util.NodeUtils; +import javafx.geometry.HPos; import javafx.scene.control.ChoiceBox; import javafx.scene.control.Label; import javafx.scene.layout.GridPane; @@ -12,12 +14,26 @@ import javafx.util.StringConverter; import java.time.Duration; import java.util.Objects; +import java.util.function.Consumer; public class ThemePage extends AbstractPage { public static final String NAME = "Theme"; - private final ColorPalette colorPalette = new ColorPalette(); + private final Consumer 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 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 // theme selection choice box have to be updated accordingly - quickConfigBtn.setVisible(false); - quickConfigBtn.setManaged(false); - sourceCodeToggleBtn.setVisible(false); - sourceCodeToggleBtn.setManaged(false); + NodeUtils.toggleVisibility(quickConfigBtn, false); + NodeUtils.toggleVisibility(sourceCodeToggleBtn, false); } private GridPane optionsGrid() { ChoiceBox themeSelector = themeSelector(); themeSelector.setPrefWidth(200); - themeSelector.disableProperty().bind(colorPalette.contrastCheckerActiveProperty()); // ~ @@ -106,4 +119,17 @@ public class ThemePage extends AbstractPage { return selector; } + + private ContrastCheckerDialog getOrCreateContrastCheckerDialog() { + if (contrastCheckerDialog == null) { + contrastCheckerDialog = new ContrastCheckerDialog(colorPalette.bgBaseColorProperty()); + } + + contrastCheckerDialog.setOnCloseRequest(() -> { + overlay.removeContent(); + overlay.toBack(); + }); + + return contrastCheckerDialog; + } } diff --git a/sampler/src/main/java/atlantafx/sampler/page/general/TypographyPage.java b/sampler/src/main/java/atlantafx/sampler/page/general/TypographyPage.java index 6576175..8c3584e 100755 --- a/sampler/src/main/java/atlantafx/sampler/page/general/TypographyPage.java +++ b/sampler/src/main/java/atlantafx/sampler/page/general/TypographyPage.java @@ -5,6 +5,7 @@ import atlantafx.sampler.page.AbstractPage; import atlantafx.sampler.page.SampleBlock; import atlantafx.sampler.theme.ThemeEvent.EventType; import atlantafx.sampler.theme.ThemeManager; +import atlantafx.sampler.util.NodeUtils; import javafx.application.Platform; import javafx.collections.FXCollections; import javafx.geometry.Pos; @@ -74,10 +75,8 @@ public class TypographyPage extends AbstractPage { ); // if you want to enable quick menu don't forget that // font size spinner value have to be updated accordingly - quickConfigBtn.setVisible(false); - quickConfigBtn.setManaged(false); - sourceCodeToggleBtn.setVisible(false); - sourceCodeToggleBtn.setManaged(false); + NodeUtils.toggleVisibility(quickConfigBtn, false); + NodeUtils.toggleVisibility(sourceCodeToggleBtn, false); } private ComboBox fontFamilyChooser() { diff --git a/sampler/src/main/java/atlantafx/sampler/util/Animations.java b/sampler/src/main/java/atlantafx/sampler/util/Animations.java new file mode 100644 index 0000000..76d048e --- /dev/null +++ b/sampler/src/main/java/atlantafx/sampler/util/Animations.java @@ -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)) + ); + } +} diff --git a/sampler/src/main/java/atlantafx/sampler/util/Containers.java b/sampler/src/main/java/atlantafx/sampler/util/Containers.java index e418899..73ce1ac 100755 --- a/sampler/src/main/java/atlantafx/sampler/util/Containers.java +++ b/sampler/src/main/java/atlantafx/sampler/util/Containers.java @@ -7,6 +7,7 @@ import javafx.scene.control.ScrollPane; import javafx.scene.layout.AnchorPane; import javafx.scene.layout.ColumnConstraints; 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_PREF_SIZE; @@ -41,4 +42,14 @@ public final class Containers { constraints.setHgrow(hgrow); 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); + } } diff --git a/sampler/src/main/java/atlantafx/sampler/util/JColorUtils.java b/sampler/src/main/java/atlantafx/sampler/util/JColorUtils.java index 88aa470..be2607e 100644 --- a/sampler/src/main/java/atlantafx/sampler/util/JColorUtils.java +++ b/sampler/src/main/java/atlantafx/sampler/util/JColorUtils.java @@ -51,10 +51,9 @@ public class JColorUtils { /** * Convert the hex color values to a hex color * - * @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 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 */ public static String toColor(String red, String green, String blue) { @@ -64,10 +63,9 @@ public class JColorUtils { /** * Convert the hex color values to a hex color, shorthanded when possible * - * @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 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 */ public static String toColorShorthand(String red, String green, @@ -79,10 +77,9 @@ public class JColorUtils { * Convert the hex color values to a hex color including an opaque alpha * value of FF * - * @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 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 */ public static String toColorWithAlpha(String red, String green, @@ -99,10 +96,9 @@ public class JColorUtils { * Convert the hex color values to a hex color including an opaque alpha * value of FF or F, shorthanded when possible * - * @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 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 */ public static String toColorShorthandWithAlpha(String red, String green, @@ -113,11 +109,10 @@ public class JColorUtils { /** * Convert the hex color values to a hex color * - * @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 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 - * * @return hex color in format #AARRGGBB or #RRGGBB */ public static String toColorWithAlpha(String red, String green, String blue, @@ -140,11 +135,10 @@ public class JColorUtils { /** * Convert the hex color values to a hex color, shorthanded when possible * - * @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 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 - * * @return hex color in format #ARGB, #RGB, #AARRGGBB, or #RRGGBB */ public static String toColorShorthandWithAlpha(String red, String green, @@ -155,10 +149,9 @@ public class JColorUtils { /** * Convert the RGB values to a color integer * - * @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 blue blue integer color inclusively between 0 and 255 - * + * @param blue blue integer color inclusively between 0 and 255 * @return integer color */ public static int toColor(int red, int green, int blue) { @@ -169,10 +162,9 @@ public class JColorUtils { * Convert the RGB values to a color integer including an opaque alpha value * of 255 * - * @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 blue blue integer color inclusively between 0 and 255 - * + * @param blue blue integer color inclusively between 0 and 255 * @return integer color */ public static int toColorWithAlpha(int red, int green, int blue) { @@ -182,12 +174,11 @@ public class JColorUtils { /** * Convert the RGBA values to a color integer * - * @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 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 - * include alpha - * + * include alpha * @return integer color */ 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 * * @param color integer color inclusively between 0 and 255 - * * @return hex single color in format FF */ public static String toHex(int color) { @@ -223,7 +213,6 @@ public class JColorUtils { * Convert the arithmetic RGB float to a hex single color * * @param color float color inclusively between 0.0 and 1.0 - * * @return hex single color in format FF */ public static String toHex(float color) { @@ -234,7 +223,6 @@ public class JColorUtils { * Convert the hex single color to a RGB integer * * @param color hex single color in format FF or F - * * @return integer color inclusively between 0 and 255 */ public static int toRGB(String color) { @@ -249,7 +237,6 @@ public class JColorUtils { * Convert the arithmetic RGB float to a RGB integer * * @param color float color inclusively between 0.0 and 1.0 - * * @return integer color inclusively between 0 and 255 */ public static int toRGB(float color) { @@ -261,7 +248,6 @@ public class JColorUtils { * Convert the hex single color to an arithmetic RGB float * * @param color hex single color in format FF or F - * * @return float color inclusively between 0.0 and 1.0 */ public static float toArithmeticRGB(String color) { @@ -272,7 +258,6 @@ public class JColorUtils { * Convert the RGB integer to an arithmetic RGB float * * @param color integer color inclusively between 0 and 255 - * * @return float color inclusively between 0.0 and 1.0 */ public static float toArithmeticRGB(int color) { @@ -284,10 +269,9 @@ public class JColorUtils { * Convert red, green, and blue arithmetic values to HSL (hue, saturation, * lightness) values * - * @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 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 */ public static float[] toHSL(float red, float green, float blue) { @@ -339,25 +323,23 @@ public class JColorUtils { * Convert red, green, and blue integer values to HSL (hue, saturation, * lightness) values * - * @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 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 */ public static float[] toHSL(int red, int green, int blue) { return toHSL(toArithmeticRGB(red), toArithmeticRGB(green), - toArithmeticRGB(blue)); + toArithmeticRGB(blue)); } /** * Convert HSL (hue, saturation, and lightness) values to RGB arithmetic * values * - * @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 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 */ public static float[] toArithmeticRGB(float hue, float saturation, @@ -386,10 +368,9 @@ public class JColorUtils { /** * Convert HSL (hue, saturation, and lightness) values to RGB integer values * - * @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 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 */ public static int[] toRGB(float hue, float saturation, float lightness) { @@ -400,10 +381,9 @@ public class JColorUtils { /** * HSL convert helper method * - * @param t1 t1 - * @param t2 t2 + * @param t1 t1 + * @param t2 t2 * @param hue hue - * * @return arithmetic RGB value */ 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 * * @param hex hex color - * * @return hex red color in format RR */ public static String getRed(String hex) { @@ -441,7 +420,6 @@ public class JColorUtils { * Get the hex green color from the hex string * * @param hex hex color - * * @return hex green color in format GG */ public static String getGreen(String hex) { @@ -452,7 +430,6 @@ public class JColorUtils { * Get the hex blue color from the hex string * * @param hex hex color - * * @return hex blue color in format BB */ 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 * * @param hex hex color - * * @return hex alpha color in format AA or null */ public static String getAlpha(String hex) { @@ -473,9 +449,8 @@ public class JColorUtils { /** * Get the hex single color * - * @param hex hex color + * @param hex hex color * @param colorIndex red=0, green=1, blue=2, alpha=-1 - * * @return hex single color in format FF or null */ private static String getHexSingle(String hex, int colorIndex) { @@ -512,7 +487,6 @@ public class JColorUtils { * Get the red color from color integer * * @param color color integer - * * @return red color */ public static int getRed(int color) { @@ -523,7 +497,6 @@ public class JColorUtils { * Get the green color from color integer * * @param color color integer - * * @return green color */ public static int getGreen(int color) { @@ -534,7 +507,6 @@ public class JColorUtils { * Get the blue color from color integer * * @param color color integer - * * @return blue color */ public static int getBlue(int color) { @@ -545,7 +517,6 @@ public class JColorUtils { * Get the alpha color from color integer * * @param color color integer - * * @return alpha color */ public static int getAlpha(int color) { @@ -556,7 +527,6 @@ public class JColorUtils { * Shorthand the hex color if possible * * @param color hex color - * * @return shorthand hex color or original value */ public static String shorthandHex(String color) { @@ -589,7 +559,6 @@ public class JColorUtils { * Expand the hex if it is in shorthand * * @param color hex color - * * @return expanded hex color or original value */ public static String expandShorthandHex(String color) { @@ -615,7 +584,6 @@ public class JColorUtils { * Shorthand the hex single color if possible * * @param color hex single color - * * @return shorthand hex color or original value */ public static String shorthandHexSingle(String color) { @@ -632,7 +600,6 @@ public class JColorUtils { * Expand the hex single if it is in shorthand * * @param color hex single color - * * @return expanded hex color or original value */ public static String expandShorthandHexSingle(String color) { @@ -647,7 +614,6 @@ public class JColorUtils { * Check if the hex color value is valid * * @param color hex color - * * @return true if valid */ public static boolean isValidHex(String color) { @@ -671,7 +637,6 @@ public class JColorUtils { * Check if the hex single color value is valid * * @param color hex single color - * * @return true if valid */ 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 * * @param color decimal color - * * @return true if valid */ public static boolean isValidRGB(int color) { @@ -719,7 +683,6 @@ public class JColorUtils { * and 1.0 * * @param color decimal color - * * @return true if valid */ public static boolean isValidArithmeticRGB(float color) { @@ -745,7 +708,6 @@ public class JColorUtils { * 360.0 * * @param hue hue value - * * @return true if valid */ public static boolean isValidHue(float hue) { @@ -770,7 +732,6 @@ public class JColorUtils { * and 1.0 * * @param saturation saturation value - * * @return true if valid */ public static boolean isValidSaturation(float saturation) { @@ -796,7 +757,6 @@ public class JColorUtils { * and 1.0 * * @param lightness lightness value - * * @return true if valid */ public static boolean isValidLightness(float lightness) { @@ -899,4 +859,21 @@ public class JColorUtils { 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(); + } } diff --git a/sampler/src/main/java/atlantafx/sampler/util/NodeUtils.java b/sampler/src/main/java/atlantafx/sampler/util/NodeUtils.java new file mode 100644 index 0000000..96258a4 --- /dev/null +++ b/sampler/src/main/java/atlantafx/sampler/util/NodeUtils.java @@ -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 getChildByIndex(Parent parent, int index, Class contentType) { + List 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; + } +} diff --git a/sampler/src/main/resources/assets/styles/scss/layout/_index.scss b/sampler/src/main/resources/assets/styles/scss/layout/_index.scss index cc6dc7b..93186fc 100644 --- a/sampler/src/main/resources/assets/styles/scss/layout/_index.scss +++ b/sampler/src/main/resources/assets/styles/scss/layout/_index.scss @@ -2,4 +2,5 @@ @use "sidebar"; @use "page"; -@use "components"; \ No newline at end of file +@use "components"; +@use "overlay"; \ No newline at end of file diff --git a/sampler/src/main/resources/assets/styles/scss/layout/_overlay.scss b/sampler/src/main/resources/assets/styles/scss/layout/_overlay.scss new file mode 100644 index 0000000..f3ab191 --- /dev/null +++ b/sampler/src/main/resources/assets/styles/scss/layout/_overlay.scss @@ -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; + } +} \ No newline at end of file diff --git a/sampler/src/main/resources/assets/styles/scss/widgets/_color-palette.scss b/sampler/src/main/resources/assets/styles/scss/widgets/_color-palette.scss index d5fdc0d..60b93ea 100644 --- a/sampler/src/main/resources/assets/styles/scss/widgets/_color-palette.scss +++ b/sampler/src/main/resources/assets/styles/scss/widgets/_color-palette.scss @@ -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; - } - } - } - } } \ No newline at end of file diff --git a/sampler/src/main/resources/assets/styles/scss/widgets/_contrast-checker.scss b/sampler/src/main/resources/assets/styles/scss/widgets/_contrast-checker.scss new file mode 100644 index 0000000..a0d798c --- /dev/null +++ b/sampler/src/main/resources/assets/styles/scss/widgets/_contrast-checker.scss @@ -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; +} \ No newline at end of file diff --git a/sampler/src/main/resources/assets/styles/scss/widgets/_index.scss b/sampler/src/main/resources/assets/styles/scss/widgets/_index.scss index 04b1f3d..a94898f 100644 --- a/sampler/src/main/resources/assets/styles/scss/widgets/_index.scss +++ b/sampler/src/main/resources/assets/styles/scss/widgets/_index.scss @@ -1,4 +1,5 @@ // SPDX-License-Identifier: MIT @use "color-palette"; +@use "contrast-checker"; @use "quick-config-menu"; \ No newline at end of file