Improve color palette widget
* add description * use better color combinations for color blocks * refactor and pay tech debts
This commit is contained in:
parent
fc19ca5a36
commit
8a7204d93c
@ -1,6 +1,9 @@
|
|||||||
/* SPDX-License-Identifier: MIT */
|
/* SPDX-License-Identifier: MIT */
|
||||||
package atlantafx.sampler;
|
package atlantafx.sampler;
|
||||||
|
|
||||||
|
import atlantafx.sampler.event.BrowseEvent;
|
||||||
|
import atlantafx.sampler.event.DefaultEventBus;
|
||||||
|
import atlantafx.sampler.event.Listener;
|
||||||
import atlantafx.sampler.layout.ApplicationWindow;
|
import atlantafx.sampler.layout.ApplicationWindow;
|
||||||
import atlantafx.sampler.theme.ThemeManager;
|
import atlantafx.sampler.theme.ThemeManager;
|
||||||
import fr.brouillard.oss.cssfx.CSSFX;
|
import fr.brouillard.oss.cssfx.CSSFX;
|
||||||
@ -63,6 +66,9 @@ public class Launcher extends Application {
|
|||||||
stage.setResizable(true);
|
stage.setResizable(true);
|
||||||
stage.setOnCloseRequest(t -> Platform.exit());
|
stage.setOnCloseRequest(t -> Platform.exit());
|
||||||
|
|
||||||
|
// register event listeners
|
||||||
|
DefaultEventBus.getInstance().subscribe(BrowseEvent.class, this::onBrowseEvent);
|
||||||
|
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
stage.show();
|
stage.show();
|
||||||
stage.requestFocus();
|
stage.requestFocus();
|
||||||
@ -109,4 +115,9 @@ public class Launcher extends Application {
|
|||||||
);
|
);
|
||||||
CSSFX.start(scene);
|
CSSFX.start(scene);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Listener
|
||||||
|
private void onBrowseEvent(BrowseEvent event) {
|
||||||
|
getHostServices().showDocument(event.getUri().toString());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,24 @@
|
|||||||
|
/* SPDX-License-Identifier: MIT */
|
||||||
|
package atlantafx.sampler.event;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
|
||||||
|
public class BrowseEvent extends Event {
|
||||||
|
|
||||||
|
private final URI uri;
|
||||||
|
|
||||||
|
public BrowseEvent(URI uri) {
|
||||||
|
this.uri = uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
public URI getUri() {
|
||||||
|
return uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "BrowseEvent{" +
|
||||||
|
"uri=" + uri +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,92 @@
|
|||||||
|
/* SPDX-License-Identifier: MIT */
|
||||||
|
package atlantafx.sampler.event;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.CopyOnWriteArraySet;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple event bus implementation.
|
||||||
|
* <p>
|
||||||
|
* Subscribe and publish events. Events are published in channels distinguished by event type.
|
||||||
|
* Channels can be grouped using an event type hierarchy.
|
||||||
|
* <p>
|
||||||
|
* You can use the default event bus instance {@link #getInstance}, which is a singleton
|
||||||
|
* or you can create one or multiple instances of {@link DefaultEventBus}.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||||
|
public final class DefaultEventBus implements EventBus {
|
||||||
|
|
||||||
|
public DefaultEventBus() {}
|
||||||
|
|
||||||
|
private final Map<Class<?>, Set<Consumer>> subscribers = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <E extends Event> void subscribe(Class<? extends E> eventType, Consumer<E> subscriber) {
|
||||||
|
Objects.requireNonNull(eventType);
|
||||||
|
Objects.requireNonNull(subscriber);
|
||||||
|
|
||||||
|
Set<Consumer> eventSubscribers = getOrCreateSubscribers(eventType);
|
||||||
|
eventSubscribers.add(subscriber);
|
||||||
|
}
|
||||||
|
|
||||||
|
private <E> Set<Consumer> getOrCreateSubscribers(Class<E> eventType) {
|
||||||
|
Set<Consumer> eventSubscribers = subscribers.get(eventType);
|
||||||
|
if (eventSubscribers == null) {
|
||||||
|
eventSubscribers = new CopyOnWriteArraySet<>();
|
||||||
|
subscribers.put(eventType, eventSubscribers);
|
||||||
|
}
|
||||||
|
return eventSubscribers;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <E extends Event> void unsubscribe(Consumer<E> subscriber) {
|
||||||
|
Objects.requireNonNull(subscriber);
|
||||||
|
|
||||||
|
subscribers.values().forEach(eventSubscribers -> eventSubscribers.remove(subscriber));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <E extends Event> void unsubscribe(Class<? extends E> eventType, Consumer<E> subscriber) {
|
||||||
|
Objects.requireNonNull(eventType);
|
||||||
|
Objects.requireNonNull(subscriber);
|
||||||
|
|
||||||
|
subscribers.keySet().stream()
|
||||||
|
.filter(eventType::isAssignableFrom)
|
||||||
|
.map(subscribers::get)
|
||||||
|
.forEach(eventSubscribers -> eventSubscribers.remove(subscriber));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <E extends Event> void publish(E event) {
|
||||||
|
Objects.requireNonNull(event);
|
||||||
|
|
||||||
|
Class<?> eventType = event.getClass();
|
||||||
|
subscribers.keySet().stream()
|
||||||
|
.filter(type -> type.isAssignableFrom(eventType))
|
||||||
|
.flatMap(type -> subscribers.get(type).stream())
|
||||||
|
.forEach(subscriber -> publish(event, subscriber));
|
||||||
|
}
|
||||||
|
|
||||||
|
private <E extends Event> void publish(E event, Consumer<E> subscriber) {
|
||||||
|
try {
|
||||||
|
subscriber.accept(event);
|
||||||
|
} catch (Exception e) {
|
||||||
|
Thread.currentThread().getUncaughtExceptionHandler().uncaughtException(Thread.currentThread(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
private static class InstanceHolder {
|
||||||
|
|
||||||
|
private static final DefaultEventBus INSTANCE = new DefaultEventBus();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static DefaultEventBus getInstance() {
|
||||||
|
return InstanceHolder.INSTANCE;
|
||||||
|
}
|
||||||
|
}
|
35
sampler/src/main/java/atlantafx/sampler/event/Event.java
Normal file
35
sampler/src/main/java/atlantafx/sampler/event/Event.java
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
/* SPDX-License-Identifier: MIT */
|
||||||
|
package atlantafx.sampler.event;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public abstract class Event {
|
||||||
|
|
||||||
|
protected final UUID id = UUID.randomUUID();
|
||||||
|
|
||||||
|
protected Event() { }
|
||||||
|
|
||||||
|
public UUID getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) { return true; }
|
||||||
|
if (o == null || getClass() != o.getClass()) { return false; }
|
||||||
|
Event event = (Event) o;
|
||||||
|
return id.equals(event.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return id.hashCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "Event{" +
|
||||||
|
"id=" + id +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
}
|
43
sampler/src/main/java/atlantafx/sampler/event/EventBus.java
Normal file
43
sampler/src/main/java/atlantafx/sampler/event/EventBus.java
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
/* SPDX-License-Identifier: MIT */
|
||||||
|
package atlantafx.sampler.event;
|
||||||
|
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
public interface EventBus {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribe to an event type
|
||||||
|
*
|
||||||
|
* @param eventType the event type, can be a super class of all events to subscribe.
|
||||||
|
* @param subscriber the subscriber which will consume the events.
|
||||||
|
* @param <T> the event type class.
|
||||||
|
*/
|
||||||
|
<T extends Event> void subscribe(Class<? extends T> eventType, Consumer<T> subscriber);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unsubscribe from all event types.
|
||||||
|
*
|
||||||
|
* @param subscriber the subscriber to unsubscribe.
|
||||||
|
*/
|
||||||
|
<T extends Event> void unsubscribe(Consumer<T> subscriber);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unsubscribe from an event type.
|
||||||
|
*
|
||||||
|
* @param eventType the event type, can be a super class of all events to unsubscribe.
|
||||||
|
* @param subscriber the subscriber to unsubscribe.
|
||||||
|
* @param <T> the event type class.
|
||||||
|
*/
|
||||||
|
<T extends Event> void unsubscribe(Class<? extends T> eventType, Consumer<T> subscriber);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Publish an event to all subscribers.
|
||||||
|
* <p>
|
||||||
|
* The event type is the class of <code>event</code>. The event is published to all consumers which subscribed to
|
||||||
|
* this event type or any super class.
|
||||||
|
*
|
||||||
|
* @param event the event.
|
||||||
|
*/
|
||||||
|
<T extends Event> void publish(T event);
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
/* SPDX-License-Identifier: MIT */
|
||||||
|
package atlantafx.sampler.event;
|
||||||
|
|
||||||
|
import java.lang.annotation.ElementType;
|
||||||
|
import java.lang.annotation.Target;
|
||||||
|
|
||||||
|
@Target(ElementType.METHOD)
|
||||||
|
public @interface Listener {}
|
@ -12,13 +12,18 @@ import javafx.scene.layout.GridPane;
|
|||||||
import javafx.scene.layout.HBox;
|
import javafx.scene.layout.HBox;
|
||||||
import javafx.scene.layout.VBox;
|
import javafx.scene.layout.VBox;
|
||||||
import javafx.scene.paint.Color;
|
import javafx.scene.paint.Color;
|
||||||
|
import javafx.scene.text.Text;
|
||||||
|
import javafx.scene.text.TextFlow;
|
||||||
import javafx.util.Duration;
|
import javafx.util.Duration;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
import static atlantafx.sampler.util.Controls.hyperlink;
|
||||||
|
|
||||||
class ColorPalette extends VBox {
|
class ColorPalette extends VBox {
|
||||||
|
|
||||||
private final List<ColorPaletteBlock> blocks = new ArrayList<>();
|
private final List<ColorPaletteBlock> blocks = new ArrayList<>();
|
||||||
@ -41,13 +46,30 @@ class ColorPalette extends VBox {
|
|||||||
headerBox.setAlignment(Pos.CENTER_LEFT);
|
headerBox.setAlignment(Pos.CENTER_LEFT);
|
||||||
headerBox.getStyleClass().add("header");
|
headerBox.getStyleClass().add("header");
|
||||||
|
|
||||||
|
var noteText = new VBox(6);
|
||||||
|
noteText.getChildren().setAll(
|
||||||
|
new TextFlow(
|
||||||
|
new Text("Color contrast between text and its background must meet "),
|
||||||
|
hyperlink("required WCAG standards",
|
||||||
|
URI.create("https://www.w3.org/WAI/WCAG21/Understanding/contrast-minimum.html")
|
||||||
|
),
|
||||||
|
new Text(":")
|
||||||
|
),
|
||||||
|
new Text(" • 4.5:1 for normal text"),
|
||||||
|
new Text(" • 3:1 for large text (>24px)"),
|
||||||
|
new Text(" • 3:1 for UI elements and graphics"),
|
||||||
|
new Text(" • no contrast requirement for decorative and disabled elements"),
|
||||||
|
new Text(),
|
||||||
|
new Text("Click on any color block to observe and modify color combination via built-in contrast checker.")
|
||||||
|
);
|
||||||
|
|
||||||
var colorGrid = colorGrid();
|
var 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
|
||||||
));
|
));
|
||||||
|
|
||||||
getChildren().setAll(headerBox, colorGrid);
|
getChildren().setAll(headerBox, noteText, colorGrid);
|
||||||
setId("color-palette");
|
setId("color-palette");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -56,33 +78,34 @@ class ColorPalette extends VBox {
|
|||||||
grid.getStyleClass().add("grid");
|
grid.getStyleClass().add("grid");
|
||||||
|
|
||||||
grid.add(colorBlock("-color-fg-default", "-color-bg-default", "-color-border-default"), 0, 0);
|
grid.add(colorBlock("-color-fg-default", "-color-bg-default", "-color-border-default"), 0, 0);
|
||||||
grid.add(colorBlock("-color-fg-muted", "-color-bg-default", "-color-border-muted"), 1, 0);
|
grid.add(colorBlock("-color-fg-default", "-color-bg-overlay", "-color-border-default"), 1, 0);
|
||||||
grid.add(colorBlock("-color-fg-subtle", "-color-bg-default", "-color-border-subtle"), 2, 0);
|
grid.add(colorBlock("-color-fg-muted", "-color-bg-default", "-color-border-muted"), 2, 0);
|
||||||
|
grid.add(colorBlock("-color-fg-subtle", "-color-bg-default", "-color-border-subtle"), 3, 0);
|
||||||
|
|
||||||
grid.add(colorBlock("-color-fg-emphasis", "-color-accent-emphasis", "-color-accent-emphasis"), 0, 1);
|
grid.add(colorBlock("-color-fg-emphasis", "-color-accent-emphasis", "-color-accent-emphasis"), 0, 1);
|
||||||
grid.add(colorBlock("-color-accent-fg", "-color-bg-default", "-color-accent-emphasis"), 1, 1);
|
grid.add(colorBlock("-color-accent-fg", "-color-bg-default", "-color-accent-emphasis"), 1, 1);
|
||||||
grid.add(colorBlock("-color-accent-fg", "-color-accent-muted", "-color-accent-muted"), 2, 1);
|
grid.add(colorBlock("-color-fg-default", "-color-accent-muted", "-color-accent-emphasis"), 2, 1);
|
||||||
grid.add(colorBlock("-color-accent-fg", "-color-accent-subtle", "-color-accent-subtle"), 3, 1);
|
grid.add(colorBlock("-color-accent-fg", "-color-accent-subtle", "-color-accent-emphasis"), 3, 1);
|
||||||
|
|
||||||
grid.add(colorBlock("-color-fg-emphasis", "-color-neutral-emphasis-plus", "-color-neutral-emphasis-plus"), 0, 2);
|
grid.add(colorBlock("-color-fg-emphasis", "-color-neutral-emphasis-plus", "-color-neutral-emphasis-plus"), 0, 2);
|
||||||
grid.add(colorBlock("-color-fg-emphasis", "-color-neutral-emphasis", "-color-neutral-emphasis"), 1, 2);
|
grid.add(colorBlock("-color-fg-emphasis", "-color-neutral-emphasis", "-color-neutral-emphasis"), 1, 2);
|
||||||
grid.add(colorBlock("-color-fg-muted", "-color-neutral-muted", "-color-neutral-muted"), 2, 2);
|
grid.add(colorBlock("-color-fg-default", "-color-neutral-muted", "-color-neutral-emphasis"), 2, 2);
|
||||||
grid.add(colorBlock("-color-fg-subtle", "-color-neutral-subtle", "-color-neutral-subtle"), 3, 2);
|
grid.add(colorBlock("-color-fg-default", "-color-neutral-subtle", "-color-neutral-emphasis"), 3, 2);
|
||||||
|
|
||||||
grid.add(colorBlock("-color-fg-emphasis", "-color-success-emphasis", "-color-success-emphasis"), 0, 3);
|
grid.add(colorBlock("-color-fg-emphasis", "-color-success-emphasis", "-color-success-emphasis"), 0, 3);
|
||||||
grid.add(colorBlock("-color-success-fg", "-color-bg-default", "-color-success-emphasis"), 1, 3);
|
grid.add(colorBlock("-color-success-fg", "-color-bg-default", "-color-success-emphasis"), 1, 3);
|
||||||
grid.add(colorBlock("-color-success-fg", "-color-success-muted", "-color-success-muted"), 2, 3);
|
grid.add(colorBlock("-color-fg-default", "-color-success-muted", "-color-success-emphasis"), 2, 3);
|
||||||
grid.add(colorBlock("-color-success-fg", "-color-success-subtle", "-color-success-subtle"), 3, 3);
|
grid.add(colorBlock("-color-success-fg", "-color-success-subtle", "-color-success-emphasis"), 3, 3);
|
||||||
|
|
||||||
grid.add(colorBlock("-color-fg-emphasis", "-color-warning-emphasis", "-color-warning-emphasis"), 0, 4);
|
grid.add(colorBlock("-color-fg-emphasis", "-color-warning-emphasis", "-color-warning-emphasis"), 0, 4);
|
||||||
grid.add(colorBlock("-color-warning-fg", "-color-bg-default", "-color-warning-emphasis"), 1, 4);
|
grid.add(colorBlock("-color-warning-fg", "-color-bg-default", "-color-warning-emphasis"), 1, 4);
|
||||||
grid.add(colorBlock("-color-warning-fg", "-color-warning-muted", "-color-warning-muted"), 2, 4);
|
grid.add(colorBlock("-color-fg-default", "-color-warning-muted", "-color-warning-emphasis"), 2, 4);
|
||||||
grid.add(colorBlock("-color-warning-fg", "-color-warning-subtle", "-color-warning-subtle"), 3, 4);
|
grid.add(colorBlock("-color-warning-fg", "-color-warning-subtle", "-color-warning-emphasis"), 3, 4);
|
||||||
|
|
||||||
grid.add(colorBlock("-color-fg-emphasis", "-color-danger-emphasis", "-color-danger-emphasis"), 0, 5);
|
grid.add(colorBlock("-color-fg-emphasis", "-color-danger-emphasis", "-color-danger-emphasis"), 0, 5);
|
||||||
grid.add(colorBlock("-color-danger-fg", "-color-bg-default", "-color-danger-emphasis"), 1, 5);
|
grid.add(colorBlock("-color-danger-fg", "-color-bg-default", "-color-danger-emphasis"), 1, 5);
|
||||||
grid.add(colorBlock("-color-danger-fg", "-color-danger-muted", "-color-danger-muted"), 2, 5);
|
grid.add(colorBlock("-color-fg-default", "-color-danger-muted", "-color-danger-emphasis"), 2, 5);
|
||||||
grid.add(colorBlock("-color-danger-fg", "-color-danger-subtle", "-color-danger-subtle"), 3, 5);
|
grid.add(colorBlock("-color-danger-fg", "-color-danger-subtle", "-color-danger-emphasis"), 3, 5);
|
||||||
|
|
||||||
return grid;
|
return grid;
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,8 @@ package atlantafx.sampler.page.general;
|
|||||||
|
|
||||||
import atlantafx.base.theme.Styles;
|
import atlantafx.base.theme.Styles;
|
||||||
import atlantafx.sampler.util.Containers;
|
import atlantafx.sampler.util.Containers;
|
||||||
|
import atlantafx.sampler.util.ContrastLevel;
|
||||||
|
import atlantafx.sampler.util.NodeUtils;
|
||||||
import javafx.beans.property.ReadOnlyObjectProperty;
|
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||||
import javafx.geometry.Insets;
|
import javafx.geometry.Insets;
|
||||||
import javafx.scene.control.Label;
|
import javafx.scene.control.Label;
|
||||||
@ -10,15 +12,17 @@ import javafx.scene.layout.AnchorPane;
|
|||||||
import javafx.scene.layout.VBox;
|
import javafx.scene.layout.VBox;
|
||||||
import javafx.scene.paint.Color;
|
import javafx.scene.paint.Color;
|
||||||
import javafx.scene.text.Text;
|
import javafx.scene.text.Text;
|
||||||
import org.kordamp.ikonli.feather.Feather;
|
|
||||||
import org.kordamp.ikonli.javafx.FontIcon;
|
import org.kordamp.ikonli.javafx.FontIcon;
|
||||||
import org.kordamp.ikonli.material2.Material2AL;
|
import org.kordamp.ikonli.material2.Material2AL;
|
||||||
|
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
import static atlantafx.sampler.page.general.ContrastChecker.*;
|
import static atlantafx.base.theme.Styles.TITLE_3;
|
||||||
|
import static atlantafx.sampler.page.general.ContrastChecker.LUMINANCE_THRESHOLD;
|
||||||
|
import static atlantafx.sampler.page.general.ContrastChecker.PASSED;
|
||||||
|
import static atlantafx.sampler.util.ContrastLevel.getColorLuminance;
|
||||||
|
import static atlantafx.sampler.util.ContrastLevel.getContrastRatioOpacityAware;
|
||||||
import static atlantafx.sampler.util.JColorUtils.flattenColor;
|
import static atlantafx.sampler.util.JColorUtils.flattenColor;
|
||||||
import static atlantafx.sampler.util.JColorUtils.getColorLuminance;
|
|
||||||
|
|
||||||
class ColorPaletteBlock extends VBox {
|
class ColorPaletteBlock extends VBox {
|
||||||
|
|
||||||
@ -27,11 +31,11 @@ class ColorPaletteBlock extends VBox {
|
|||||||
private final String borderColorName;
|
private final String borderColorName;
|
||||||
private final ReadOnlyObjectProperty<Color> bgBaseColor;
|
private final ReadOnlyObjectProperty<Color> bgBaseColor;
|
||||||
|
|
||||||
private final AnchorPane colorBox;
|
private final AnchorPane colorRectangle;
|
||||||
private final Text fgText;
|
private final Text contrastRatioText;
|
||||||
private final FontIcon wsagIcon = new FontIcon();
|
private final FontIcon contrastLevelIcon = new FontIcon();
|
||||||
private final Label wsagLabel = new Label();
|
private final Label contrastLevelLabel = new Label();
|
||||||
private final FontIcon expandIcon = new FontIcon(Feather.MAXIMIZE_2);
|
private final FontIcon editIcon = new FontIcon(Material2AL.COLORIZE);
|
||||||
|
|
||||||
private Consumer<ColorPaletteBlock> actionHandler;
|
private Consumer<ColorPaletteBlock> actionHandler;
|
||||||
|
|
||||||
@ -44,69 +48,54 @@ class ColorPaletteBlock extends VBox {
|
|||||||
this.borderColorName = validateColorName(borderColorName);
|
this.borderColorName = validateColorName(borderColorName);
|
||||||
this.bgBaseColor = bgBaseColor;
|
this.bgBaseColor = bgBaseColor;
|
||||||
|
|
||||||
fgText = new Text();
|
contrastRatioText = new Text();
|
||||||
fgText.setStyle("-fx-fill:" + fgColorName + ";");
|
contrastRatioText.setStyle("-fx-fill:" + fgColorName + ";");
|
||||||
fgText.getStyleClass().addAll("text", Styles.TITLE_3);
|
contrastRatioText.getStyleClass().addAll("contrast-ratio-text", TITLE_3);
|
||||||
Containers.setAnchors(fgText, new Insets(5, -1, -1, 5));
|
Containers.setAnchors(contrastRatioText, new Insets(5, -1, -1, 5));
|
||||||
|
|
||||||
wsagLabel.setGraphic(wsagIcon);
|
contrastLevelLabel.setGraphic(contrastLevelIcon);
|
||||||
wsagLabel.getStyleClass().add("wsag-label");
|
contrastLevelLabel.getStyleClass().add("contrast-level-label");
|
||||||
wsagLabel.setVisible(false);
|
contrastLevelLabel.setVisible(false);
|
||||||
Containers.setAnchors(wsagLabel, new Insets(-1, 3, 3, -1));
|
Containers.setAnchors(contrastLevelLabel, new Insets(-1, 3, 3, -1));
|
||||||
|
|
||||||
expandIcon.setIconSize(24);
|
editIcon.setIconSize(24);
|
||||||
expandIcon.getStyleClass().add("expand-icon");
|
editIcon.getStyleClass().add("edit-icon");
|
||||||
expandIcon.setVisible(false);
|
NodeUtils.toggleVisibility(editIcon, false);
|
||||||
expandIcon.setManaged(false);
|
Containers.setAnchors(editIcon, new Insets(3, 3, -1, -1));
|
||||||
Containers.setAnchors(expandIcon, new Insets(3, 3, -1, -1));
|
|
||||||
|
|
||||||
colorBox = new AnchorPane();
|
colorRectangle = new AnchorPane();
|
||||||
colorBox.setStyle("-fx-background-color:" + bgColorName + ";" + "-fx-border-color:" + borderColorName + ";");
|
colorRectangle.setStyle(
|
||||||
colorBox.getStyleClass().add("box");
|
String.format("-fx-background-color:%s;-fx-border-color:%s;", bgColorName, borderColorName)
|
||||||
colorBox.getChildren().setAll(fgText, wsagLabel, expandIcon);
|
);
|
||||||
colorBox.setOnMouseEntered(e -> {
|
colorRectangle.getStyleClass().add("rectangle");
|
||||||
|
colorRectangle.getChildren().setAll(contrastRatioText, contrastLevelLabel, editIcon);
|
||||||
|
colorRectangle.setOnMouseEntered(e -> {
|
||||||
var bgFill = getBgColor();
|
var bgFill = getBgColor();
|
||||||
|
|
||||||
// this happens when css isn't updated yet
|
// this happens when css isn't updated yet
|
||||||
if (bgFill == null) { return; }
|
if (bgFill == null) { return; }
|
||||||
|
|
||||||
toggleHover(true);
|
toggleHover(true);
|
||||||
expandIcon.setFill(getColorLuminance(flattenColor(bgBaseColor.get(), bgFill)) < LUMINANCE_THRESHOLD ?
|
editIcon.setFill(getColorLuminance(flattenColor(bgBaseColor.get(), bgFill)) < LUMINANCE_THRESHOLD ?
|
||||||
Color.WHITE : Color.BLACK
|
Color.WHITE : Color.BLACK
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
colorBox.setOnMouseExited(e -> toggleHover(false));
|
colorRectangle.setOnMouseExited(e -> toggleHover(false));
|
||||||
colorBox.setOnMouseClicked(e -> {
|
colorRectangle.setOnMouseClicked(e -> {
|
||||||
if (actionHandler != null) { actionHandler.accept(this); }
|
if (actionHandler != null) { actionHandler.accept(this); }
|
||||||
});
|
});
|
||||||
|
|
||||||
getChildren().addAll(
|
getChildren().addAll(
|
||||||
colorBox,
|
colorRectangle,
|
||||||
description(fgColorName),
|
colorNameText(fgColorName),
|
||||||
description(bgColorName),
|
colorNameText(bgColorName),
|
||||||
description(borderColorName)
|
colorNameText(borderColorName)
|
||||||
);
|
);
|
||||||
getStyleClass().add("color-block");
|
getStyleClass().add("block");
|
||||||
}
|
}
|
||||||
|
|
||||||
static String validateColorName(String colorName) {
|
public void setOnAction(Consumer<ColorPaletteBlock> actionHandler) {
|
||||||
if (colorName == null || !colorName.startsWith("-color")) {
|
this.actionHandler = actionHandler;
|
||||||
throw new IllegalArgumentException("Invalid color name: '" + colorName + "'.");
|
|
||||||
}
|
|
||||||
return colorName;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void toggleHover(boolean state) {
|
|
||||||
expandIcon.setVisible(state);
|
|
||||||
expandIcon.setManaged(state);
|
|
||||||
fgText.setOpacity(state ? 0.5 : 1);
|
|
||||||
wsagLabel.setOpacity(state ? 0.5 : 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Text description(String text) {
|
|
||||||
var t = new Text(text);
|
|
||||||
t.getStyleClass().addAll("description", Styles.TEXT_SMALL);
|
|
||||||
return t;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void update() {
|
public void update() {
|
||||||
@ -114,28 +103,28 @@ class ColorPaletteBlock extends VBox {
|
|||||||
var bgFill = getBgColor();
|
var bgFill = getBgColor();
|
||||||
|
|
||||||
if (fgFill == null || bgFill == null) {
|
if (fgFill == null || bgFill == null) {
|
||||||
fgText.setText("");
|
contrastRatioText.setText("");
|
||||||
wsagLabel.setText("");
|
contrastLevelLabel.setText("");
|
||||||
wsagLabel.setVisible(false);
|
contrastLevelLabel.setVisible(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
double contrastRatio = 1 / getContrastRatioOpacityAware(bgFill, fgFill, bgBaseColor.get());
|
double contrastRatio = getContrastRatioOpacityAware(bgFill, fgFill, bgBaseColor.get());
|
||||||
colorBox.pseudoClassStateChanged(PASSED, contrastRatio >= 4.5);
|
colorRectangle.pseudoClassStateChanged(PASSED, ContrastLevel.AA_NORMAL.satisfies(contrastRatio));
|
||||||
|
|
||||||
wsagIcon.setIconCode(contrastRatio >= 4.5 ? Material2AL.CHECK : Material2AL.CLOSE);
|
contrastRatioText.setText(String.format("%.2f", contrastRatio));
|
||||||
wsagLabel.setVisible(true);
|
contrastLevelIcon.setIconCode(ContrastLevel.AA_NORMAL.satisfies(contrastRatio) ? Material2AL.CHECK : Material2AL.CLOSE);
|
||||||
wsagLabel.setText(contrastRatio >= 7 ? "AAA" : "AA");
|
contrastLevelLabel.setVisible(true);
|
||||||
fgText.setText(String.format("%.2f", contrastRatio));
|
contrastLevelLabel.setText(ContrastLevel.AAA_NORMAL.satisfies(contrastRatio) ? "AAA" : "AA");
|
||||||
}
|
}
|
||||||
|
|
||||||
public Color getFgColor() {
|
public Color getFgColor() {
|
||||||
return (Color) fgText.getFill();
|
return (Color) contrastRatioText.getFill();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Color getBgColor() {
|
public Color getBgColor() {
|
||||||
return colorBox.getBackground() != null && !colorBox.getBackground().isEmpty() ?
|
return colorRectangle.getBackground() != null && !colorRectangle.getBackground().isEmpty() ?
|
||||||
(Color) colorBox.getBackground().getFills().get(0).getFill() : null;
|
(Color) colorRectangle.getBackground().getFills().get(0).getFill() : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getFgColorName() {
|
public String getFgColorName() {
|
||||||
@ -150,7 +139,22 @@ class ColorPaletteBlock extends VBox {
|
|||||||
return borderColorName;
|
return borderColorName;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setOnAction(Consumer<ColorPaletteBlock> actionHandler) {
|
private void toggleHover(boolean state) {
|
||||||
this.actionHandler = actionHandler;
|
NodeUtils.toggleVisibility(editIcon, state);
|
||||||
|
contrastRatioText.setOpacity(state ? 0.5 : 1);
|
||||||
|
contrastLevelLabel.setOpacity(state ? 0.5 : 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Text colorNameText(String text) {
|
||||||
|
var t = new Text(text);
|
||||||
|
t.getStyleClass().addAll("color-name", Styles.TEXT_SMALL);
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
|
||||||
|
static String validateColorName(String colorName) {
|
||||||
|
if (colorName == null || !colorName.startsWith("-color")) {
|
||||||
|
throw new IllegalArgumentException("Invalid color name: '" + colorName + "'.");
|
||||||
|
}
|
||||||
|
return colorName;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,12 +11,14 @@ import javafx.scene.layout.FlowPane;
|
|||||||
import javafx.scene.layout.HBox;
|
import javafx.scene.layout.HBox;
|
||||||
import javafx.scene.layout.VBox;
|
import javafx.scene.layout.VBox;
|
||||||
import javafx.scene.paint.Color;
|
import javafx.scene.paint.Color;
|
||||||
|
import javafx.scene.text.Text;
|
||||||
|
import javafx.scene.text.TextFlow;
|
||||||
import javafx.util.Duration;
|
import javafx.util.Duration;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public class ColorScale extends VBox {
|
class ColorScale extends VBox {
|
||||||
|
|
||||||
private final ReadOnlyObjectWrapper<Color> bgBaseColor = new ReadOnlyObjectWrapper<>(Color.WHITE);
|
private final ReadOnlyObjectWrapper<Color> bgBaseColor = new ReadOnlyObjectWrapper<>(Color.WHITE);
|
||||||
private final List<ColorScaleBlock> blocks = Arrays.asList(
|
private final List<ColorScaleBlock> blocks = Arrays.asList(
|
||||||
@ -42,6 +44,10 @@ public class ColorScale extends VBox {
|
|||||||
headerBox.setAlignment(Pos.CENTER_LEFT);
|
headerBox.setAlignment(Pos.CENTER_LEFT);
|
||||||
headerBox.getStyleClass().add("header");
|
headerBox.getStyleClass().add("header");
|
||||||
|
|
||||||
|
var noteText = new TextFlow(
|
||||||
|
new Text("Avoid referencing scale variables directly when building UI that needs to adapt to different color themes. Instead, use the functional variables listed above.")
|
||||||
|
);
|
||||||
|
|
||||||
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
|
||||||
));
|
));
|
||||||
@ -49,6 +55,7 @@ public class ColorScale extends VBox {
|
|||||||
setId("color-scale");
|
setId("color-scale");
|
||||||
getChildren().setAll(
|
getChildren().setAll(
|
||||||
headerBox,
|
headerBox,
|
||||||
|
noteText,
|
||||||
colorTable()
|
colorTable()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -8,10 +8,10 @@ import javafx.scene.control.Label;
|
|||||||
import javafx.scene.layout.VBox;
|
import javafx.scene.layout.VBox;
|
||||||
import javafx.scene.paint.Color;
|
import javafx.scene.paint.Color;
|
||||||
|
|
||||||
|
import static atlantafx.sampler.util.ContrastLevel.getColorLuminance;
|
||||||
import static atlantafx.sampler.util.JColorUtils.flattenColor;
|
import static atlantafx.sampler.util.JColorUtils.flattenColor;
|
||||||
import static atlantafx.sampler.util.JColorUtils.getColorLuminance;
|
|
||||||
|
|
||||||
public class ColorScaleBlock extends VBox {
|
class ColorScaleBlock extends VBox {
|
||||||
|
|
||||||
private static final double BLOCK_WIDTH = 250;
|
private static final double BLOCK_WIDTH = 250;
|
||||||
private static final double BLOCK_HEIGHT = 50;
|
private static final double BLOCK_HEIGHT = 50;
|
||||||
|
@ -5,6 +5,7 @@ import atlantafx.base.controls.CustomTextField;
|
|||||||
import atlantafx.base.controls.Spacer;
|
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.ContrastLevel;
|
||||||
import atlantafx.sampler.util.JColor;
|
import atlantafx.sampler.util.JColor;
|
||||||
import atlantafx.sampler.util.JColorUtils;
|
import atlantafx.sampler.util.JColorUtils;
|
||||||
import javafx.beans.binding.Bindings;
|
import javafx.beans.binding.Bindings;
|
||||||
@ -34,16 +35,19 @@ import org.kordamp.ikonli.material2.Material2AL;
|
|||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
import static atlantafx.sampler.page.general.ColorPaletteBlock.validateColorName;
|
import static atlantafx.sampler.page.general.ColorPaletteBlock.validateColorName;
|
||||||
|
import static atlantafx.sampler.util.ContrastLevel.getColorLuminance;
|
||||||
|
import static atlantafx.sampler.util.ContrastLevel.getContrastRatioOpacityAware;
|
||||||
import static atlantafx.sampler.util.JColorUtils.flattenColor;
|
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 ContrastChecker extends GridPane {
|
class ContrastChecker extends GridPane {
|
||||||
|
|
||||||
public static final double CONTRAST_RATIO_THRESHOLD = 1.5;
|
public static final double CONTRAST_RATIO_THRESHOLD = 1.5;
|
||||||
public static final double LUMINANCE_THRESHOLD = 0.55;
|
public static final double LUMINANCE_THRESHOLD = 0.55;
|
||||||
public static final PseudoClass PASSED = PseudoClass.getPseudoClass("passed");
|
public static final PseudoClass PASSED = PseudoClass.getPseudoClass("passed");
|
||||||
|
|
||||||
|
private static final String STATE_PASS = "PASS";
|
||||||
|
private static final String STATE_FAIL = "FAIL";
|
||||||
private static final int SLIDER_WIDTH = 300;
|
private static final int SLIDER_WIDTH = 300;
|
||||||
|
|
||||||
private String bgColorName;
|
private String bgColorName;
|
||||||
@ -70,7 +74,7 @@ public class ContrastChecker extends GridPane {
|
|||||||
|
|
||||||
this.bgBaseColor = bgBaseColor;
|
this.bgBaseColor = bgBaseColor;
|
||||||
this.contrastRatio = Bindings.createDoubleBinding(
|
this.contrastRatio = Bindings.createDoubleBinding(
|
||||||
() -> 1 / getContrastRatioOpacityAware(bgColor.getColor(), fgColor.getColor(), bgBaseColor.get()),
|
() -> getContrastRatioOpacityAware(bgColor.getColor(), fgColor.getColor(), bgBaseColor.get()),
|
||||||
bgColor.colorProperty(),
|
bgColor.colorProperty(),
|
||||||
fgColor.colorProperty(),
|
fgColor.colorProperty(),
|
||||||
bgBaseColor
|
bgBaseColor
|
||||||
@ -112,60 +116,42 @@ public class ContrastChecker extends GridPane {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void createView() {
|
private void createView() {
|
||||||
var textLabel = new Label("Aa");
|
var largeFontLabel = new Label("Aa");
|
||||||
textLabel.getStyleClass().add("text");
|
largeFontLabel.getStyleClass().add("large-font");
|
||||||
|
|
||||||
var ratioLabel = new Label("0.0");
|
var contrastRatioLabel = new Label("0.0");
|
||||||
ratioLabel.getStyleClass().add("ratio");
|
contrastRatioLabel.getStyleClass().add("ratio");
|
||||||
ratioLabel.textProperty().bind(Bindings.createStringBinding(
|
contrastRatioLabel.textProperty().bind(Bindings.createStringBinding(
|
||||||
() -> String.format("%.2f", contrastRatio.get()), contrastRatio
|
() -> String.format("%.2f", contrastRatio.get()), contrastRatio
|
||||||
));
|
));
|
||||||
|
|
||||||
var fontBox = new HBox(20, textLabel, ratioLabel);
|
var contrastRatioBox = new HBox(20, largeFontLabel, contrastRatioLabel);
|
||||||
fontBox.getStyleClass().add("font-box");
|
contrastRatioBox.getStyleClass().add("contrast-ratio");
|
||||||
fontBox.setAlignment(Pos.BASELINE_LEFT);
|
contrastRatioBox.setAlignment(Pos.BASELINE_LEFT);
|
||||||
|
|
||||||
// !
|
// !
|
||||||
|
|
||||||
var aaNormalLabel = wsagLabel();
|
var aaNormalLabel = contrastLevelLabel();
|
||||||
var aaNormalBox = wsagBox(aaNormalLabel, "AA Normal");
|
var aaNormalBox = contrastLevelBox(aaNormalLabel, "AA Normal");
|
||||||
|
|
||||||
var aaLargeLabel = wsagLabel();
|
var aaLargeLabel = contrastLevelLabel();
|
||||||
var aaLargeBox = wsagBox(aaLargeLabel, "AA Large");
|
var aaLargeBox = contrastLevelBox(aaLargeLabel, "AA Large");
|
||||||
|
|
||||||
var aaaNormalLabel = wsagLabel();
|
var aaaNormalLabel = contrastLevelLabel();
|
||||||
var aaaNormalBox = wsagBox(aaaNormalLabel, "AAA Normal");
|
var aaaNormalBox = contrastLevelBox(aaaNormalLabel, "AAA Normal");
|
||||||
|
|
||||||
var aaaLargeLabel = wsagLabel();
|
var aaaLargeLabel = contrastLevelLabel();
|
||||||
var aaaLargeBox = wsagBox(aaaLargeLabel, "AAA Large");
|
var aaaLargeBox = contrastLevelBox(aaaLargeLabel, "AAA Large");
|
||||||
|
|
||||||
var wsagBox = new HBox(20, aaNormalBox, aaLargeBox, aaaNormalBox, aaaLargeBox);
|
var contrastLevels = new HBox(20, aaNormalBox, aaLargeBox, aaaNormalBox, aaaLargeBox);
|
||||||
wsagBox.getStyleClass().add("wsag-box");
|
|
||||||
|
|
||||||
contrastRatio.addListener((obs, old, val) -> {
|
contrastRatio.addListener((obs, old, val) -> {
|
||||||
if (val == null) { return; }
|
if (val == null) { return; }
|
||||||
float ratio = val.floatValue();
|
float ratio = val.floatValue();
|
||||||
if (ratio >= 7) {
|
updateWsagLabel(aaNormalLabel, ContrastLevel.AA_NORMAL.satisfies(ratio));
|
||||||
updateWsagLabel(aaNormalLabel, true);
|
updateWsagLabel(aaLargeLabel, ContrastLevel.AA_LARGE.satisfies(ratio));
|
||||||
updateWsagLabel(aaLargeLabel, true);
|
updateWsagLabel(aaaNormalLabel, ContrastLevel.AAA_NORMAL.satisfies(ratio));
|
||||||
updateWsagLabel(aaaNormalLabel, true);
|
updateWsagLabel(aaaLargeLabel, ContrastLevel.AAA_LARGE.satisfies(ratio));
|
||||||
updateWsagLabel(aaaLargeLabel, true);
|
|
||||||
} else if (ratio < 7 && ratio >= 4.5) {
|
|
||||||
updateWsagLabel(aaNormalLabel, true);
|
|
||||||
updateWsagLabel(aaLargeLabel, true);
|
|
||||||
updateWsagLabel(aaaNormalLabel, false);
|
|
||||||
updateWsagLabel(aaaLargeLabel, true);
|
|
||||||
} else if (ratio < 4.5 && ratio >= 3) {
|
|
||||||
updateWsagLabel(aaNormalLabel, false);
|
|
||||||
updateWsagLabel(aaLargeLabel, true);
|
|
||||||
updateWsagLabel(aaaNormalLabel, false);
|
|
||||||
updateWsagLabel(aaaLargeLabel, false);
|
|
||||||
} else {
|
|
||||||
updateWsagLabel(aaNormalLabel, false);
|
|
||||||
updateWsagLabel(aaLargeLabel, false);
|
|
||||||
updateWsagLabel(aaaNormalLabel, false);
|
|
||||||
updateWsagLabel(aaaLargeLabel, false);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// ~
|
// ~
|
||||||
@ -296,7 +282,7 @@ public class ContrastChecker extends GridPane {
|
|||||||
getStyleClass().add("contrast-checker");
|
getStyleClass().add("contrast-checker");
|
||||||
|
|
||||||
// column 0
|
// column 0
|
||||||
add(new HBox(fontBox, new Spacer(), wsagBox), 0, 0, REMAINING, 1);
|
add(new HBox(contrastRatioBox, new Spacer(), contrastLevels), 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);
|
||||||
@ -359,25 +345,26 @@ public class ContrastChecker extends GridPane {
|
|||||||
private void updateWsagLabel(Label label, boolean success) {
|
private void updateWsagLabel(Label label, boolean success) {
|
||||||
FontIcon icon = Objects.requireNonNull((FontIcon) label.getGraphic());
|
FontIcon icon = Objects.requireNonNull((FontIcon) label.getGraphic());
|
||||||
if (success) {
|
if (success) {
|
||||||
label.setText("PASS");
|
label.setText(STATE_PASS);
|
||||||
icon.setIconCode(Material2AL.CHECK);
|
icon.setIconCode(Material2AL.CHECK);
|
||||||
} else {
|
} else {
|
||||||
label.setText("FAIL");
|
label.setText(STATE_FAIL);
|
||||||
icon.setIconCode(Material2AL.CLOSE);
|
icon.setIconCode(Material2AL.CLOSE);
|
||||||
}
|
}
|
||||||
label.pseudoClassStateChanged(PASSED, success);
|
label.pseudoClassStateChanged(PASSED, success);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Label wsagLabel() {
|
private Label contrastLevelLabel() {
|
||||||
var label = new Label("FAIL");
|
var label = new Label(STATE_FAIL);
|
||||||
label.getStyleClass().add("wsag-label");
|
label.getStyleClass().add("state");
|
||||||
label.setContentDisplay(ContentDisplay.RIGHT);
|
label.setContentDisplay(ContentDisplay.RIGHT);
|
||||||
label.setGraphic(new FontIcon(Material2AL.CLOSE));
|
label.setGraphic(new FontIcon(Material2AL.CLOSE));
|
||||||
return label;
|
return label;
|
||||||
}
|
}
|
||||||
|
|
||||||
private VBox wsagBox(Label label, String description) {
|
private VBox contrastLevelBox(Label label, String description) {
|
||||||
var box = new VBox(10, label, new Label(description));
|
var box = new VBox(10, label, new Label(description));
|
||||||
|
box.getStyleClass().add("contrast-level");
|
||||||
box.setAlignment(Pos.CENTER);
|
box.setAlignment(Pos.CENTER);
|
||||||
return box;
|
return box;
|
||||||
}
|
}
|
||||||
@ -392,12 +379,6 @@ public class ContrastChecker extends GridPane {
|
|||||||
return slider;
|
return slider;
|
||||||
}
|
}
|
||||||
|
|
||||||
static double getContrastRatioOpacityAware(Color bgColor, Color fgColor, Color bgBaseColor) {
|
|
||||||
double luminance1 = getColorLuminance(flattenColor(bgBaseColor, bgColor));
|
|
||||||
double luminance2 = getColorLuminance(flattenColor(bgBaseColor, fgColor));
|
|
||||||
return JColorUtils.getContrastRatio(luminance1, luminance2);
|
|
||||||
}
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
private static class ObservableHSLAColor {
|
private static class ObservableHSLAColor {
|
||||||
|
@ -10,12 +10,17 @@ 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;
|
||||||
|
import javafx.scene.text.Text;
|
||||||
|
import javafx.scene.text.TextFlow;
|
||||||
import javafx.util.Duration;
|
import javafx.util.Duration;
|
||||||
import javafx.util.StringConverter;
|
import javafx.util.StringConverter;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
import static atlantafx.sampler.util.Controls.hyperlink;
|
||||||
|
|
||||||
public class ThemePage extends AbstractPage {
|
public class ThemePage extends AbstractPage {
|
||||||
|
|
||||||
public static final String NAME = "Theme";
|
public static final String NAME = "Theme";
|
||||||
@ -59,11 +64,21 @@ public class ThemePage extends AbstractPage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void createView() {
|
private void createView() {
|
||||||
|
var noteText = new TextFlow(
|
||||||
|
new Text("AtlantaFX follows "),
|
||||||
|
hyperlink("Github Primer interface guidelines",
|
||||||
|
URI.create("https://primer.style/design/foundations/color")
|
||||||
|
),
|
||||||
|
new Text(" and color system.")
|
||||||
|
);
|
||||||
|
|
||||||
userContent.getChildren().addAll(
|
userContent.getChildren().addAll(
|
||||||
optionsGrid(),
|
optionsGrid(),
|
||||||
|
noteText,
|
||||||
colorPalette,
|
colorPalette,
|
||||||
colorScale
|
colorScale
|
||||||
);
|
);
|
||||||
|
|
||||||
// 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
|
||||||
NodeUtils.toggleVisibility(quickConfigBtn, false);
|
NodeUtils.toggleVisibility(quickConfigBtn, false);
|
||||||
|
@ -0,0 +1,74 @@
|
|||||||
|
/* SPDX-License-Identifier: MIT */
|
||||||
|
package atlantafx.sampler.util;
|
||||||
|
|
||||||
|
import javafx.scene.paint.Color;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
import static atlantafx.sampler.util.JColorUtils.flattenColor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The WCAG contrast measures the difference in brightness (luminance) between two colours.
|
||||||
|
* It ranges from 1:1 (white on white) to 21:1 (black on white). WCAG requirements are:
|
||||||
|
* <ul>
|
||||||
|
* <li>0.14285 (7.0:1) for small text in AAA-level</li>
|
||||||
|
* <li>0.22222 (4.5:1) for small text in AA-level, or large text in AAA-level</li>
|
||||||
|
* <li>0.33333 (3.0:1) for large text in AA-level</li>
|
||||||
|
* </ul>
|
||||||
|
* WCAG defines large text as text that is 18pt and larger, or 14pt and larger if it is bold.
|
||||||
|
* <br/>
|
||||||
|
* <a href="https://www.w3.org/TR/WCAG20-TECHS/G18.html">More info</a>.
|
||||||
|
*/
|
||||||
|
public enum ContrastLevel {
|
||||||
|
|
||||||
|
AA_NORMAL,
|
||||||
|
AA_LARGE,
|
||||||
|
AAA_NORMAL,
|
||||||
|
AAA_LARGE;
|
||||||
|
|
||||||
|
public boolean satisfies(double ratio) {
|
||||||
|
switch (this) {
|
||||||
|
case AA_NORMAL -> { return ratio >= 4.5; }
|
||||||
|
case AA_LARGE -> { return ratio >= 3; }
|
||||||
|
case AAA_NORMAL -> { return ratio >= 7; }
|
||||||
|
case AAA_LARGE -> { return ratio >= 4.5; }
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static double getContrastRatio(Color color1, Color color2) {
|
||||||
|
return getContrastRatio(getColorLuminance(color1), getColorLuminance(color2));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static double getContrastRatio(double luminance1, double luminance2) {
|
||||||
|
return 1 / (luminance1 > luminance2 ?
|
||||||
|
(luminance2 + 0.05) / (luminance1 + 0.05) :
|
||||||
|
(luminance1 + 0.05) / (luminance2 + 0.05)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Measures relative color luminance according to the
|
||||||
|
* <a href="https://www.w3.org/TR/WCAG20-TECHS/G18.html">W3C</a>.
|
||||||
|
* <br/>
|
||||||
|
* Note that JavaFX provides {@link Color#getBrightness()} which
|
||||||
|
* IS NOT the same thing as luminance.
|
||||||
|
*/
|
||||||
|
public static double getColorLuminance(double[] rgb) {
|
||||||
|
double[] tmp = Arrays.stream(rgb)
|
||||||
|
.map(v -> v <= 0.03928 ? (v / 12.92) : Math.pow((v + 0.055) / 1.055, 2.4))
|
||||||
|
.toArray();
|
||||||
|
return (tmp[0] * 0.2126) + (tmp[1] * 0.7152) + (tmp[2] * 0.0722);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @see ContrastLevel#getColorLuminance(double[]) */
|
||||||
|
public static double getColorLuminance(Color color) {
|
||||||
|
return getColorLuminance(new double[] { color.getRed(), color.getGreen(), color.getBlue() });
|
||||||
|
}
|
||||||
|
}
|
@ -1,14 +1,15 @@
|
|||||||
/* SPDX-License-Identifier: MIT */
|
/* SPDX-License-Identifier: MIT */
|
||||||
package atlantafx.sampler.util;
|
package atlantafx.sampler.util;
|
||||||
|
|
||||||
import javafx.scene.control.Button;
|
import atlantafx.sampler.event.BrowseEvent;
|
||||||
import javafx.scene.control.MenuItem;
|
import atlantafx.sampler.event.DefaultEventBus;
|
||||||
import javafx.scene.control.ToggleButton;
|
import javafx.scene.control.*;
|
||||||
import javafx.scene.control.ToggleGroup;
|
|
||||||
import javafx.scene.input.KeyCombination;
|
import javafx.scene.input.KeyCombination;
|
||||||
import org.kordamp.ikonli.Ikon;
|
import org.kordamp.ikonli.Ikon;
|
||||||
import org.kordamp.ikonli.javafx.FontIcon;
|
import org.kordamp.ikonli.javafx.FontIcon;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
|
||||||
import static atlantafx.base.theme.Styles.BUTTON_ICON;
|
import static atlantafx.base.theme.Styles.BUTTON_ICON;
|
||||||
|
|
||||||
public final class Controls {
|
public final class Controls {
|
||||||
@ -52,4 +53,12 @@ public final class Controls {
|
|||||||
|
|
||||||
return toggleButton;
|
return toggleButton;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Hyperlink hyperlink(String text, URI uri) {
|
||||||
|
var hyperlink = new Hyperlink(text);
|
||||||
|
if (uri != null) {
|
||||||
|
hyperlink.setOnAction(event -> DefaultEventBus.getInstance().publish(new BrowseEvent(uri)));
|
||||||
|
}
|
||||||
|
return hyperlink;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -780,48 +780,6 @@ public class JColorUtils {
|
|||||||
// Additional Utils, mkpaz (c) //
|
// Additional Utils, mkpaz (c) //
|
||||||
///////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
/**
|
|
||||||
* The WCAG contrast measures the difference in brightness (luminance) between two colours.
|
|
||||||
* It ranges from 1:1 (white on white) to 21:1 (black on white). WCAG requirements are:
|
|
||||||
* <ul>
|
|
||||||
* <li>0.14285 (7.0:1) for small text in AAA-level</li>
|
|
||||||
* <li>0.22222 (4.5:1) for small text in AA-level, or large text in AAA-level</li>
|
|
||||||
* <li>0.33333 (3.0:1) for large text in AA-level</li>
|
|
||||||
* </ul>
|
|
||||||
* WCAG defines large text as text that is 18pt and larger, or 14pt and larger if it is bold.
|
|
||||||
* <br/>
|
|
||||||
* <a href="https://www.w3.org/TR/WCAG20-TECHS/G18.html">More info</a>.
|
|
||||||
*/
|
|
||||||
public static double getContrastRatio(Color color1, Color color2) {
|
|
||||||
return getContrastRatio(getColorLuminance(color1), getColorLuminance(color2));
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @see JColorUtils#getContrastRatio(Color, Color) */
|
|
||||||
public static double getContrastRatio(double luminance1, double luminance2) {
|
|
||||||
return luminance1 > luminance2 ?
|
|
||||||
(luminance2 + 0.05) / (luminance1 + 0.05) :
|
|
||||||
(luminance1 + 0.05) / (luminance2 + 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Measures relative color luminance according to the
|
|
||||||
* <a href="https://www.w3.org/TR/WCAG20-TECHS/G18.html">W3C</a>.
|
|
||||||
* <br/>
|
|
||||||
* Note that JavaFX provides {@link Color#getBrightness()} which
|
|
||||||
* IS NOT the same thing as luminance.
|
|
||||||
*/
|
|
||||||
public static double getColorLuminance(double[] rgb) {
|
|
||||||
double[] tmp = Arrays.stream(rgb)
|
|
||||||
.map(v -> v <= 0.03928 ? (v / 12.92) : Math.pow((v + 0.055) / 1.055, 2.4))
|
|
||||||
.toArray();
|
|
||||||
return (tmp[0] * 0.2126) + (tmp[1] * 0.7152) + (tmp[2] * 0.0722);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @see JColorUtils#getColorLuminance(double[]) */
|
|
||||||
public static double getColorLuminance(Color color) {
|
|
||||||
return getColorLuminance(new double[] { color.getRed(), color.getGreen(), color.getBlue() });
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes given color opacity, if present.
|
* Removes given color opacity, if present.
|
||||||
* <br/><br/>
|
* <br/><br/>
|
||||||
|
@ -20,6 +20,7 @@ module atlantafx.sampler {
|
|||||||
exports atlantafx.sampler;
|
exports atlantafx.sampler;
|
||||||
exports atlantafx.sampler.fake;
|
exports atlantafx.sampler.fake;
|
||||||
exports atlantafx.sampler.fake.domain;
|
exports atlantafx.sampler.fake.domain;
|
||||||
|
exports atlantafx.sampler.event;
|
||||||
exports atlantafx.sampler.layout;
|
exports atlantafx.sampler.layout;
|
||||||
exports atlantafx.sampler.page;
|
exports atlantafx.sampler.page;
|
||||||
exports atlantafx.sampler.page.general;
|
exports atlantafx.sampler.page.general;
|
||||||
|
@ -13,21 +13,21 @@ $color-wsag-fg: white;
|
|||||||
-fx-vgap: 20px;
|
-fx-vgap: 20px;
|
||||||
-fx-hgap: 40px;
|
-fx-hgap: 40px;
|
||||||
|
|
||||||
>.color-block {
|
>.block {
|
||||||
-fx-spacing: 5px;
|
-fx-spacing: 5px;
|
||||||
|
|
||||||
>.box {
|
>.rectangle {
|
||||||
-fx-min-width: 12em;
|
-fx-min-width: 12em;
|
||||||
-fx-min-height: 5em;
|
-fx-min-height: 5em;
|
||||||
-fx-max-width: 12em;
|
-fx-max-width: 12em;
|
||||||
-fx-max-height: 5em;
|
-fx-max-height: 5em;
|
||||||
-fx-cursor: hand;
|
-fx-cursor: hand;
|
||||||
|
|
||||||
&:passed>.wsag-label {
|
&:passed>.contrast-level-label {
|
||||||
-fx-background-color: $color-wsag-bg-passed;
|
-fx-background-color: $color-wsag-bg-passed;
|
||||||
}
|
}
|
||||||
|
|
||||||
>.wsag-label {
|
>.contrast-level-label {
|
||||||
-fx-text-fill: $color-wsag-fg;
|
-fx-text-fill: $color-wsag-fg;
|
||||||
-fx-background-color: $color-wsag-bg-failed;
|
-fx-background-color: $color-wsag-bg-failed;
|
||||||
-fx-background-radius: 6px;
|
-fx-background-radius: 6px;
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
@use "color-palette" as palette;
|
@use "color-palette" as palette;
|
||||||
|
|
||||||
.contrast-checker {
|
.contrast-checker {
|
||||||
|
|
||||||
-fx-background-color: -color-contrast-checker-bg;
|
-fx-background-color: -color-contrast-checker-bg;
|
||||||
-fx-hgap: 40px;
|
-fx-hgap: 40px;
|
||||||
-fx-vgap: 20px;
|
-fx-vgap: 20px;
|
||||||
@ -47,11 +48,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.ikonli-font-icon {
|
|
||||||
-fx-icon-color: -color-contrast-checker-fg;
|
|
||||||
-fx-fill: -color-contrast-checker-fg;
|
|
||||||
}
|
|
||||||
|
|
||||||
.slider {
|
.slider {
|
||||||
>.thumb {
|
>.thumb {
|
||||||
-fx-background-color: -color-contrast-checker-fg;
|
-fx-background-color: -color-contrast-checker-fg;
|
||||||
@ -63,10 +59,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.font-box {
|
.ikonli-font-icon {
|
||||||
|
-fx-icon-color: -color-contrast-checker-fg;
|
||||||
|
-fx-fill: -color-contrast-checker-fg;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contrast-ratio {
|
||||||
-fx-padding: 0 40px 0 0;
|
-fx-padding: 0 40px 0 0;
|
||||||
|
|
||||||
>.text {
|
>.large-font {
|
||||||
-fx-font-size: 4em;
|
-fx-font-size: 4em;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -75,7 +76,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.wsag-box>*>.wsag-label {
|
.contrast-level {
|
||||||
|
>.state {
|
||||||
-fx-padding: 0.5em 1em 0.5em 1em;
|
-fx-padding: 0.5em 1em 0.5em 1em;
|
||||||
-fx-background-color: palette.$color-wsag-bg-failed;
|
-fx-background-color: palette.$color-wsag-bg-failed;
|
||||||
-fx-background-radius: 4px;
|
-fx-background-radius: 4px;
|
||||||
@ -91,6 +93,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.contrast-checker-dialog {
|
.contrast-checker-dialog {
|
||||||
-color-dialog-bg: white;
|
-color-dialog-bg: white;
|
||||||
|
Loading…
Reference in New Issue
Block a user