diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ae9776..d60ec8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,17 @@ ## [Unreleased] +### Features + +- (Sampler) External themes support. Sampler can now be used to develop custom themes. Just clone [AtlantaFX sample theme](https://github.com/mkpaz/atlantafx-sample-theme) repository, compile it and add resulting CSS file to the Sampler. +- (CSS) `TextField` and `Button` now support rounded style. + ### Improvements -- `Hyperlink`, `TextField`, `TextArea` and `ProgressBar` can now be styled via looked-up color variables. +- (CSS) Nord light and dark themes revamped with better color contrast and improved design. +- (CSS) `Hyperlink`, `TextField`, `TextArea` and `ProgressBar` can now be styled via looked-up color variables. + +### Fixes ## [0.1] diff --git a/base/src/main/java/atlantafx/base/controls/InlineDatePicker.java b/base/src/main/java/atlantafx/base/controls/InlineDatePicker.java index ce15316..1b7e7ef 100755 --- a/base/src/main/java/atlantafx/base/controls/InlineDatePicker.java +++ b/base/src/main/java/atlantafx/base/controls/InlineDatePicker.java @@ -80,7 +80,7 @@ public class InlineDatePicker extends Control { if (isValidDate(chrono, date)) { lastValidDate = date; } else { - System.err.println("Restoring value to " + (lastValidDate == null ? "null" : lastValidDate)); + System.err.println("[ERROR] Restoring value to " + (lastValidDate == null ? "null" : lastValidDate)); setValue(lastValidDate); } }); @@ -92,7 +92,7 @@ public class InlineDatePicker extends Control { if (isValidDate(chrono, date)) { lastValidChronology = chrono; } else { - System.err.println("Restoring value to " + lastValidChronology); + System.err.println("[ERROR] Restoring value to " + lastValidChronology); setChronology(lastValidChronology); } }); diff --git a/base/src/main/java/atlantafx/base/theme/AbstractTheme.java b/base/src/main/java/atlantafx/base/theme/AbstractTheme.java deleted file mode 100644 index b47f1a6..0000000 --- a/base/src/main/java/atlantafx/base/theme/AbstractTheme.java +++ /dev/null @@ -1,40 +0,0 @@ -/* SPDX-License-Identifier: MIT */ -package atlantafx.base.theme; - -import java.net.URI; -import java.util.LinkedHashSet; -import java.util.Objects; -import java.util.Set; - -public abstract class AbstractTheme implements Theme { - - private final Set stylesheets; - - public AbstractTheme() { - this(new LinkedHashSet<>()); - } - - public AbstractTheme(URI... stylesheets) { - this(Set.of(stylesheets)); - } - - public AbstractTheme(Set stylesheets) { - this.stylesheets = Objects.requireNonNull(stylesheets); - } - - @Override - public Set getStylesheets() { - return stylesheets; - } - - @Override - public String toString() { - return getClass().getSimpleName() + - "{" + - "name=" + getName() + - ", userAgentStylesheet=" + getUserAgentStylesheet() + - ", stylesheets=" + stylesheets + - ", isDarkMode=" + isDarkMode() + - '}'; - } -} diff --git a/base/src/main/java/atlantafx/base/theme/NordDark.java b/base/src/main/java/atlantafx/base/theme/NordDark.java index e2b8d71..bb2c45c 100755 --- a/base/src/main/java/atlantafx/base/theme/NordDark.java +++ b/base/src/main/java/atlantafx/base/theme/NordDark.java @@ -1,20 +1,9 @@ /* SPDX-License-Identifier: MIT */ package atlantafx.base.theme; -import java.net.URI; -import java.util.Set; +public final class NordDark implements Theme { -public class NordDark extends AbstractTheme { - - public NordDark() {} - - public NordDark(URI... stylesheets) { - super(stylesheets); - } - - public NordDark(Set stylesheets) { - super(stylesheets); - } + public NordDark() { } @Override public String getName() { diff --git a/base/src/main/java/atlantafx/base/theme/NordLight.java b/base/src/main/java/atlantafx/base/theme/NordLight.java index bc4e075..8b40d32 100755 --- a/base/src/main/java/atlantafx/base/theme/NordLight.java +++ b/base/src/main/java/atlantafx/base/theme/NordLight.java @@ -1,20 +1,9 @@ /* SPDX-License-Identifier: MIT */ package atlantafx.base.theme; -import java.net.URI; -import java.util.Set; +public final class NordLight implements Theme { -public class NordLight extends AbstractTheme { - - public NordLight() {} - - public NordLight(URI... stylesheets) { - super(stylesheets); - } - - public NordLight(Set stylesheets) { - super(stylesheets); - } + public NordLight() { } @Override public String getName() { diff --git a/base/src/main/java/atlantafx/base/theme/PrimerDark.java b/base/src/main/java/atlantafx/base/theme/PrimerDark.java index 139b378..4586bbd 100755 --- a/base/src/main/java/atlantafx/base/theme/PrimerDark.java +++ b/base/src/main/java/atlantafx/base/theme/PrimerDark.java @@ -1,20 +1,9 @@ /* SPDX-License-Identifier: MIT */ package atlantafx.base.theme; -import java.net.URI; -import java.util.Set; +public final class PrimerDark implements Theme { -public class PrimerDark extends AbstractTheme { - - public PrimerDark() {} - - public PrimerDark(URI... stylesheets) { - super(stylesheets); - } - - public PrimerDark(Set stylesheets) { - super(stylesheets); - } + public PrimerDark() { } @Override public String getName() { diff --git a/base/src/main/java/atlantafx/base/theme/PrimerLight.java b/base/src/main/java/atlantafx/base/theme/PrimerLight.java index 6922332..2b5adb8 100755 --- a/base/src/main/java/atlantafx/base/theme/PrimerLight.java +++ b/base/src/main/java/atlantafx/base/theme/PrimerLight.java @@ -1,20 +1,9 @@ /* SPDX-License-Identifier: MIT */ package atlantafx.base.theme; -import java.net.URI; -import java.util.Set; +public final class PrimerLight implements Theme { -public class PrimerLight extends AbstractTheme { - - public PrimerLight() {} - - public PrimerLight(URI... stylesheets) { - super(stylesheets); - } - - public PrimerLight(Set stylesheets) { - super(stylesheets); - } + public PrimerLight() { } @Override public String getName() { diff --git a/base/src/main/java/atlantafx/base/theme/Theme.java b/base/src/main/java/atlantafx/base/theme/Theme.java index 86fefd2..e797b1d 100755 --- a/base/src/main/java/atlantafx/base/theme/Theme.java +++ b/base/src/main/java/atlantafx/base/theme/Theme.java @@ -1,8 +1,7 @@ /* SPDX-License-Identifier: MIT */ package atlantafx.base.theme; -import java.net.URI; -import java.util.Set; +import java.util.Objects; import static javafx.application.Application.STYLESHEET_CASPIAN; import static javafx.application.Application.STYLESHEET_MODENA; @@ -16,10 +15,31 @@ public interface Theme { String getUserAgentStylesheet(); - Set getStylesheets(); - boolean isDarkMode(); + static Theme of(String name, String userAgentStylesheet, boolean darkMode) { + Objects.requireNonNull(name); + Objects.requireNonNull(userAgentStylesheet); + + return new Theme() { + + @Override + public String getName() { + return name; + } + + @Override + public String getUserAgentStylesheet() { + return userAgentStylesheet; + } + + @Override + public boolean isDarkMode() { + return darkMode; + } + }; + } + default boolean isDefault() { return STYLESHEET_MODENA.equals(getUserAgentStylesheet()) || STYLESHEET_CASPIAN.equals(getUserAgentStylesheet()); } diff --git a/sampler/pom.xml b/sampler/pom.xml index 27c4cbe..7925b73 100755 --- a/sampler/pom.xml +++ b/sampler/pom.xml @@ -223,7 +223,7 @@ jlink - java.base,java.logging,jdk.localedata,java.desktop,javafx.controls,javafx.swing,javafx.web + java.base,java.logging,jdk.localedata,java.desktop,java.prefs,javafx.controls,javafx.swing,javafx.web ${build.platformModulesDir} ${build.package.runtimeImageDir} diff --git a/sampler/src/main/java/atlantafx/sampler/FileResource.java b/sampler/src/main/java/atlantafx/sampler/FileResource.java new file mode 100644 index 0000000..38757eb --- /dev/null +++ b/sampler/src/main/java/atlantafx/sampler/FileResource.java @@ -0,0 +1,72 @@ +/* SPDX-License-Identifier: MIT */ +package atlantafx.sampler; + +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Objects; + +public final class FileResource { + + private final String location; + private final boolean internal; + private final Class anchor; + + private FileResource(String location, boolean internal, Class anchor) { + this.location = location; + this.internal = internal; + this.anchor = anchor; + } + + public String location() { + return location; + } + + public boolean internal() { + return internal; + } + + public boolean exists() { + return internal ? anchor.getResource(location) != null : Files.exists(toPath()); + } + + public Path toPath() { + return Paths.get(location); + } + + public URI toURI() { + // the latter adds "file://" scheme to the URI + return internal ? URI.create(location) : Paths.get(location).toUri(); + } + + public String getFilename() { + return Paths.get(location).getFileName().toString(); + } + + public InputStream getInputStream() throws IOException { + if (internal) { + var is = anchor.getResourceAsStream(location); + if (is == null) { throw new IOException("Resource not found: " + location); } + return is; + } + return new FileInputStream(toPath().toFile()); + } + + /////////////////////////////////////////////////////////////////////////// + + public static FileResource internal(String location) { + return internal(location, FileResource.class); + } + + public static FileResource internal(String location, Class anchor) { + return new FileResource(location, true, Objects.requireNonNull(anchor)); + } + + public static FileResource external(String location) { + return new FileResource(location, false, null); + } +} \ No newline at end of file diff --git a/sampler/src/main/java/atlantafx/sampler/Launcher.java b/sampler/src/main/java/atlantafx/sampler/Launcher.java index 804f051..7d0e4a0 100755 --- a/sampler/src/main/java/atlantafx/sampler/Launcher.java +++ b/sampler/src/main/java/atlantafx/sampler/Launcher.java @@ -9,6 +9,7 @@ import atlantafx.sampler.theme.ThemeManager; import fr.brouillard.oss.cssfx.CSSFX; import fr.brouillard.oss.cssfx.api.URIToPathConverter; import fr.brouillard.oss.cssfx.impl.log.CSSFXLogger; +import fr.brouillard.oss.cssfx.impl.log.CSSFXLogger.LogLevel; import javafx.application.Application; import javafx.application.Platform; import javafx.scene.Scene; @@ -55,7 +56,7 @@ public class Launcher extends Application { var tm = ThemeManager.getInstance(); tm.setScene(scene); - tm.setTheme(tm.getAvailableThemes().get(0)); + tm.setTheme(tm.getDefaultTheme()); if (IS_DEV_MODE) { startCssFX(scene); } scene.getStylesheets().addAll(Resources.resolve("assets/styles/index.css")); @@ -110,9 +111,11 @@ public class Launcher extends Application { }; CSSFX.addConverter(fileUrlConverter).start(); - CSSFXLogger.setLoggerFactory(loggerName -> (level, message, args) -> - System.out.println("[CSSFX] " + String.format(message, args)) - ); + CSSFXLogger.setLoggerFactory(loggerName -> (level, message, args) -> { + if (level.ordinal() <= LogLevel.INFO.ordinal()) { + System.out.println("[" + level + "] CSSFX: " + String.format(message, args)); + } + }); CSSFX.start(scene); } diff --git a/sampler/src/main/java/atlantafx/sampler/Resources.java b/sampler/src/main/java/atlantafx/sampler/Resources.java index 29c5df4..3d08269 100755 --- a/sampler/src/main/java/atlantafx/sampler/Resources.java +++ b/sampler/src/main/java/atlantafx/sampler/Resources.java @@ -5,6 +5,7 @@ import java.io.InputStream; import java.net.URI; import java.net.URL; import java.util.Objects; +import java.util.prefs.Preferences; public final class Resources { @@ -32,4 +33,8 @@ public final class Resources { public static String getPropertyOrEnv(String propertyKey, String envKey) { return System.getProperty(propertyKey, System.getenv(envKey)); } + + public static Preferences getPreferences() { + return Preferences.userRoot().node("atlantafx"); + } } diff --git a/sampler/src/main/java/atlantafx/sampler/event/ThemeEvent.java b/sampler/src/main/java/atlantafx/sampler/event/ThemeEvent.java index 98296c1..332e8fc 100644 --- a/sampler/src/main/java/atlantafx/sampler/event/ThemeEvent.java +++ b/sampler/src/main/java/atlantafx/sampler/event/ThemeEvent.java @@ -18,6 +18,9 @@ public class ThemeEvent extends Event { // font size or family only change FONT_CHANGE, // colors only change - COLOR_CHANGE + COLOR_CHANGE, + // new theme added or removed + THEME_ADD, + THEME_REMOVE } } diff --git a/sampler/src/main/java/atlantafx/sampler/page/QuickConfigMenu.java b/sampler/src/main/java/atlantafx/sampler/page/QuickConfigMenu.java index 51f6992..37915d3 100644 --- a/sampler/src/main/java/atlantafx/sampler/page/QuickConfigMenu.java +++ b/sampler/src/main/java/atlantafx/sampler/page/QuickConfigMenu.java @@ -211,7 +211,7 @@ public class QuickConfigMenu extends StackPane { getChildren().setAll(mainMenu, new Separator()); - tm.getAvailableThemes().forEach(theme -> { + tm.getRepository().getAll().forEach(theme -> { var icon = new FontIcon(Material2AL.CHECK); var item = new HBox(20, icon, new Label(theme.getName())); 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 dcc9696..716c2c8 100755 --- a/sampler/src/main/java/atlantafx/sampler/page/general/ThemePage.java +++ b/sampler/src/main/java/atlantafx/sampler/page/general/ThemePage.java @@ -1,36 +1,42 @@ /* SPDX-License-Identifier: MIT */ package atlantafx.sampler.page.general; -import atlantafx.base.theme.Theme; +import atlantafx.base.theme.Styles; import atlantafx.sampler.event.DefaultEventBus; import atlantafx.sampler.event.ThemeEvent; import atlantafx.sampler.page.AbstractPage; +import atlantafx.sampler.theme.SamplerTheme; import atlantafx.sampler.theme.ThemeManager; import atlantafx.sampler.util.NodeUtils; import javafx.geometry.HPos; +import javafx.scene.control.Button; import javafx.scene.control.ChoiceBox; import javafx.scene.control.Label; +import javafx.scene.control.Tooltip; import javafx.scene.layout.GridPane; import javafx.scene.text.Text; import javafx.scene.text.TextFlow; import javafx.util.Duration; import javafx.util.StringConverter; +import org.kordamp.ikonli.javafx.FontIcon; +import org.kordamp.ikonli.material2.Material2OutlinedMZ; import java.net.URI; import java.util.Objects; import java.util.function.Consumer; -import static atlantafx.sampler.event.ThemeEvent.EventType.COLOR_CHANGE; -import static atlantafx.sampler.event.ThemeEvent.EventType.THEME_CHANGE; +import static atlantafx.sampler.event.ThemeEvent.EventType.*; import static atlantafx.sampler.util.Controls.hyperlink; public class ThemePage extends AbstractPage { public static final String NAME = "Theme"; + private static final ThemeManager TM = ThemeManager.getInstance(); private final Consumer colorBlockActionHandler = colorBlock -> { ContrastCheckerDialog dialog = getOrCreateContrastCheckerDialog(); - dialog.getContent().setValues(colorBlock.getFgColorName(), + dialog.getContent().setValues( + colorBlock.getFgColorName(), colorBlock.getFgColor(), colorBlock.getBgColorName(), colorBlock.getBgColor() @@ -41,7 +47,9 @@ public class ThemePage extends AbstractPage { private final ColorPalette colorPalette = new ColorPalette(colorBlockActionHandler); private final ColorScale colorScale = new ColorScale(); + private final ChoiceBox themeSelector = themeSelector(); + private ThemeRepoManagerDialog themeRepoManagerDialog; private ContrastCheckerDialog contrastCheckerDialog; @Override @@ -51,6 +59,10 @@ public class ThemePage extends AbstractPage { super(); createView(); DefaultEventBus.getInstance().subscribe(ThemeEvent.class, e -> { + if (e.getEventType() == THEME_ADD || e.getEventType() == THEME_REMOVE) { + themeSelector.getItems().setAll(TM.getRepository().getAll()); + selectCurrentTheme(); + } if (e.getEventType() == THEME_CHANGE || e.getEventType() == COLOR_CHANGE) { colorPalette.updateColorInfo(Duration.seconds(1)); colorScale.updateColorInfo(Duration.seconds(1)); @@ -69,7 +81,7 @@ public class ThemePage extends AbstractPage { var noteText = new TextFlow( new Text("AtlantaFX follows "), hyperlink("Github Primer interface guidelines", - URI.create("https://primer.style/design/foundations/color") + URI.create("https://primer.style/design/foundations/color") ), new Text(" and color system.") ); @@ -81,6 +93,8 @@ public class ThemePage extends AbstractPage { colorScale ); + selectCurrentTheme(); + // if you want to enable quick menu don't forget that // theme selection choice box have to be updated accordingly NodeUtils.toggleVisibility(quickConfigBtn, false); @@ -88,8 +102,17 @@ public class ThemePage extends AbstractPage { } private GridPane optionsGrid() { - ChoiceBox themeSelector = themeSelector(); - themeSelector.setPrefWidth(200); + var themeRepoBtn = new Button("", new FontIcon(Material2OutlinedMZ.SETTINGS)); + themeRepoBtn.getStyleClass().addAll(Styles.BUTTON_ICON, Styles.FLAT); + themeRepoBtn.setTooltip(new Tooltip("Settings")); + themeRepoBtn.setOnAction(e -> { + ThemeRepoManagerDialog dialog = getOrCreateThemeRepoManagerDialog(); + + overlay.setContent(dialog, HPos.CENTER); + dialog.getContent().update(); + + overlay.toFront(); + }); var accentSelector = new AccentColorSelector(); @@ -101,47 +124,58 @@ public class ThemePage extends AbstractPage { grid.add(new Label("Color theme"), 0, 0); grid.add(themeSelector, 1, 0); + grid.add(themeRepoBtn, 2, 0); grid.add(new Label("Accent color"), 0, 1); grid.add(accentSelector, 1, 1); return grid; } - private ChoiceBox themeSelector() { - var manager = ThemeManager.getInstance(); - var selector = new ChoiceBox(); - selector.getItems().setAll(manager.getAvailableThemes()); + private ChoiceBox themeSelector() { + var selector = new ChoiceBox(); + selector.getItems().setAll(TM.getRepository().getAll()); selector.getSelectionModel().selectedItemProperty().addListener((obs, old, val) -> { - if (val != null && getScene() != null) { - var tm = ThemeManager.getInstance(); - tm.setTheme(val); - } + if (val != null && getScene() != null) { TM.setTheme(val); } }); + selector.setPrefWidth(250); selector.setConverter(new StringConverter<>() { @Override - public String toString(Theme theme) { - return theme.getName(); + public String toString(SamplerTheme theme) { + return theme != null ? theme.getName() : ""; } @Override - public Theme fromString(String themeName) { - return manager.getAvailableThemes().stream() + public SamplerTheme fromString(String themeName) { + return TM.getRepository().getAll().stream() .filter(t -> Objects.equals(themeName, t.getName())) .findFirst() .orElse(null); } }); - // select current theme - if (manager.getTheme() != null) { - selector.getItems().stream() - .filter(t -> Objects.equals(manager.getTheme().getName(), t.getName())) - .findFirst() - .ifPresent(t -> selector.getSelectionModel().select(t)); + return selector; + } + + private void selectCurrentTheme() { + if (TM.getTheme() == null) { return; } + themeSelector.getItems().stream() + .filter(t -> Objects.equals(TM.getTheme().getName(), t.getName())) + .findFirst() + .ifPresent(t -> themeSelector.getSelectionModel().select(t)); + } + + private ThemeRepoManagerDialog getOrCreateThemeRepoManagerDialog() { + if (themeRepoManagerDialog == null) { + themeRepoManagerDialog = new ThemeRepoManagerDialog(); } - return selector; + themeRepoManagerDialog.setOnCloseRequest(() -> { + overlay.removeContent(); + overlay.toBack(); + }); + + return themeRepoManagerDialog; } private ContrastCheckerDialog getOrCreateContrastCheckerDialog() { diff --git a/sampler/src/main/java/atlantafx/sampler/page/general/ThemeRepoManager.java b/sampler/src/main/java/atlantafx/sampler/page/general/ThemeRepoManager.java new file mode 100644 index 0000000..62681e6 --- /dev/null +++ b/sampler/src/main/java/atlantafx/sampler/page/general/ThemeRepoManager.java @@ -0,0 +1,227 @@ +/* SPDX-License-Identifier: MIT */ +package atlantafx.sampler.page.general; + +import atlantafx.base.controls.Spacer; +import atlantafx.sampler.theme.SamplerTheme; +import atlantafx.sampler.theme.ThemeManager; +import atlantafx.sampler.theme.ThemeRepository; +import atlantafx.sampler.util.Containers; +import javafx.concurrent.Task; +import javafx.css.PseudoClass; +import javafx.geometry.Pos; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.ScrollPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Region; +import javafx.scene.layout.VBox; +import javafx.scene.text.Text; +import org.kordamp.ikonli.javafx.FontIcon; +import org.kordamp.ikonli.material2.Material2OutlinedAL; + +import java.io.File; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.function.Consumer; + +import static atlantafx.base.theme.Styles.*; +import static javafx.scene.control.ScrollPane.ScrollBarPolicy.AS_NEEDED; +import static javafx.scene.layout.Priority.ALWAYS; + +class ThemeRepoManager extends VBox { + + private static final Executor THREAD_POOL = Executors.newFixedThreadPool(3); + private static final ThemeRepository REPO = ThemeManager.getInstance().getRepository(); + + private VBox themeList; + + private final Consumer deleteHandler = theme -> { + REPO.remove(theme); + themeList.getChildren().removeIf(c -> { + if (c instanceof ThemeCell cell) { + return Objects.equals(theme.getName(), cell.getTheme().getName()); + } + return false; + }); + }; + + public ThemeRepoManager() { + super(); + createView(); + } + + private void createView() { + themeList = new VBox(); + themeList.getStyleClass().add("theme-list"); + REPO.getAll().forEach(theme -> themeList.getChildren().add(themeCell(theme))); + + var scrollPane = new ScrollPane(themeList); + Containers.setScrollConstraints(scrollPane, AS_NEEDED, true, AS_NEEDED, true); + scrollPane.setMaxHeight(4000); + VBox.setVgrow(scrollPane, ALWAYS); + + var infoBox = new HBox(new Label("Default or selected themes can not be removed.")); + infoBox.getStyleClass().add("info"); + + setId("theme-repo-manager"); + getChildren().addAll(scrollPane, infoBox); + } + + public void update() { + themeList.getChildren().forEach(c -> { + if (c instanceof ThemeCell cell) { cell.update(); } + }); + } + + public void addFromFile(File file) { + SamplerTheme theme = REPO.addFromFile(file); + themeList.getChildren().add(themeCell(theme)); + } + + private ThemeCell themeCell(SamplerTheme theme) { + var cell = new ThemeCell(theme); + cell.setOnDelete(deleteHandler); + return cell; + } + + /////////////////////////////////////////////////////////////////////////// + + private static class ThemeCell extends HBox { + + private static final PseudoClass SELECTED = PseudoClass.getPseudoClass("selected"); + + private final SamplerTheme theme; + + private Button deleteBtn; + private Consumer deleteHandler; + + public ThemeCell(SamplerTheme theme) { + this.theme = theme; + createView(); + } + + private void createView() { + setAlignment(Pos.CENTER_LEFT); + getStyleClass().add("theme"); + + // == TITLE == + + var text = new Text(theme.getName()); + text.getStyleClass().addAll("text"); + + String path = theme.getPath(); + if (path.length() > 64) { + path = "..." + path.substring(path.length() - 64); + } + + var subText = new Text(path); + subText.getStyleClass().addAll(TEXT_SMALL, "sub-text"); + + var titleBox = new VBox(5); + titleBox.getStyleClass().addAll("title"); + titleBox.getChildren().setAll(text, subText); + + getChildren().addAll(titleBox, new Spacer(), new Region(/* placeholder */)); + + // == PREVIEW == + + var task = new Task>() { + @Override + protected Map call() throws Exception { + return theme.parseColors(); + } + }; + + task.setOnSucceeded(e -> { + var colors = task.getValue(); + + if (colors != null && !colors.isEmpty()) { + var style = new StringBuilder(); + var presence = 0; + + presence += appendStyleIfPresent(colors, style, "-color-bg-default"); + presence += appendStyleIfPresent(colors, style, "-color-fg-default"); + presence += appendStyleIfPresent(colors, style, "-color-fg-emphasis"); + presence += appendStyleIfPresent(colors, style, "-color-accent-emphasis"); + presence += appendStyleIfPresent(colors, style, "-color-success-emphasis"); + presence += appendStyleIfPresent(colors, style, "-color-danger-emphasis"); + + // all colors must be present for preview + if (presence == 6) { + var previewBox = new HBox(); + previewBox.setAlignment(Pos.CENTER_LEFT); + previewBox.getStyleClass().add("preview"); + previewBox.setStyle(style.toString()); + previewBox.getChildren().setAll( + previewLabel("A", "-color-bg-default", "-color-fg-default"), + previewLabel("B", "-color-accent-emphasis", "-color-fg-emphasis"), + previewLabel("C", "-color-success-emphasis", "-color-fg-emphasis"), + previewLabel("D", "-color-danger-emphasis", "-color-fg-emphasis") + ); + + getChildren().set(2, previewBox); + } + } + }); + + task.setOnFailed( + e -> System.err.println("[ERROR] Unable to parse \"" + theme.getName() + "\" theme colors. Either CSS not valid or file isn't readable.") + ); + + THREAD_POOL.execute(task); + + // == CONTROLS == + + deleteBtn = new Button("", new FontIcon(Material2OutlinedAL.DELETE)); + deleteBtn.getStyleClass().addAll(BUTTON_ICON, BUTTON_CIRCLE, FLAT, DANGER); + deleteBtn.setOnAction(e -> { + if (deleteHandler != null) { deleteHandler.accept(theme); } + }); + + var controlsBox = new HBox(); + controlsBox.getStyleClass().add("controls"); + controlsBox.setAlignment(Pos.CENTER_RIGHT); + controlsBox.getChildren().add(deleteBtn); + + getChildren().add(3, controlsBox); + } + + public SamplerTheme getTheme() { + return theme; + } + + public void update() { + boolean isSelectedTheme = isSelectedTheme(); + pseudoClassStateChanged(SELECTED, isSelectedTheme); + deleteBtn.setDisable(theme.isProjectTheme() || isSelectedTheme); + } + + public void setOnDelete(Consumer handler) { + this.deleteHandler = handler; + } + + private boolean isSelectedTheme() { + return Objects.equals(ThemeManager.getInstance().getTheme().getName(), theme.getName()); + } + + private int appendStyleIfPresent(Map colors, StringBuilder sb, String colorName) { + var value = colors.get(colorName); + if (value != null) { + sb.append(colorName); + sb.append(":"); + sb.append(value); + sb.append(";"); + return 1; + } + return 0; + } + + private Label previewLabel(String text, String bg, String fg) { + var label = new Label(text); + label.setStyle(String.format("-fx-background-color:%s;-fx-text-fill:%s;", bg, fg)); + return label; + } + } +} diff --git a/sampler/src/main/java/atlantafx/sampler/page/general/ThemeRepoManagerDialog.java b/sampler/src/main/java/atlantafx/sampler/page/general/ThemeRepoManagerDialog.java new file mode 100644 index 0000000..047faea --- /dev/null +++ b/sampler/src/main/java/atlantafx/sampler/page/general/ThemeRepoManagerDialog.java @@ -0,0 +1,40 @@ +/* SPDX-License-Identifier: MIT */ +package atlantafx.sampler.page.general; + +import atlantafx.base.theme.Styles; +import atlantafx.sampler.page.OverlayDialog; +import javafx.geometry.Pos; +import javafx.scene.control.Button; +import javafx.stage.FileChooser; +import javafx.stage.FileChooser.ExtensionFilter; +import org.kordamp.ikonli.javafx.FontIcon; +import org.kordamp.ikonli.material2.Material2MZ; + +import java.io.File; + +class ThemeRepoManagerDialog extends OverlayDialog { + + private final ThemeRepoManager repoManager = new ThemeRepoManager(); + + public ThemeRepoManagerDialog() { + setId("theme-repo-manager-dialog"); + setTitle("Theme Manager"); + setContent(repoManager); + + var addBtn = new Button("Add", new FontIcon(Material2MZ.PLUS)); + addBtn.getStyleClass().add(Styles.ACCENT); + addBtn.setOnAction(e -> { + var fileChooser = new FileChooser(); + fileChooser.getExtensionFilters().addAll(new ExtensionFilter("CSS (*.css)", "*.css")); + File file = fileChooser.showOpenDialog(getScene().getWindow()); + if (file != null) { repoManager.addFromFile(file); } + }); + + footerBox.getChildren().add(0, addBtn); + footerBox.setAlignment(Pos.CENTER_LEFT); + } + + public ThemeRepoManager getContent() { + return repoManager; + } +} diff --git a/sampler/src/main/java/atlantafx/sampler/theme/ExternalTheme.java b/sampler/src/main/java/atlantafx/sampler/theme/ExternalTheme.java deleted file mode 100644 index 9e209aa..0000000 --- a/sampler/src/main/java/atlantafx/sampler/theme/ExternalTheme.java +++ /dev/null @@ -1,38 +0,0 @@ -/* SPDX-License-Identifier: MIT */ -package atlantafx.sampler.theme; - -import atlantafx.base.theme.AbstractTheme; - -import java.net.URI; -import java.util.Objects; -import java.util.Set; - -public class ExternalTheme extends AbstractTheme { - - private final String name; - private final String stylesheet; - private final boolean darkMode; - - public ExternalTheme(String name, String stylesheet, Set stylesheets, boolean darkMode) { - super(stylesheets); - - this.name = Objects.requireNonNull(name); - this.stylesheet = Objects.requireNonNull(stylesheet); - this.darkMode = darkMode; - } - - @Override - public String getName() { - return name; - } - - @Override - public String getUserAgentStylesheet() { - return stylesheet; - } - - @Override - public boolean isDarkMode() { - return darkMode; - } -} diff --git a/sampler/src/main/java/atlantafx/sampler/theme/SamplerTheme.java b/sampler/src/main/java/atlantafx/sampler/theme/SamplerTheme.java new file mode 100644 index 0000000..6aff0b3 --- /dev/null +++ b/sampler/src/main/java/atlantafx/sampler/theme/SamplerTheme.java @@ -0,0 +1,189 @@ +/* SPDX-License-Identifier: MIT */ +package atlantafx.sampler.theme; + +import atlantafx.base.theme.Theme; +import atlantafx.sampler.FileResource; +import atlantafx.sampler.Launcher; +import atlantafx.sampler.Resources; +import fr.brouillard.oss.cssfx.CSSFX; +import javafx.application.Application; +import javafx.scene.Scene; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.file.Files; +import java.nio.file.attribute.FileTime; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static atlantafx.sampler.Launcher.IS_DEV_MODE; +import static atlantafx.sampler.Resources.resolve; +import static atlantafx.sampler.theme.ThemeManager.*; +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.nio.file.LinkOption.NOFOLLOW_LINKS; + +/** + * The {@link Theme} decorator to work around some JavaFX CSS limitations. + *

+ * JavaFX doesn't provide the theme API. Moreover, there is no such notion, it's called + * "user agent stylesheet". There are two ways to change the platform stylesheet: + *
    + *
  • {@link Scene#setUserAgentStylesheet(String)}
  • + *
  • {@link Application#setUserAgentStylesheet(String)}
  • + *
+ * The first one is observable and the latter is not. Both fail to provide hot reload with + * {@link CSSFX}. This means that if we'd set up some CSS file as the platform stylesheet + * and then change (re-compile) this file, nothing will happen. + *

+ * To work around this issue we use dev mode trick. In this mode, we set the dummy stylesheet + * to the Application, while real theme CSS is added to the {@link Scene#getStylesheets()} + * along with app own stylesheet. When CSSFX detects any of those CSS files changes, it forces + * observable list update and thus reloading CSS with changes. + *

+ * Also, note that some styles aren't applied when using {@link Application#setUserAgentStylesheet(String)}. + * E.g. JavaFX ignores Ikonli -fx-icon-color and -fx-icon-size properties, but for an unknown + * reason they won't be ignored when exactly the same stylesheet is set via {@link Scene#getStylesheets()}. + *

+ */ +public final class SamplerTheme implements Theme { + + private static final int PARSE_LIMIT = 250; + private static final Pattern COLOR_PATTERN = + Pattern.compile("\s*?(-color-(fg|bg|accent|success|danger)-.+?):\s*?(.+?);"); + + private final Theme theme; + + private FileTime lastModified; + private Map colors; + + public SamplerTheme(Theme theme) { + Objects.requireNonNull(theme); + + if (theme instanceof SamplerTheme) { + throw new IllegalArgumentException("Sampler theme must not be wrapped into itself."); + } + + this.theme = theme; + } + + @Override + public String getName() { + return theme.getName(); + } + + // Application.setUserAgentStylesheet() only accepts URL (or URL string representation), + // any external file path must have "file://" prefix + @Override + public String getUserAgentStylesheet() { + return IS_DEV_MODE ? DUMMY_STYLESHEET : getThemeFile().toURI().toString(); + } + + @Override + public boolean isDarkMode() { + return theme.isDarkMode(); + } + + public Set getAllStylesheets() { + return IS_DEV_MODE ? merge(getThemeFile().toURI().toString(), APP_STYLESHEETS) : Set.of(APP_STYLESHEETS); + } + + // Checks whether wrapped theme is a project theme or user external theme. + public boolean isProjectTheme() { + return PROJECT_THEMES.contains(theme.getClass()); + } + + // Tries to parse theme CSS and extract conventional looked-up colors. There are few limitations: + // - minified CSS files are not supported + // - only first PARSE_LIMIT lines will be read + public Map parseColors() throws IOException { + FileResource file = getThemeFile(); + return file.internal() ? parseColorsForClasspath(file) : parseColorsForFilesystem(file); + } + + private Map parseColorsForClasspath(FileResource file) throws IOException { + // classpath resources are static, no need to parse project theme more than once + if (colors != null) { return colors; } + + try (var br = new BufferedReader(new InputStreamReader(file.getInputStream()))) { + colors = parseColors(br); + } + + return colors; + } + + private Map parseColorsForFilesystem(FileResource file) throws IOException { + // return cached colors if file wasn't changed since the last read + FileTime fileTime = Files.getLastModifiedTime(file.toPath(), NOFOLLOW_LINKS); + if (Objects.equals(fileTime, lastModified)) { + return colors; + } + + try (var br = new BufferedReader(new InputStreamReader(file.getInputStream(), UTF_8))) { + colors = parseColors(br); + } + + // don't save time before parsing is finished to avoid + // remembering operation that might end up with an error + lastModified = fileTime; + + return colors; + } + + private Map parseColors(BufferedReader br) throws IOException { + Map colors = new HashMap<>(); + + String line; + int lineCount = 0; + + while ((line = br.readLine()) != null) { + Matcher matcher = COLOR_PATTERN.matcher(line); + if (matcher.matches()) { + colors.put(matcher.group(1), matcher.group(3)); + } + + lineCount++; + if (lineCount > PARSE_LIMIT) { break; } + } + + return colors; + } + + public String getPath() { + return getThemeFile().toPath().toString(); + } + + private FileResource getThemeFile() { + if (!isProjectTheme()) { + return FileResource.external(theme.getUserAgentStylesheet()); + } + + FileResource classpathTheme = FileResource.internal(theme.getUserAgentStylesheet(), Theme.class); + if (!IS_DEV_MODE) { return classpathTheme; } + + String filename = classpathTheme.getFilename(); + + try { + FileResource testTheme = FileResource.internal(Resources.resolve("theme-test/" + filename), Launcher.class); + if (!testTheme.exists()) { throw new IOException(); } + return testTheme; + } catch (Exception e) { + var failedPath = resolve("theme-test/" + filename); + System.err.println("[WARNING] Unable to find theme file \"" + failedPath + "\". Fall back to the classpath."); + return classpathTheme; + } + } + + public Theme unwrap() { + return theme; + } + + @SafeVarargs + private Set merge(T first, T... arr) { + var set = new LinkedHashSet(); + set.add(first); + Collections.addAll(set, arr); + return set; + } +} diff --git a/sampler/src/main/java/atlantafx/sampler/theme/ThemeManager.java b/sampler/src/main/java/atlantafx/sampler/theme/ThemeManager.java index 950a65b..8e46aaa 100644 --- a/sampler/src/main/java/atlantafx/sampler/theme/ThemeManager.java +++ b/sampler/src/main/java/atlantafx/sampler/theme/ThemeManager.java @@ -2,7 +2,6 @@ package atlantafx.sampler.theme; import atlantafx.base.theme.*; -import atlantafx.sampler.Launcher; import atlantafx.sampler.Resources; import atlantafx.sampler.event.DefaultEventBus; import atlantafx.sampler.event.EventBus; @@ -12,10 +11,8 @@ import atlantafx.sampler.util.JColor; import javafx.application.Application; import javafx.css.PseudoClass; import javafx.scene.Scene; -import javafx.scene.layout.Pane; import javafx.scene.paint.Color; -import java.net.URI; import java.util.*; import java.util.stream.Collectors; import java.util.stream.IntStream; @@ -25,7 +22,14 @@ import static java.nio.charset.StandardCharsets.UTF_8; public final class ThemeManager { - private static final String DUMMY_STYLESHEET = getResource("assets/styles/empty.css").toString(); + static final String DUMMY_STYLESHEET = getResource("assets/styles/empty.css").toString(); + static final String[] APP_STYLESHEETS = new String[] { + Resources.resolve("assets/styles/index.css") + }; + static final Set> PROJECT_THEMES = Set.of( + PrimerLight.class, PrimerDark.class, NordLight.class, NordDark.class + ); + private static final PseudoClass DARK = PseudoClass.getPseudoClass("dark"); private static final PseudoClass USER_CUSTOM = PseudoClass.getPseudoClass("user-custom"); private static final EventBus EVENT_BUS = DefaultEventBus.getInstance(); @@ -40,15 +44,20 @@ public final class ThemeManager { private final Map customCSSDeclarations = new LinkedHashMap<>(); // -fx-property | value; private final Map customCSSRules = new LinkedHashMap<>(); // .foo | -fx-property: value; - private Scene scene; - private Pane body; + private final ThemeRepository repository = new ThemeRepository(); - private Theme currentTheme = null; + private Scene scene; + + private SamplerTheme currentTheme = null; private String fontFamily = DEFAULT_FONT_FAMILY_NAME; private int fontSize = DEFAULT_FONT_SIZE; private int zoom = DEFAULT_ZOOM; private AccentColor accentColor = DEFAULT_ACCENT_COLOR; + public ThemeRepository getRepository() { + return repository; + } + public Scene getScene() { return scene; } @@ -59,46 +68,26 @@ public final class ThemeManager { this.scene = Objects.requireNonNull(scene); } - public Theme getTheme() { + public SamplerTheme getTheme() { return currentTheme; } - public List getAvailableThemes() { - var themes = new ArrayList(); - var appStylesheets = new URI[] { URI.create(Resources.resolve("assets/styles/index.css")) }; - - if (Launcher.IS_DEV_MODE) { - themes.add(new ExternalTheme("Primer Light", DUMMY_STYLESHEET, merge(getResource("theme-test/primer-light.css"), appStylesheets), false)); - themes.add(new ExternalTheme("Primer Dark", DUMMY_STYLESHEET, merge(getResource("theme-test/primer-dark.css"), appStylesheets), true)); - themes.add(new ExternalTheme("Nord Light", DUMMY_STYLESHEET, merge(getResource("theme-test/nord-light.css"), appStylesheets), false)); - themes.add(new ExternalTheme("Nord Dark", DUMMY_STYLESHEET, merge(getResource("theme-test/nord-dark.css"), appStylesheets), true)); - } else { - themes.add(new PrimerLight(appStylesheets)); - themes.add(new PrimerDark(appStylesheets)); - themes.add(new NordLight(appStylesheets)); - themes.add(new NordDark(appStylesheets)); - } - return themes; + public SamplerTheme getDefaultTheme() { + return getRepository().getAll().get(0); } - /** - * Resets user agent stylesheet and then adds {@link Theme} styles to the {@link Scene} - * stylesheets. This is necessary when we want to reload style changes at runtime, because - * CSSFX doesn't monitor user agent stylesheet. - * Also, some styles aren't applied when using {@link Application#setUserAgentStylesheet(String)} ). - * E.g. JavaFX ignores Ikonli -fx-icon-color and -fx-icon-size properties, but for an unknown - * reason they won't be ignored when exactly the same stylesheet is set via {@link Scene#getStylesheets()}. - */ - public void setTheme(Theme theme) { + /** @see SamplerTheme */ + public void setTheme(SamplerTheme theme) { Objects.requireNonNull(theme); Application.setUserAgentStylesheet(Objects.requireNonNull(theme.getUserAgentStylesheet())); if (currentTheme != null) { - getScene().getStylesheets().removeIf(url -> currentTheme.getStylesheets().contains(URI.create(url))); + getScene().getStylesheets().removeIf(s -> theme.getAllStylesheets().contains(s)); } - theme.getStylesheets().forEach(uri -> getScene().getStylesheets().add(uri.toString())); + theme.getAllStylesheets().forEach(s -> getScene().getStylesheets().add(s)); + getScene().getRoot().pseudoClassStateChanged(DARK, theme.isDarkMode()); // remove user CSS customizations and reset accent on theme change @@ -133,11 +122,12 @@ public final class ThemeManager { public void setFontSize(int size) { if (!SUPPORTED_FONT_SIZE.contains(size)) { - throw new IllegalArgumentException(String.format("Font size must in the range %d-%dpx. Actual value is %d.", - SUPPORTED_FONT_SIZE.get(0), - SUPPORTED_FONT_SIZE.get(SUPPORTED_FONT_SIZE.size() - 1), - size - )); + throw new IllegalArgumentException( + String.format("Font size must in the range %d-%dpx. Actual value is %d.", + SUPPORTED_FONT_SIZE.get(0), + SUPPORTED_FONT_SIZE.get(SUPPORTED_FONT_SIZE.size() - 1), + size + )); } setCustomDeclaration("-fx-font-size", size + "px"); @@ -164,10 +154,9 @@ public final class ThemeManager { public void setZoom(int zoom) { if (!SUPPORTED_ZOOM.contains(zoom)) { - throw new IllegalArgumentException(String.format("Zoom value must one of %s. Actual value is %d.", - SUPPORTED_ZOOM, - zoom - )); + throw new IllegalArgumentException( + String.format("Zoom value must one of %s. Actual value is %d.", SUPPORTED_ZOOM, zoom) + ); } setFontSize((int) Math.ceil(zoom != 100 ? (DEFAULT_FONT_SIZE * zoom) / 100.0f : DEFAULT_FONT_SIZE)); @@ -299,14 +288,6 @@ public final class ThemeManager { getScene().getRoot().pseudoClassStateChanged(USER_CUSTOM, false); } - @SafeVarargs - private Set merge(T first, T... arr) { - var set = new LinkedHashSet(); - set.add(first); - Collections.addAll(set, arr); - return set; - } - /////////////////////////////////////////////////////////////////////////// // Singleton // /////////////////////////////////////////////////////////////////////////// diff --git a/sampler/src/main/java/atlantafx/sampler/theme/ThemeRepository.java b/sampler/src/main/java/atlantafx/sampler/theme/ThemeRepository.java new file mode 100644 index 0000000..9747a47 --- /dev/null +++ b/sampler/src/main/java/atlantafx/sampler/theme/ThemeRepository.java @@ -0,0 +1,129 @@ +/* SPDX-License-Identifier: MIT */ +package atlantafx.sampler.theme; + +import atlantafx.base.theme.*; +import atlantafx.sampler.Resources; +import atlantafx.sampler.event.DefaultEventBus; +import atlantafx.sampler.event.ThemeEvent; +import atlantafx.sampler.event.ThemeEvent.EventType; + +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.*; +import java.util.prefs.BackingStoreException; +import java.util.prefs.Preferences; +import java.util.stream.Collectors; + +import static java.nio.file.LinkOption.NOFOLLOW_LINKS; + +public final class ThemeRepository { + + private static final Comparator THEME_COMPARATOR = Comparator.comparing(SamplerTheme::getName); + + private final List internalThemes = Arrays.asList( + new SamplerTheme(new PrimerLight()), + new SamplerTheme(new PrimerDark()), + new SamplerTheme(new NordLight()), + new SamplerTheme(new NordDark()) + ); + + private final List externalThemes = new ArrayList<>(); + private final Preferences themePreferences = Resources.getPreferences().node("theme"); + + public ThemeRepository() { + try { + loadPreferences(); + } catch (BackingStoreException e) { + System.out.println("[WARNING] Unable to load themes from the preferences."); + e.printStackTrace(); + } + } + + public List getAll() { + var list = new ArrayList<>(internalThemes); + list.addAll(externalThemes); + return list; + } + + public SamplerTheme addFromFile(File file) { + Objects.requireNonNull(file); + + if (!isFileValid(file.toPath())) { + throw new RuntimeException("Invalid CSS file \"" + file.getAbsolutePath() + "\"."); + } + + // creating GUI dialogs is hard, so we just obtain theme name from the file name :) + String filename = file.getName(); + String themeName = Arrays.stream(filename.replace(".css", "").split("[-_]")) + .map(s -> !s.isEmpty() ? s.substring(0, 1).toUpperCase() + s.substring(1) : "") + .collect(Collectors.joining(" ")); + + var theme = new SamplerTheme(Theme.of(themeName, file.toString(), filename.contains("dark"))); + + if (!isUnique(theme)) { + throw new RuntimeException("A theme with the same name or user agent stylesheet already exists in the repository."); + } + + addToPreferences(theme); + externalThemes.add(theme); + externalThemes.sort(THEME_COMPARATOR); + DefaultEventBus.getInstance().publish(new ThemeEvent(EventType.THEME_ADD)); + + return theme; + } + + public void remove(SamplerTheme theme) { + Objects.requireNonNull(theme); + externalThemes.removeIf(t -> Objects.equals(t.getName(), theme.getName())); + DefaultEventBus.getInstance().publish(new ThemeEvent(EventType.THEME_REMOVE)); + removeFromPreferences(theme); + } + + public boolean isFileValid(Path path) { + Objects.requireNonNull(path); + return !Files.isDirectory(path, NOFOLLOW_LINKS) && + Files.isRegularFile(path, NOFOLLOW_LINKS) && + Files.isReadable(path) && + path.getFileName().toString().endsWith(".css"); + } + + public boolean isUnique(SamplerTheme theme) { + Objects.requireNonNull(theme); + for (SamplerTheme t : getAll()) { + if (Objects.equals(t.getName(), theme.getName()) || Objects.equals(t.getPath(), theme.getPath())) { + return false; + } + } + return true; + } + + private void loadPreferences() throws BackingStoreException { + for (String themeName : themePreferences.keys()) { + var uaStylesheet = themePreferences.get(themeName, ""); + var uaStylesheetPath = Paths.get(uaStylesheet); + + // cleanup broken links, e.g. if theme was added for testing + // but then CSS file was removed from the filesystem + if (!isFileValid(uaStylesheetPath)) { + System.err.println("[WARNING] CSS file invalid or missing: \"" + uaStylesheetPath + "\". Removing silently."); + themePreferences.remove(themeName); + continue; + } + + externalThemes.add(new SamplerTheme( + Theme.of(themeName, uaStylesheet, uaStylesheetPath.getFileName().toString().contains("dark")) + )); + externalThemes.sort(THEME_COMPARATOR); + } + } + + private void addToPreferences(SamplerTheme theme) { + themePreferences.put(theme.getName(), theme.getPath()); + } + + private void removeFromPreferences(SamplerTheme theme) { + themePreferences.remove(theme.getName()); + } +} diff --git a/sampler/src/main/java/module-info.java b/sampler/src/main/java/module-info.java index 8021c15..960fa37 100755 --- a/sampler/src/main/java/module-info.java +++ b/sampler/src/main/java/module-info.java @@ -5,6 +5,7 @@ module atlantafx.sampler { requires atlantafx.base; requires java.desktop; + requires java.prefs; requires javafx.swing; requires javafx.media; requires javafx.web; diff --git a/sampler/src/main/resources/assets/styles/scss/layout/_overlay.scss b/sampler/src/main/resources/assets/styles/scss/layout/_overlay.scss index 3c2a667..b6b2006 100644 --- a/sampler/src/main/resources/assets/styles/scss/layout/_overlay.scss +++ b/sampler/src/main/resources/assets/styles/scss/layout/_overlay.scss @@ -28,6 +28,6 @@ >.footer { -fx-border-width: 1 0 0 0; -fx-border-color: -color-border-default; - -fx-padding: 10; + -fx-padding: 10px 20px 10px 20px; } } \ 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 441c016..91825f2 100644 --- a/sampler/src/main/resources/assets/styles/scss/widgets/_index.scss +++ b/sampler/src/main/resources/assets/styles/scss/widgets/_index.scss @@ -4,4 +4,5 @@ @use "color-scale"; @use "contrast-checker"; @use "accent-color-selector"; -@use "quick-config-menu"; \ No newline at end of file +@use "quick-config-menu"; +@use "theme-repo-manager"; diff --git a/sampler/src/main/resources/assets/styles/scss/widgets/_theme-repo-manager.scss b/sampler/src/main/resources/assets/styles/scss/widgets/_theme-repo-manager.scss new file mode 100644 index 0000000..92ce245 --- /dev/null +++ b/sampler/src/main/resources/assets/styles/scss/widgets/_theme-repo-manager.scss @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: MIT +@use "color-palette" as palette; + +#theme-repo-manager { + -fx-padding: 10px 20px 10px 20px; + -fx-spacing: 10px; + + >.info { + -fx-background-color: -color-accent-subtle; + -fx-padding: 0.5em; + -fx-border-width: 1px; + -fx-border-color: -color-accent-muted; + + >.label { + -fx-text-fill: -color-accent-fg; + } + } + + .scroll-pane { + -fx-border-width: 1px; + -fx-border-color: -color-border-muted; + } + + .theme-list { + -fx-padding: 10px; + -fx-spacing: 5px; + + >.theme { + -fx-spacing: 10px; + -fx-min-height: 3em; + -fx-padding: 10px; + + &:hover { + -fx-background-color: -color-bg-subtle; + } + + &:selected { + >.title { + >.text { + -fx-fill: -color-accent-fg; + } + } + } + + >.title { + >.sub-text { + -fx-fill: -color-fg-muted; + } + } + + >.preview { + >.label { + -fx-alignment: CENTER; + -fx-font-size: 1.2em; + -fx-min-width: 2em; + -fx-pref-width: 2em; + -fx-max-width: 2em; + -fx-min-height: 2em; + -fx-pref-height: 2em; + -fx-max-height: 2em; + } + } + + >.controls { + -fx-min-width: 4em; + -fx-pref-width: 4em; + -fx-max-width: 4em; + } + } + } +} + +#theme-repo-manager-dialog { + -fx-min-width: 800px; + -fx-pref-width: 800px; + -fx-min-height: 500px; + -fx-max-height: 500px; +}