RFR: 8267546: Add CSS themes as a first-class concept
Michael Strauß
mstrauss at openjdk.org
Wed Nov 9 22:30:43 UTC 2022
On Fri, 21 May 2021 04:30:28 GMT, Michael Strauß <mstrauss at openjdk.org> wrote:
> This PR adds style themes as a first-class concept to OpenJFX. A style theme is a collection of stylesheets and the logic that governs them. Style themes can respond to OS notifications and update their stylesheets dynamically. This PR also re-implements Caspian and Modena as style themes.
>
> ### New APIs in `javafx.graphics`
> The new theming-related APIs in `javafx.graphics` provide a basic framework to support application-wide style themes. Higher-level theming concepts (for example, "dark mode" detection or accent coloring) are not a part of this basic framework, because any API invented here might soon be out of date. Implementations can build on top of this framework to add useful higher-level features.
> #### 1. StyleTheme
> A style theme is an implementation of the `javafx.css.StyleTheme` interface:
>
> /**
> * {@code StyleTheme} is a collection of stylesheets that specify the appearance of UI controls and other
> * nodes in the application. Like a user-agent stylesheet, a {@code StyleTheme} is implicitly used by all
> * JavaFX nodes in the scene graph.
> * <p>
> * The list of stylesheets that comprise a {@code StyleTheme} can be modified while the application is running,
> * enabling applications to create dynamic themes that respond to changing user preferences.
> * <p>
> * In the CSS subsystem, stylesheets that comprise a {@code StyleTheme} are classified as
> * {@link StyleOrigin#USER_AGENT} stylesheets, but have a higher precedence in the CSS cascade
> * than a stylesheet referenced by {@link Application#userAgentStylesheetProperty()}.
> */
> public interface StyleTheme {
> /**
> * Gets the list of stylesheet URLs that comprise this {@code StyleTheme}.
> * <p>
> * If the list of stylesheets that comprise this {@code StyleTheme} is changed at runtime, this
> * method must return an {@link ObservableList} to allow the CSS subsystem to subscribe to list
> * change notifications.
> *
> * @implNote Implementations of this method that return an {@link ObservableList} are encouraged
> * to minimize the number of subsequent list change notifications that are fired by the
> * list, as each change notification causes the CSS subsystem to re-apply the referenced
> * stylesheets.
> */
> List<String> getStylesheets();
> }
>
>
> A new `styleTheme` property is added to `javafx.application.Application`, and `userAgentStylesheet` is promoted to a JavaFX property (currently, this is just a getter/setter pair):
>
> public class Application {
> ...
> /**
> * Specifies the user-agent stylesheet of the application.
> * <p>
> * A user-agent stylesheet is a global stylesheet that can be specified in addition to a
> * {@link StyleTheme} and that is implicitly used by all JavaFX nodes in the scene graph.
> * It can be used to provide default styling for UI controls and other nodes.
> * A user-agent stylesheets has the lowest precedence in the CSS cascade.
> * <p>
> * Before JavaFX 20, built-in themes were selectable using the special user-agent stylesheet constants
> * {@link #STYLESHEET_CASPIAN} and {@link #STYLESHEET_MODENA}. For backwards compatibility, the meaning
> * of these special constants is retained: setting the user-agent stylesheet to either {@code STYLESHEET_CASPIAN}
> * or {@code STYLESHEET_MODENA} will also set the value of the {@link #styleThemeProperty() styleTheme}
> * property to a new instance of the corresponding theme class.
> * <p>
> * Note: this property must only be modified on the JavaFX application thread.
> */
> public static StringProperty userAgentStylesheetProperty();
> public static String getUserAgentStylesheet();
> public static void setUserAgentStylesheet(String url);
>
> /**
> * Specifies the {@link StyleTheme} of the application.
> * <p>
> * {@code StyleTheme} is a collection of stylesheets that define the appearance of the application.
> * Like a user-agent stylesheet, a {@code StyleTheme} is implicitly used by all JavaFX nodes in the
> * scene graph.
> * <p>
> * Stylesheets that comprise a {@code StyleTheme} have a higher precedence in the CSS cascade than a
> * stylesheet referenced by the {@link #userAgentStylesheetProperty() userAgentStylesheet} property.
> * <p>
> * Note: this property must only be modified on the JavaFX application thread.
> */
> public static ObjectProperty<StyleTheme> styleThemeProperty();
> public static StyleTheme getStyleTheme();
> public static void setStyleTheme(StyleTheme theme);
> ...
> }
>
>
> `styleTheme` and `userAgentStylesheet` are correlated to preserve backwards compatibility: setting `userAgentStylesheet` to the magic values "CASPIAN" or "MODENA" will implicitly set `styleTheme` to a new instance of the `CaspianTheme` or `ModenaTheme` class. Aside from these magic values, `userAgentStylesheet` can be set independently from `styleTheme`. In the CSS cascade, `userAgentStylesheet` has a lower precedence than `styleTheme`.
>
> #### 2. PlatformPreferences
> `javafx.application.PlatformPreferences` can be used to query UI-related information about the current platform to allow theme implementations to adapt to the operating system. The interface extends `Map` and adds several useful methods, as well as the option to register a listener for change notifications:
>
> /**
> * Contains UI preferences of the current platform.
> * <p>
> * {@code PlatformPreferences} implements {@link Map} to expose platform preferences as key-value pairs.
> * For convenience, {@link #getString}, {@link #getBoolean} and {@link #getColor} are provided as typed
> * alternatives to the untyped {@link #get} method.
> * <p>
> * The preferences that are reported by the platform may be dependent on the operating system version.
> * Applications should always test whether a preference is available, or use the {@link #getString(String, String)},
> * {@link #getBoolean(String, boolean)} or {@link #getColor(String, Color)} overloads that accept a fallback
> * value if the preference is not available.
> */
> public interface PlatformPreferences extends Map<String, Object> {
> String getString(String key);
> String getString(String key, String fallbackValue);
>
> Boolean getBoolean(String key);
> boolean getBoolean(String key, boolean fallbackValue);
>
> Color getColor(String key);
> Color getColor(String key, Color fallbackValue);
>
> void addListener(PlatformPreferencesListener listener);
> void removeListener(PlatformPreferencesListener listener);
> }
>
> An instance of `PlatformPreferences` can be retrieved via `Platform.getPreferences()`.
>
> ### Usage
> In its simplest form, a style theme is just a static collection of stylesheets:
>
> Application.setStyleTheme(() -> List.of("stylesheet1.css", "stylesheet2.css");
>
> A dynamic theme can be created by returning an instance of `ObservableList`:
>
> public class MyCustomTheme implements StyleTheme {
> private final ObservableList<String> stylesheets =
> FXCollections.observableArrayList("colors-light.css", "controls.css");
>
> @Override
> public List<String> getStylesheets() {
> return stylesheets;
> }
>
> public void setDarkMode(boolean enabled) {
> stylesheets.set(0, enabled ? "colors-dark.css" : "colors-light.css");
> }
> }
>
> `CaspianTheme` and `ModenaTheme` can be extended by prepending or appending additional stylesheets:
>
> Application.setStyleTheme(new ModenaTheme() {
> {
> addFirst("stylesheet1.css");
> addLast("stylesheet2.css");
> }
> });
Here's an update on the current state of this feature. The API has evolved a bit to offer more flexibility, and can be used in several different ways:
### 1. Ad-hoc themes
An ad-hoc theme is a way to programmatically tie together a bunch of stylesheets with very little effort:
var theme = Theme.of("stylesheet1.css", "stylesheet2.css");
Similarly, existing themes can be extended with additional stylesheets. The stylesheet order is retained such that base stylesheets always come before extension stylesheets, even if the list of base stylesheets changes.
var theme = Theme.extensionOf(new ModenaTheme(), "stylesheet1.css", "stylesheet2.css");
### 2. Theme classes
When more control is desired, a theme can be implemented by extending the `Theme` class. In this example, a custom theme toggles a high-contrast stylesheet:
public class MyTheme extends Theme {
private final ObservableList<String> stylesheets = FXCollections.observableArrayList();
public MyTheme() {
stylesheets.add("stylesheet1.css");
platformThemeChanged(getPlatformThemeProperties());
}
@Override
public ObservableList<String> getStylesheets() {
return stylesheets;
}
@Override
protected void platformThemeChanged(Map<String, String> properties) {
String value = properties.get("Windows.SPI_HighContrastOn");
if (value != null) {
if (Boolean.parseBoolean(value)) {
stylesheets.add("highconstrast.css");
} else {
stylesheets.remove("highconstrast.css");
}
}
}
}
Similarly, an existing theme class can be extended by subclassing:
public class MyTheme extends ModenaTheme {
// concat existing stylesheets with new stylesheets, react to platform changes, etc.
}
### 3. Usage
Themes are collections of user-agent stylesheets, and as such, they can be used in all places where user-agent stylesheets can be used (that's in `Application`, `Scene` and `SubScene`).
Themes, like user-agent stylesheets, must be specified with a URL to be able to use them with the existing `setUserAgentStylesheet(String)` APIs. There are three ways to specify a theme:
#### 3.1. Class URLs
The easiest option to reference a theme class is with the `theme` scheme:
Application.setUserAgentStylesheet("theme:com.example.MyTheme");
The `theme` scheme works when the theme class is instantiable with a parameterless constructor.
#### 3.2. Instance URLs
Ad-hoc themes, or theme classes that require custom instantiation, can be referenced by their instance URL:
var theme = Theme.of("stylesheet1.css", "stylesheet2.css");
Application.setUserAgentStylesheet(theme.toURL());
The URL returned by `Theme.toURL()` can be dereferenced as long as the theme instance is alive. It is not required to keep a reference to the theme instance around after `setUserAgentStylesheet` has been called, as the theming subsystem will keep the theme instance alive.
#### 3.3. Legacy names
Backwards compatibility is preserved by interpreting the names `CASPIAN` and `MODENA` as aliases for their corresponding theme class. Since themes can be used with all user-agent stylesheet APIs, we can now also use different themes in the same application:
<img src="https://user-images.githubusercontent.com/43553916/130900849-302a24ca-7b7b-412f-8af8-9cabad11f4ea.PNG" width="300">
-------------
PR: https://git.openjdk.org/jfx/pull/511
More information about the openjfx-dev
mailing list