Add external themes support
This commit is contained in:
parent
a65ea977c0
commit
9fe8cbde8a
10
CHANGELOG.md
10
CHANGELOG.md
@ -2,9 +2,17 @@
|
|||||||
|
|
||||||
## [Unreleased]
|
## [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
|
### 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]
|
## [0.1]
|
||||||
|
|
||||||
|
@ -80,7 +80,7 @@ public class InlineDatePicker extends Control {
|
|||||||
if (isValidDate(chrono, date)) {
|
if (isValidDate(chrono, date)) {
|
||||||
lastValidDate = date;
|
lastValidDate = date;
|
||||||
} else {
|
} else {
|
||||||
System.err.println("Restoring value to " + (lastValidDate == null ? "null" : lastValidDate));
|
System.err.println("[ERROR] Restoring value to " + (lastValidDate == null ? "null" : lastValidDate));
|
||||||
setValue(lastValidDate);
|
setValue(lastValidDate);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -92,7 +92,7 @@ public class InlineDatePicker extends Control {
|
|||||||
if (isValidDate(chrono, date)) {
|
if (isValidDate(chrono, date)) {
|
||||||
lastValidChronology = chrono;
|
lastValidChronology = chrono;
|
||||||
} else {
|
} else {
|
||||||
System.err.println("Restoring value to " + lastValidChronology);
|
System.err.println("[ERROR] Restoring value to " + lastValidChronology);
|
||||||
setChronology(lastValidChronology);
|
setChronology(lastValidChronology);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -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<URI> stylesheets;
|
|
||||||
|
|
||||||
public AbstractTheme() {
|
|
||||||
this(new LinkedHashSet<>());
|
|
||||||
}
|
|
||||||
|
|
||||||
public AbstractTheme(URI... stylesheets) {
|
|
||||||
this(Set.of(stylesheets));
|
|
||||||
}
|
|
||||||
|
|
||||||
public AbstractTheme(Set<URI> stylesheets) {
|
|
||||||
this.stylesheets = Objects.requireNonNull(stylesheets);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Set<URI> getStylesheets() {
|
|
||||||
return stylesheets;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return getClass().getSimpleName() +
|
|
||||||
"{" +
|
|
||||||
"name=" + getName() +
|
|
||||||
", userAgentStylesheet=" + getUserAgentStylesheet() +
|
|
||||||
", stylesheets=" + stylesheets +
|
|
||||||
", isDarkMode=" + isDarkMode() +
|
|
||||||
'}';
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,21 +1,10 @@
|
|||||||
/* SPDX-License-Identifier: MIT */
|
/* SPDX-License-Identifier: MIT */
|
||||||
package atlantafx.base.theme;
|
package atlantafx.base.theme;
|
||||||
|
|
||||||
import java.net.URI;
|
public final class NordDark implements Theme {
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
public class NordDark extends AbstractTheme {
|
|
||||||
|
|
||||||
public NordDark() { }
|
public NordDark() { }
|
||||||
|
|
||||||
public NordDark(URI... stylesheets) {
|
|
||||||
super(stylesheets);
|
|
||||||
}
|
|
||||||
|
|
||||||
public NordDark(Set<URI> stylesheets) {
|
|
||||||
super(stylesheets);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getName() {
|
public String getName() {
|
||||||
return "Nord Dark";
|
return "Nord Dark";
|
||||||
|
@ -1,21 +1,10 @@
|
|||||||
/* SPDX-License-Identifier: MIT */
|
/* SPDX-License-Identifier: MIT */
|
||||||
package atlantafx.base.theme;
|
package atlantafx.base.theme;
|
||||||
|
|
||||||
import java.net.URI;
|
public final class NordLight implements Theme {
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
public class NordLight extends AbstractTheme {
|
|
||||||
|
|
||||||
public NordLight() { }
|
public NordLight() { }
|
||||||
|
|
||||||
public NordLight(URI... stylesheets) {
|
|
||||||
super(stylesheets);
|
|
||||||
}
|
|
||||||
|
|
||||||
public NordLight(Set<URI> stylesheets) {
|
|
||||||
super(stylesheets);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getName() {
|
public String getName() {
|
||||||
return "Nord Light";
|
return "Nord Light";
|
||||||
|
@ -1,21 +1,10 @@
|
|||||||
/* SPDX-License-Identifier: MIT */
|
/* SPDX-License-Identifier: MIT */
|
||||||
package atlantafx.base.theme;
|
package atlantafx.base.theme;
|
||||||
|
|
||||||
import java.net.URI;
|
public final class PrimerDark implements Theme {
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
public class PrimerDark extends AbstractTheme {
|
|
||||||
|
|
||||||
public PrimerDark() { }
|
public PrimerDark() { }
|
||||||
|
|
||||||
public PrimerDark(URI... stylesheets) {
|
|
||||||
super(stylesheets);
|
|
||||||
}
|
|
||||||
|
|
||||||
public PrimerDark(Set<URI> stylesheets) {
|
|
||||||
super(stylesheets);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getName() {
|
public String getName() {
|
||||||
return "Primer Dark";
|
return "Primer Dark";
|
||||||
|
@ -1,21 +1,10 @@
|
|||||||
/* SPDX-License-Identifier: MIT */
|
/* SPDX-License-Identifier: MIT */
|
||||||
package atlantafx.base.theme;
|
package atlantafx.base.theme;
|
||||||
|
|
||||||
import java.net.URI;
|
public final class PrimerLight implements Theme {
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
public class PrimerLight extends AbstractTheme {
|
|
||||||
|
|
||||||
public PrimerLight() { }
|
public PrimerLight() { }
|
||||||
|
|
||||||
public PrimerLight(URI... stylesheets) {
|
|
||||||
super(stylesheets);
|
|
||||||
}
|
|
||||||
|
|
||||||
public PrimerLight(Set<URI> stylesheets) {
|
|
||||||
super(stylesheets);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getName() {
|
public String getName() {
|
||||||
return "Primer Light";
|
return "Primer Light";
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
/* SPDX-License-Identifier: MIT */
|
/* SPDX-License-Identifier: MIT */
|
||||||
package atlantafx.base.theme;
|
package atlantafx.base.theme;
|
||||||
|
|
||||||
import java.net.URI;
|
import java.util.Objects;
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
import static javafx.application.Application.STYLESHEET_CASPIAN;
|
import static javafx.application.Application.STYLESHEET_CASPIAN;
|
||||||
import static javafx.application.Application.STYLESHEET_MODENA;
|
import static javafx.application.Application.STYLESHEET_MODENA;
|
||||||
@ -16,10 +15,31 @@ public interface Theme {
|
|||||||
|
|
||||||
String getUserAgentStylesheet();
|
String getUserAgentStylesheet();
|
||||||
|
|
||||||
Set<URI> getStylesheets();
|
|
||||||
|
|
||||||
boolean isDarkMode();
|
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() {
|
default boolean isDefault() {
|
||||||
return STYLESHEET_MODENA.equals(getUserAgentStylesheet()) || STYLESHEET_CASPIAN.equals(getUserAgentStylesheet());
|
return STYLESHEET_MODENA.equals(getUserAgentStylesheet()) || STYLESHEET_CASPIAN.equals(getUserAgentStylesheet());
|
||||||
}
|
}
|
||||||
|
@ -223,7 +223,7 @@
|
|||||||
</goals>
|
</goals>
|
||||||
<configuration>
|
<configuration>
|
||||||
<toolName>jlink</toolName>
|
<toolName>jlink</toolName>
|
||||||
<addModules>java.base,java.logging,jdk.localedata,java.desktop,javafx.controls,javafx.swing,javafx.web</addModules>
|
<addModules>java.base,java.logging,jdk.localedata,java.desktop,java.prefs,javafx.controls,javafx.swing,javafx.web</addModules>
|
||||||
<modulePath>${build.platformModulesDir}</modulePath>
|
<modulePath>${build.platformModulesDir}</modulePath>
|
||||||
<output>${build.package.runtimeImageDir}</output>
|
<output>${build.package.runtimeImageDir}</output>
|
||||||
<args>
|
<args>
|
||||||
|
72
sampler/src/main/java/atlantafx/sampler/FileResource.java
Normal file
72
sampler/src/main/java/atlantafx/sampler/FileResource.java
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -9,6 +9,7 @@ import atlantafx.sampler.theme.ThemeManager;
|
|||||||
import fr.brouillard.oss.cssfx.CSSFX;
|
import fr.brouillard.oss.cssfx.CSSFX;
|
||||||
import fr.brouillard.oss.cssfx.api.URIToPathConverter;
|
import fr.brouillard.oss.cssfx.api.URIToPathConverter;
|
||||||
import fr.brouillard.oss.cssfx.impl.log.CSSFXLogger;
|
import fr.brouillard.oss.cssfx.impl.log.CSSFXLogger;
|
||||||
|
import fr.brouillard.oss.cssfx.impl.log.CSSFXLogger.LogLevel;
|
||||||
import javafx.application.Application;
|
import javafx.application.Application;
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
import javafx.scene.Scene;
|
import javafx.scene.Scene;
|
||||||
@ -55,7 +56,7 @@ public class Launcher extends Application {
|
|||||||
|
|
||||||
var tm = ThemeManager.getInstance();
|
var tm = ThemeManager.getInstance();
|
||||||
tm.setScene(scene);
|
tm.setScene(scene);
|
||||||
tm.setTheme(tm.getAvailableThemes().get(0));
|
tm.setTheme(tm.getDefaultTheme());
|
||||||
if (IS_DEV_MODE) { startCssFX(scene); }
|
if (IS_DEV_MODE) { startCssFX(scene); }
|
||||||
|
|
||||||
scene.getStylesheets().addAll(Resources.resolve("assets/styles/index.css"));
|
scene.getStylesheets().addAll(Resources.resolve("assets/styles/index.css"));
|
||||||
@ -110,9 +111,11 @@ public class Launcher extends Application {
|
|||||||
};
|
};
|
||||||
|
|
||||||
CSSFX.addConverter(fileUrlConverter).start();
|
CSSFX.addConverter(fileUrlConverter).start();
|
||||||
CSSFXLogger.setLoggerFactory(loggerName -> (level, message, args) ->
|
CSSFXLogger.setLoggerFactory(loggerName -> (level, message, args) -> {
|
||||||
System.out.println("[CSSFX] " + String.format(message, args))
|
if (level.ordinal() <= LogLevel.INFO.ordinal()) {
|
||||||
);
|
System.out.println("[" + level + "] CSSFX: " + String.format(message, args));
|
||||||
|
}
|
||||||
|
});
|
||||||
CSSFX.start(scene);
|
CSSFX.start(scene);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@ import java.io.InputStream;
|
|||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
import java.util.prefs.Preferences;
|
||||||
|
|
||||||
public final class Resources {
|
public final class Resources {
|
||||||
|
|
||||||
@ -32,4 +33,8 @@ public final class Resources {
|
|||||||
public static String getPropertyOrEnv(String propertyKey, String envKey) {
|
public static String getPropertyOrEnv(String propertyKey, String envKey) {
|
||||||
return System.getProperty(propertyKey, System.getenv(envKey));
|
return System.getProperty(propertyKey, System.getenv(envKey));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Preferences getPreferences() {
|
||||||
|
return Preferences.userRoot().node("atlantafx");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,6 +18,9 @@ public class ThemeEvent extends Event {
|
|||||||
// font size or family only change
|
// font size or family only change
|
||||||
FONT_CHANGE,
|
FONT_CHANGE,
|
||||||
// colors only change
|
// colors only change
|
||||||
COLOR_CHANGE
|
COLOR_CHANGE,
|
||||||
|
// new theme added or removed
|
||||||
|
THEME_ADD,
|
||||||
|
THEME_REMOVE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -211,7 +211,7 @@ public class QuickConfigMenu extends StackPane {
|
|||||||
|
|
||||||
getChildren().setAll(mainMenu, new Separator());
|
getChildren().setAll(mainMenu, new Separator());
|
||||||
|
|
||||||
tm.getAvailableThemes().forEach(theme -> {
|
tm.getRepository().getAll().forEach(theme -> {
|
||||||
var icon = new FontIcon(Material2AL.CHECK);
|
var icon = new FontIcon(Material2AL.CHECK);
|
||||||
|
|
||||||
var item = new HBox(20, icon, new Label(theme.getName()));
|
var item = new HBox(20, icon, new Label(theme.getName()));
|
||||||
|
@ -1,36 +1,42 @@
|
|||||||
/* SPDX-License-Identifier: MIT */
|
/* SPDX-License-Identifier: MIT */
|
||||||
package atlantafx.sampler.page.general;
|
package atlantafx.sampler.page.general;
|
||||||
|
|
||||||
import atlantafx.base.theme.Theme;
|
import atlantafx.base.theme.Styles;
|
||||||
import atlantafx.sampler.event.DefaultEventBus;
|
import atlantafx.sampler.event.DefaultEventBus;
|
||||||
import atlantafx.sampler.event.ThemeEvent;
|
import atlantafx.sampler.event.ThemeEvent;
|
||||||
import atlantafx.sampler.page.AbstractPage;
|
import atlantafx.sampler.page.AbstractPage;
|
||||||
|
import atlantafx.sampler.theme.SamplerTheme;
|
||||||
import atlantafx.sampler.theme.ThemeManager;
|
import atlantafx.sampler.theme.ThemeManager;
|
||||||
import atlantafx.sampler.util.NodeUtils;
|
import atlantafx.sampler.util.NodeUtils;
|
||||||
import javafx.geometry.HPos;
|
import javafx.geometry.HPos;
|
||||||
|
import javafx.scene.control.Button;
|
||||||
import javafx.scene.control.ChoiceBox;
|
import javafx.scene.control.ChoiceBox;
|
||||||
import javafx.scene.control.Label;
|
import javafx.scene.control.Label;
|
||||||
|
import javafx.scene.control.Tooltip;
|
||||||
import javafx.scene.layout.GridPane;
|
import javafx.scene.layout.GridPane;
|
||||||
import javafx.scene.text.Text;
|
import javafx.scene.text.Text;
|
||||||
import javafx.scene.text.TextFlow;
|
import javafx.scene.text.TextFlow;
|
||||||
import javafx.util.Duration;
|
import javafx.util.Duration;
|
||||||
import javafx.util.StringConverter;
|
import javafx.util.StringConverter;
|
||||||
|
import org.kordamp.ikonli.javafx.FontIcon;
|
||||||
|
import org.kordamp.ikonli.material2.Material2OutlinedMZ;
|
||||||
|
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
import static atlantafx.sampler.event.ThemeEvent.EventType.COLOR_CHANGE;
|
import static atlantafx.sampler.event.ThemeEvent.EventType.*;
|
||||||
import static atlantafx.sampler.event.ThemeEvent.EventType.THEME_CHANGE;
|
|
||||||
import static atlantafx.sampler.util.Controls.hyperlink;
|
import static atlantafx.sampler.util.Controls.hyperlink;
|
||||||
|
|
||||||
public class ThemePage extends AbstractPage {
|
public class ThemePage extends AbstractPage {
|
||||||
|
|
||||||
public static final String NAME = "Theme";
|
public static final String NAME = "Theme";
|
||||||
|
private static final ThemeManager TM = ThemeManager.getInstance();
|
||||||
|
|
||||||
private final Consumer<ColorPaletteBlock> colorBlockActionHandler = colorBlock -> {
|
private final Consumer<ColorPaletteBlock> colorBlockActionHandler = colorBlock -> {
|
||||||
ContrastCheckerDialog dialog = getOrCreateContrastCheckerDialog();
|
ContrastCheckerDialog dialog = getOrCreateContrastCheckerDialog();
|
||||||
dialog.getContent().setValues(colorBlock.getFgColorName(),
|
dialog.getContent().setValues(
|
||||||
|
colorBlock.getFgColorName(),
|
||||||
colorBlock.getFgColor(),
|
colorBlock.getFgColor(),
|
||||||
colorBlock.getBgColorName(),
|
colorBlock.getBgColorName(),
|
||||||
colorBlock.getBgColor()
|
colorBlock.getBgColor()
|
||||||
@ -41,7 +47,9 @@ public class ThemePage extends AbstractPage {
|
|||||||
|
|
||||||
private final ColorPalette colorPalette = new ColorPalette(colorBlockActionHandler);
|
private final ColorPalette colorPalette = new ColorPalette(colorBlockActionHandler);
|
||||||
private final ColorScale colorScale = new ColorScale();
|
private final ColorScale colorScale = new ColorScale();
|
||||||
|
private final ChoiceBox<SamplerTheme> themeSelector = themeSelector();
|
||||||
|
|
||||||
|
private ThemeRepoManagerDialog themeRepoManagerDialog;
|
||||||
private ContrastCheckerDialog contrastCheckerDialog;
|
private ContrastCheckerDialog contrastCheckerDialog;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -51,6 +59,10 @@ public class ThemePage extends AbstractPage {
|
|||||||
super();
|
super();
|
||||||
createView();
|
createView();
|
||||||
DefaultEventBus.getInstance().subscribe(ThemeEvent.class, e -> {
|
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) {
|
if (e.getEventType() == THEME_CHANGE || e.getEventType() == COLOR_CHANGE) {
|
||||||
colorPalette.updateColorInfo(Duration.seconds(1));
|
colorPalette.updateColorInfo(Duration.seconds(1));
|
||||||
colorScale.updateColorInfo(Duration.seconds(1));
|
colorScale.updateColorInfo(Duration.seconds(1));
|
||||||
@ -81,6 +93,8 @@ public class ThemePage extends AbstractPage {
|
|||||||
colorScale
|
colorScale
|
||||||
);
|
);
|
||||||
|
|
||||||
|
selectCurrentTheme();
|
||||||
|
|
||||||
// if you want to enable quick menu don't forget that
|
// if you want to enable quick menu don't forget that
|
||||||
// theme selection choice box have to be updated accordingly
|
// theme selection choice box have to be updated accordingly
|
||||||
NodeUtils.toggleVisibility(quickConfigBtn, false);
|
NodeUtils.toggleVisibility(quickConfigBtn, false);
|
||||||
@ -88,8 +102,17 @@ public class ThemePage extends AbstractPage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private GridPane optionsGrid() {
|
private GridPane optionsGrid() {
|
||||||
ChoiceBox<Theme> themeSelector = themeSelector();
|
var themeRepoBtn = new Button("", new FontIcon(Material2OutlinedMZ.SETTINGS));
|
||||||
themeSelector.setPrefWidth(200);
|
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();
|
var accentSelector = new AccentColorSelector();
|
||||||
|
|
||||||
@ -101,47 +124,58 @@ public class ThemePage extends AbstractPage {
|
|||||||
|
|
||||||
grid.add(new Label("Color theme"), 0, 0);
|
grid.add(new Label("Color theme"), 0, 0);
|
||||||
grid.add(themeSelector, 1, 0);
|
grid.add(themeSelector, 1, 0);
|
||||||
|
grid.add(themeRepoBtn, 2, 0);
|
||||||
grid.add(new Label("Accent color"), 0, 1);
|
grid.add(new Label("Accent color"), 0, 1);
|
||||||
grid.add(accentSelector, 1, 1);
|
grid.add(accentSelector, 1, 1);
|
||||||
|
|
||||||
return grid;
|
return grid;
|
||||||
}
|
}
|
||||||
|
|
||||||
private ChoiceBox<Theme> themeSelector() {
|
private ChoiceBox<SamplerTheme> themeSelector() {
|
||||||
var manager = ThemeManager.getInstance();
|
var selector = new ChoiceBox<SamplerTheme>();
|
||||||
var selector = new ChoiceBox<Theme>();
|
selector.getItems().setAll(TM.getRepository().getAll());
|
||||||
selector.getItems().setAll(manager.getAvailableThemes());
|
|
||||||
selector.getSelectionModel().selectedItemProperty().addListener((obs, old, val) -> {
|
selector.getSelectionModel().selectedItemProperty().addListener((obs, old, val) -> {
|
||||||
if (val != null && getScene() != null) {
|
if (val != null && getScene() != null) { TM.setTheme(val); }
|
||||||
var tm = ThemeManager.getInstance();
|
|
||||||
tm.setTheme(val);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
selector.setPrefWidth(250);
|
||||||
|
|
||||||
selector.setConverter(new StringConverter<>() {
|
selector.setConverter(new StringConverter<>() {
|
||||||
@Override
|
@Override
|
||||||
public String toString(Theme theme) {
|
public String toString(SamplerTheme theme) {
|
||||||
return theme.getName();
|
return theme != null ? theme.getName() : "";
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Theme fromString(String themeName) {
|
public SamplerTheme fromString(String themeName) {
|
||||||
return manager.getAvailableThemes().stream()
|
return TM.getRepository().getAll().stream()
|
||||||
.filter(t -> Objects.equals(themeName, t.getName()))
|
.filter(t -> Objects.equals(themeName, t.getName()))
|
||||||
.findFirst()
|
.findFirst()
|
||||||
.orElse(null);
|
.orElse(null);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// select current theme
|
return selector;
|
||||||
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
themeRepoManagerDialog.setOnCloseRequest(() -> {
|
||||||
|
overlay.removeContent();
|
||||||
|
overlay.toBack();
|
||||||
|
});
|
||||||
|
|
||||||
|
return themeRepoManagerDialog;
|
||||||
}
|
}
|
||||||
|
|
||||||
private ContrastCheckerDialog getOrCreateContrastCheckerDialog() {
|
private ContrastCheckerDialog getOrCreateContrastCheckerDialog() {
|
||||||
|
@ -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<SamplerTheme> 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<SamplerTheme> 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<Map<String, String>>() {
|
||||||
|
@Override
|
||||||
|
protected Map<String, String> 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<SamplerTheme> handler) {
|
||||||
|
this.deleteHandler = handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isSelectedTheme() {
|
||||||
|
return Objects.equals(ThemeManager.getInstance().getTheme().getName(), theme.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
private int appendStyleIfPresent(Map<String, String> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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<ThemeRepoManager> {
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
@ -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<URI> 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;
|
|
||||||
}
|
|
||||||
}
|
|
189
sampler/src/main/java/atlantafx/sampler/theme/SamplerTheme.java
Normal file
189
sampler/src/main/java/atlantafx/sampler/theme/SamplerTheme.java
Normal file
@ -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.
|
||||||
|
* <br/><br/>
|
||||||
|
* 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:
|
||||||
|
* <ul>
|
||||||
|
* <li>{@link Scene#setUserAgentStylesheet(String)}</li>
|
||||||
|
* <li>{@link Application#setUserAgentStylesheet(String)}</li>
|
||||||
|
* </ul>
|
||||||
|
* 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.
|
||||||
|
* <br/><br/>
|
||||||
|
* 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.
|
||||||
|
* <br/><br/>
|
||||||
|
* 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()}.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
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<String, String> 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<String> 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<String, String> parseColors() throws IOException {
|
||||||
|
FileResource file = getThemeFile();
|
||||||
|
return file.internal() ? parseColorsForClasspath(file) : parseColorsForFilesystem(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, String> 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<String, String> 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<String, String> parseColors(BufferedReader br) throws IOException {
|
||||||
|
Map<String, String> 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 <T> Set<T> merge(T first, T... arr) {
|
||||||
|
var set = new LinkedHashSet<T>();
|
||||||
|
set.add(first);
|
||||||
|
Collections.addAll(set, arr);
|
||||||
|
return set;
|
||||||
|
}
|
||||||
|
}
|
@ -2,7 +2,6 @@
|
|||||||
package atlantafx.sampler.theme;
|
package atlantafx.sampler.theme;
|
||||||
|
|
||||||
import atlantafx.base.theme.*;
|
import atlantafx.base.theme.*;
|
||||||
import atlantafx.sampler.Launcher;
|
|
||||||
import atlantafx.sampler.Resources;
|
import atlantafx.sampler.Resources;
|
||||||
import atlantafx.sampler.event.DefaultEventBus;
|
import atlantafx.sampler.event.DefaultEventBus;
|
||||||
import atlantafx.sampler.event.EventBus;
|
import atlantafx.sampler.event.EventBus;
|
||||||
@ -12,10 +11,8 @@ import atlantafx.sampler.util.JColor;
|
|||||||
import javafx.application.Application;
|
import javafx.application.Application;
|
||||||
import javafx.css.PseudoClass;
|
import javafx.css.PseudoClass;
|
||||||
import javafx.scene.Scene;
|
import javafx.scene.Scene;
|
||||||
import javafx.scene.layout.Pane;
|
|
||||||
import javafx.scene.paint.Color;
|
import javafx.scene.paint.Color;
|
||||||
|
|
||||||
import java.net.URI;
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import java.util.stream.IntStream;
|
import java.util.stream.IntStream;
|
||||||
@ -25,7 +22,14 @@ import static java.nio.charset.StandardCharsets.UTF_8;
|
|||||||
|
|
||||||
public final class ThemeManager {
|
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<Class<? extends Theme>> PROJECT_THEMES = Set.of(
|
||||||
|
PrimerLight.class, PrimerDark.class, NordLight.class, NordDark.class
|
||||||
|
);
|
||||||
|
|
||||||
private static final PseudoClass DARK = PseudoClass.getPseudoClass("dark");
|
private static final PseudoClass DARK = PseudoClass.getPseudoClass("dark");
|
||||||
private static final PseudoClass USER_CUSTOM = PseudoClass.getPseudoClass("user-custom");
|
private static final PseudoClass USER_CUSTOM = PseudoClass.getPseudoClass("user-custom");
|
||||||
private static final EventBus EVENT_BUS = DefaultEventBus.getInstance();
|
private static final EventBus EVENT_BUS = DefaultEventBus.getInstance();
|
||||||
@ -40,15 +44,20 @@ public final class ThemeManager {
|
|||||||
private final Map<String, String> customCSSDeclarations = new LinkedHashMap<>(); // -fx-property | value;
|
private final Map<String, String> customCSSDeclarations = new LinkedHashMap<>(); // -fx-property | value;
|
||||||
private final Map<String, String> customCSSRules = new LinkedHashMap<>(); // .foo | -fx-property: value;
|
private final Map<String, String> customCSSRules = new LinkedHashMap<>(); // .foo | -fx-property: value;
|
||||||
|
|
||||||
private Scene scene;
|
private final ThemeRepository repository = new ThemeRepository();
|
||||||
private Pane body;
|
|
||||||
|
|
||||||
private Theme currentTheme = null;
|
private Scene scene;
|
||||||
|
|
||||||
|
private SamplerTheme currentTheme = null;
|
||||||
private String fontFamily = DEFAULT_FONT_FAMILY_NAME;
|
private String fontFamily = DEFAULT_FONT_FAMILY_NAME;
|
||||||
private int fontSize = DEFAULT_FONT_SIZE;
|
private int fontSize = DEFAULT_FONT_SIZE;
|
||||||
private int zoom = DEFAULT_ZOOM;
|
private int zoom = DEFAULT_ZOOM;
|
||||||
private AccentColor accentColor = DEFAULT_ACCENT_COLOR;
|
private AccentColor accentColor = DEFAULT_ACCENT_COLOR;
|
||||||
|
|
||||||
|
public ThemeRepository getRepository() {
|
||||||
|
return repository;
|
||||||
|
}
|
||||||
|
|
||||||
public Scene getScene() {
|
public Scene getScene() {
|
||||||
return scene;
|
return scene;
|
||||||
}
|
}
|
||||||
@ -59,46 +68,26 @@ public final class ThemeManager {
|
|||||||
this.scene = Objects.requireNonNull(scene);
|
this.scene = Objects.requireNonNull(scene);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Theme getTheme() {
|
public SamplerTheme getTheme() {
|
||||||
return currentTheme;
|
return currentTheme;
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<Theme> getAvailableThemes() {
|
public SamplerTheme getDefaultTheme() {
|
||||||
var themes = new ArrayList<Theme>();
|
return getRepository().getAll().get(0);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** @see SamplerTheme */
|
||||||
* Resets user agent stylesheet and then adds {@link Theme} styles to the {@link Scene}
|
public void setTheme(SamplerTheme theme) {
|
||||||
* 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) {
|
|
||||||
Objects.requireNonNull(theme);
|
Objects.requireNonNull(theme);
|
||||||
|
|
||||||
Application.setUserAgentStylesheet(Objects.requireNonNull(theme.getUserAgentStylesheet()));
|
Application.setUserAgentStylesheet(Objects.requireNonNull(theme.getUserAgentStylesheet()));
|
||||||
|
|
||||||
if (currentTheme != null) {
|
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());
|
getScene().getRoot().pseudoClassStateChanged(DARK, theme.isDarkMode());
|
||||||
|
|
||||||
// remove user CSS customizations and reset accent on theme change
|
// remove user CSS customizations and reset accent on theme change
|
||||||
@ -133,7 +122,8 @@ public final class ThemeManager {
|
|||||||
|
|
||||||
public void setFontSize(int size) {
|
public void setFontSize(int size) {
|
||||||
if (!SUPPORTED_FONT_SIZE.contains(size)) {
|
if (!SUPPORTED_FONT_SIZE.contains(size)) {
|
||||||
throw new IllegalArgumentException(String.format("Font size must in the range %d-%dpx. Actual value is %d.",
|
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(0),
|
||||||
SUPPORTED_FONT_SIZE.get(SUPPORTED_FONT_SIZE.size() - 1),
|
SUPPORTED_FONT_SIZE.get(SUPPORTED_FONT_SIZE.size() - 1),
|
||||||
size
|
size
|
||||||
@ -164,10 +154,9 @@ public final class ThemeManager {
|
|||||||
|
|
||||||
public void setZoom(int zoom) {
|
public void setZoom(int zoom) {
|
||||||
if (!SUPPORTED_ZOOM.contains(zoom)) {
|
if (!SUPPORTED_ZOOM.contains(zoom)) {
|
||||||
throw new IllegalArgumentException(String.format("Zoom value must one of %s. Actual value is %d.",
|
throw new IllegalArgumentException(
|
||||||
SUPPORTED_ZOOM,
|
String.format("Zoom value must one of %s. Actual value is %d.", SUPPORTED_ZOOM, zoom)
|
||||||
zoom
|
);
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setFontSize((int) Math.ceil(zoom != 100 ? (DEFAULT_FONT_SIZE * zoom) / 100.0f : DEFAULT_FONT_SIZE));
|
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);
|
getScene().getRoot().pseudoClassStateChanged(USER_CUSTOM, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@SafeVarargs
|
|
||||||
private <T> Set<T> merge(T first, T... arr) {
|
|
||||||
var set = new LinkedHashSet<T>();
|
|
||||||
set.add(first);
|
|
||||||
Collections.addAll(set, arr);
|
|
||||||
return set;
|
|
||||||
}
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////
|
||||||
// Singleton //
|
// Singleton //
|
||||||
///////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
@ -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<SamplerTheme> THEME_COMPARATOR = Comparator.comparing(SamplerTheme::getName);
|
||||||
|
|
||||||
|
private final List<SamplerTheme> internalThemes = Arrays.asList(
|
||||||
|
new SamplerTheme(new PrimerLight()),
|
||||||
|
new SamplerTheme(new PrimerDark()),
|
||||||
|
new SamplerTheme(new NordLight()),
|
||||||
|
new SamplerTheme(new NordDark())
|
||||||
|
);
|
||||||
|
|
||||||
|
private final List<SamplerTheme> 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<SamplerTheme> 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());
|
||||||
|
}
|
||||||
|
}
|
@ -5,6 +5,7 @@ module atlantafx.sampler {
|
|||||||
requires atlantafx.base;
|
requires atlantafx.base;
|
||||||
|
|
||||||
requires java.desktop;
|
requires java.desktop;
|
||||||
|
requires java.prefs;
|
||||||
requires javafx.swing;
|
requires javafx.swing;
|
||||||
requires javafx.media;
|
requires javafx.media;
|
||||||
requires javafx.web;
|
requires javafx.web;
|
||||||
|
@ -28,6 +28,6 @@
|
|||||||
>.footer {
|
>.footer {
|
||||||
-fx-border-width: 1 0 0 0;
|
-fx-border-width: 1 0 0 0;
|
||||||
-fx-border-color: -color-border-default;
|
-fx-border-color: -color-border-default;
|
||||||
-fx-padding: 10;
|
-fx-padding: 10px 20px 10px 20px;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -5,3 +5,4 @@
|
|||||||
@use "contrast-checker";
|
@use "contrast-checker";
|
||||||
@use "accent-color-selector";
|
@use "accent-color-selector";
|
||||||
@use "quick-config-menu";
|
@use "quick-config-menu";
|
||||||
|
@use "theme-repo-manager";
|
||||||
|
@ -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;
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user