Refactor theme manager to support arbitrary CSS modification at runtime

This commit is contained in:
mkpaz 2022-08-25 14:53:48 +04:00
parent 8c60ae7c07
commit cd16046340
4 changed files with 91 additions and 26 deletions

@ -51,7 +51,8 @@ public class Launcher extends Application {
var scene = new Scene(root, 1200, 768); var scene = new Scene(root, 1200, 768);
var tm = ThemeManager.getInstance(); var tm = ThemeManager.getInstance();
tm.setTheme(scene, tm.getAvailableThemes().get(0)); tm.setScene(scene);
tm.setTheme(tm.getAvailableThemes().get(0));
if (IS_DEV_MODE) { startCssFX(scene); } if (IS_DEV_MODE) { startCssFX(scene); }
scene.getStylesheets().addAll( scene.getStylesheets().addAll(

@ -62,7 +62,9 @@ public class ThemePage extends AbstractPage {
selector.getItems().setAll(manager.getAvailableThemes()); 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) {
ThemeManager.getInstance().setTheme(getScene(), val); var tm = ThemeManager.getInstance();
tm.setTheme(val);
tm.reloadCustomCSS();
colorPalette.updateColorInfo(Duration.ofSeconds(1)); colorPalette.updateColorInfo(Duration.ofSeconds(1));
} }
}); });

@ -69,7 +69,9 @@ public class TypographyPage extends AbstractPage {
spinner.valueProperty().addListener((obs, old, val) -> { spinner.valueProperty().addListener((obs, old, val) -> {
if (val != null && getScene() != null) { if (val != null && getScene() != null) {
ThemeManager.getInstance().setFontSize(getScene(), val); var tm = ThemeManager.getInstance();
tm.setFontSize(val);
tm.reloadCustomCSS();
updateFontInfo(Duration.ofMillis(1000)); updateFontInfo(Duration.ofMillis(1000));
} }
}); });
@ -122,10 +124,10 @@ public class TypographyPage extends AbstractPage {
private SampleBlock fontWeightSample() { private SampleBlock fontWeightSample() {
var box = new HBox(10, var box = new HBox(10,
text("Bold", TEXT_BOLD), text("Bold", TEXT_BOLD),
text("Bolder", TEXT_BOLDER), text("Bolder", TEXT_BOLDER),
text("Normal", TEXT_NORMAL), text("Normal", TEXT_NORMAL),
text("Lighter", TEXT_LIGHTER) text("Lighter", TEXT_LIGHTER)
); );
box.setAlignment(Pos.BASELINE_LEFT); box.setAlignment(Pos.BASELINE_LEFT);
@ -134,10 +136,10 @@ public class TypographyPage extends AbstractPage {
private SampleBlock fontStyleSample() { private SampleBlock fontStyleSample() {
var box = new HBox(10, var box = new HBox(10,
text("Italic", TEXT_ITALIC), text("Italic", TEXT_ITALIC),
text("Oblique", TEXT_OBLIQUE), text("Oblique", TEXT_OBLIQUE),
text("Underlined", TEXT_UNDERLINED), text("Underlined", TEXT_UNDERLINED),
text("Strikethrough", TEXT_STRIKETHROUGH) text("Strikethrough", TEXT_STRIKETHROUGH)
); );
box.setAlignment(Pos.BASELINE_LEFT); box.setAlignment(Pos.BASELINE_LEFT);
@ -146,10 +148,10 @@ public class TypographyPage extends AbstractPage {
private SampleBlock textColorSample() { private SampleBlock textColorSample() {
var box = new HBox(10, var box = new HBox(10,
text("Accent", TEXT, ACCENT), text("Accent", TEXT, ACCENT),
text("Success", TEXT, SUCCESS), text("Success", TEXT, SUCCESS),
text("Warning", TEXT, WARNING), text("Warning", TEXT, WARNING),
text("Danger", TEXT, DANGER) text("Danger", TEXT, DANGER)
); );
box.setAlignment(Pos.BASELINE_LEFT); box.setAlignment(Pos.BASELINE_LEFT);
@ -164,9 +166,9 @@ public class TypographyPage extends AbstractPage {
linkVisited.setMnemonicParsing(true); linkVisited.setMnemonicParsing(true);
var box = new HBox(10, var box = new HBox(10,
linkNormal, linkNormal,
linkVisited, linkVisited,
hyperlink("Disabled", false, true) hyperlink("Disabled", false, true)
); );
box.setAlignment(Pos.BASELINE_LEFT); box.setAlignment(Pos.BASELINE_LEFT);

@ -7,6 +7,7 @@ import atlantafx.base.theme.Theme;
import atlantafx.sampler.Launcher; import atlantafx.sampler.Launcher;
import atlantafx.sampler.Resources; import atlantafx.sampler.Resources;
import javafx.application.Application; import javafx.application.Application;
import javafx.css.PseudoClass;
import javafx.scene.Scene; import javafx.scene.Scene;
import java.net.URI; import java.net.URI;
@ -17,9 +18,25 @@ import static java.nio.charset.StandardCharsets.UTF_8;
public final class ThemeManager { public final class ThemeManager {
private static final String DUMMY_STYLESHEET = Resources.getResource("assets/styles/empty.css").toString(); private static final String DUMMY_STYLESHEET = Resources.getResource("assets/styles/empty.css").toString();
private static final PseudoClass USER_CUSTOM = PseudoClass.getPseudoClass("user-custom");
// KEY | VALUE
// -fx-property | value;
private final Map<String, String> customCSSDeclarations = new LinkedHashMap<>();
// .foo | -fx-property: value;
private final Map<String, String> customCSSRules = new LinkedHashMap<>();
private Theme currentTheme = null; private Theme currentTheme = null;
private int currentFontSize = 14; private int currentFontSize = 14;
private Scene scene;
public Scene getScene() {
return scene;
}
public void setScene(Scene scene) {
this.scene = scene;
}
public Theme getTheme() { public Theme getTheme() {
return currentTheme; return currentTheme;
@ -33,7 +50,8 @@ public final class ThemeManager {
* E.g. JavaFX ignores Ikonli -fx-icon-color and -fx-icon-size properties, but for an unknown * 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()}. * reason they won't be ignored when exactly the same stylesheet is set via {@link Scene#getStylesheets()}.
*/ */
public void setTheme(Scene scene, Theme theme) { public void setTheme(Theme theme) {
Objects.requireNonNull(scene);
Objects.requireNonNull(theme); Objects.requireNonNull(theme);
Application.setUserAgentStylesheet(Objects.requireNonNull(theme.getUserAgentStylesheet())); Application.setUserAgentStylesheet(Objects.requireNonNull(theme.getUserAgentStylesheet()));
@ -81,17 +99,59 @@ public final class ThemeManager {
return currentFontSize; return currentFontSize;
} }
public void setFontSize(Scene scene, int fontSize) { public void setFontSize(int fontSize) {
String css = String.format(".root { -fx-font-size: %dpx; } .ikonli-font-icon { -fx-icon-size: %dpx; }", setCustomDeclaration("-fx-font-size", fontSize + "px");
fontSize, setCustomRule(".ikonli-font-icon", String.format("-fx-icon-size: %dpx;", fontSize + 2));
fontSize + 2 currentFontSize = fontSize;
); }
private void setCustomDeclaration(String property, String value) {
customCSSDeclarations.put(property, value);
}
private void setCustomRule(String selector, String rule) {
customCSSRules.put(selector, rule);
}
public void reloadCustomCSS() {
Objects.requireNonNull(scene);
StringBuilder css = new StringBuilder();
css.append(".root:");
css.append(USER_CUSTOM.getPseudoClassName());
css.append(" {\n");
customCSSDeclarations.forEach((k, v) -> {
css.append("\t");
css.append(k);
css.append(":\s");
css.append(v);
css.append(";\n");
});
css.append("}\n");
customCSSRules.forEach((k, v) -> {
css.append(".root:");
css.append(USER_CUSTOM.getPseudoClassName());
css.append(" ");
css.append(k);
css.append(" {");
css.append(v);
css.append("}\n");
});
System.out.println(css);
scene.getStylesheets().removeIf(uri -> uri.startsWith("data:text/css")); scene.getStylesheets().removeIf(uri -> uri.startsWith("data:text/css"));
scene.getStylesheets().add( scene.getStylesheets().add(
"data:text/css;base64," + Base64.getEncoder().encodeToString(css.getBytes(UTF_8)) "data:text/css;base64," + Base64.getEncoder().encodeToString(css.toString().getBytes(UTF_8))
); );
scene.getRoot().pseudoClassStateChanged(USER_CUSTOM, true);
}
currentFontSize = fontSize; public void resetCustomCSS() {
customCSSDeclarations.clear();
customCSSRules.clear();
scene.getRoot().pseudoClassStateChanged(USER_CUSTOM, false);
} }
public HighlightJSTheme getMatchingHighlightJSTheme(Theme theme) { public HighlightJSTheme getMatchingHighlightJSTheme(Theme theme) {