RFR: 8267546: Add CSS themes as a first-class concept
Michael Strauß
mstrauss at openjdk.org
Wed Nov 9 22:30:44 UTC 2022
On Thu, 1 Jul 2021 09:36:13 GMT, Mike Hearn <duke 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");
>> }
>> });
>
> modules/javafx.graphics/src/main/native-glass/win/RoActivationSupport.cpp line 93:
>
>> 91:
>> 92: FnRtlGetVersion* pRtlGetVersion;
>> 93: if (!loadFunction(hLibNtdll, pRtlGetVersion, "RtlGetVersion")) {
>
> Is this really the right way to get the Windows version these days? It used to be that everything in NTDLL was off limits. Seems there's an API now for detecting if you're on Windows 8+:
>
> https://docs.microsoft.com/en-us/windows/win32/sysinfo/version-helper-apis
Thanks, I'll look into that.
> modules/javafx.graphics/src/test/java/test/javafx/application/ThemeTest.java line 16:
>
>> 14: public class ThemeTest {
>> 15:
>> 16: public static class PublicConstructor implements Theme {
>
> Rather than support varying constructor types to pass in the properties map, why not just specify the API as that platformThemeChanged will be called immediately after construction? That way there's only one code path that handles the properties map, and the way the class is constructed no longer matters.
Yes, the constructor argument is gone. As an alternative, there's `Theme.getPlatformThemeProperties()`, which can safely be called in the constructor of a theme class.
-------------
PR: https://git.openjdk.org/jfx/pull/511
More information about the openjfx-dev
mailing list