From 9fe8cbde8a5a4ca23e0d9fcefc059ec95b6e514f Mon Sep 17 00:00:00 2001
From: mkpaz
Date: Wed, 14 Sep 2022 21:26:58 +0400
Subject: [PATCH] Add external themes support
---
CHANGELOG.md | 10 +-
.../base/controls/InlineDatePicker.java | 4 +-
.../atlantafx/base/theme/AbstractTheme.java | 40 ---
.../java/atlantafx/base/theme/NordDark.java | 15 +-
.../java/atlantafx/base/theme/NordLight.java | 15 +-
.../java/atlantafx/base/theme/PrimerDark.java | 15 +-
.../atlantafx/base/theme/PrimerLight.java | 15 +-
.../main/java/atlantafx/base/theme/Theme.java | 28 ++-
sampler/pom.xml | 2 +-
.../java/atlantafx/sampler/FileResource.java | 72 ++++++
.../main/java/atlantafx/sampler/Launcher.java | 11 +-
.../java/atlantafx/sampler/Resources.java | 5 +
.../atlantafx/sampler/event/ThemeEvent.java | 5 +-
.../sampler/page/QuickConfigMenu.java | 2 +-
.../sampler/page/general/ThemePage.java | 86 +++++--
.../page/general/ThemeRepoManager.java | 227 ++++++++++++++++++
.../page/general/ThemeRepoManagerDialog.java | 40 +++
.../sampler/theme/ExternalTheme.java | 38 ---
.../atlantafx/sampler/theme/SamplerTheme.java | 189 +++++++++++++++
.../atlantafx/sampler/theme/ThemeManager.java | 85 +++----
.../sampler/theme/ThemeRepository.java | 129 ++++++++++
sampler/src/main/java/module-info.java | 1 +
.../assets/styles/scss/layout/_overlay.scss | 2 +-
.../assets/styles/scss/widgets/_index.scss | 3 +-
.../scss/widgets/_theme-repo-manager.scss | 78 ++++++
25 files changed, 893 insertions(+), 224 deletions(-)
delete mode 100644 base/src/main/java/atlantafx/base/theme/AbstractTheme.java
create mode 100644 sampler/src/main/java/atlantafx/sampler/FileResource.java
create mode 100644 sampler/src/main/java/atlantafx/sampler/page/general/ThemeRepoManager.java
create mode 100644 sampler/src/main/java/atlantafx/sampler/page/general/ThemeRepoManagerDialog.java
delete mode 100644 sampler/src/main/java/atlantafx/sampler/theme/ExternalTheme.java
create mode 100644 sampler/src/main/java/atlantafx/sampler/theme/SamplerTheme.java
create mode 100644 sampler/src/main/java/atlantafx/sampler/theme/ThemeRepository.java
create mode 100644 sampler/src/main/resources/assets/styles/scss/widgets/_theme-repo-manager.scss
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}
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
+ */
+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;
+}