RFR: 8267546: Add CSS themes as a first-class concept [v18]
Michael Strauß
mstrauss at openjdk.org
Mon Jan 9 18:33:40 UTC 2023
> 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.
> *
> * @implSpec Implementations of this method that return an {@link ObservableList} must emit all
> * change notifications on the JavaFX application thread.
> *
> * @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 21, 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 can be modified on any thread, but it is not thread-safe and must
> * not be concurrently modified with {@link #styleThemeProperty() styleTheme}.
> */
> 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 can be modified on any thread, but it is not thread-safe and must not be
> * concurrently modified with {@link #userAgentStylesheetProperty() userAgentStylesheet}.
> */
> 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()`.
>
> Here's a list of the preferences available for Windows, as reported by the [SystemParametersInfo](https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-systemparametersinfow), [GetSysColor](https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getsyscolor) and [Windows.UI.ViewManagement.UISettings.GetColorValue](https://learn.microsoft.com/en-us/uwp/api/windows.ui.viewmanagement.uisettings.getcolorvalue) APIs. Deprecated colors are not included.
> | Windows preferences | Type |
> |--------------------------------------|---------|
> | Windows.SPI.HighContrast | Boolean |
> | Windows.SPI.HighContrastColorScheme | String |
> | Windows.SysColor.COLOR_3DFACE | Color |
> | Windows.SysColor.COLOR_BTNTEXT | Color |
> | Windows.SysColor.COLOR_GRAYTEXT | Color |
> | Windows.SysColor.COLOR_HIGHLIGHT | Color |
> | Windows.SysColor.COLOR_HIGHLIGHTTEXT | Color |
> | Windows.SysColor.COLOR_HOTLIGHT | Color |
> | Windows.SysColor.COLOR_WINDOW | Color |
> | Windows.SysColor.COLOR_WINDOWTEXT | Color |
> | Windows.UIColor.Background | Color |
> | Windows.UIColor.Foreground | Color |
> | Windows.UIColor.AccentDark3 | Color |
> | Windows.UIColor.AccentDark2 | Color |
> | Windows.UIColor.AccentDark1 | Color |
> | Windows.UIColor.Accent | Color |
> | Windows.UIColor.AccentLight1 | Color |
> | Windows.UIColor.AccentLight2 | Color |
> | Windows.UIColor.AccentLight3 | Color |
>
> Here is a list of macOS preferences as reported by `NSColor`'s [UI Element Colors](https://developer.apple.com/documentation/appkit/nscolor/ui_element_colors) and [Adaptable System Colors](https://developer.apple.com/documentation/appkit/nscolor/standard_colors). Deprecated colors are not included.
> | macOS preferences | Type |
> |----------------------------------------------------------|---------|
> | macOS.NSColor.labelColor | Color |
> | macOS.NSColor.secondaryLabelColor | Color |
> | macOS.NSColor.tertiaryLabelColor | Color |
> | macOS.NSColor.quaternaryLabelColor | Color |
> | macOS.NSColor.textColor | Color |
> | macOS.NSColor.placeholderTextColor | Color |
> | macOS.NSColor.selectedTextColor | Color |
> | macOS.NSColor.textBackgroundColor | Color |
> | macOS.NSColor.selectedTextBackgroundColor | Color |
> | macOS.NSColor.keyboardFocusIndicatorColor | Color |
> | macOS.NSColor.unemphasizedSelectedTextColor | Color |
> | macOS.NSColor.unemphasizedSelectedTextBackgroundColor | Color |
> | macOS.NSColor.linkColor | Color |
> | macOS.NSColor.separatorColor | Color |
> | macOS.NSColor.selectedContentBackgroundColor | Color |
> | macOS.NSColor.unemphasizedSelectedContentBackgroundColor | Color |
> | macOS.NSColor.selectedMenuItemTextColor | Color |
> | macOS.NSColor.gridColor | Color |
> | macOS.NSColor.headerTextColor | Color |
> | macOS.NSColor.alternatingContentBackgroundColors | Color[] |
> | macOS.NSColor.controlAccentColor | Color |
> | macOS.NSColor.controlColor | Color |
> | macOS.NSColor.controlBackgroundColor | Color |
> | macOS.NSColor.controlTextColor | Color |
> | macOS.NSColor.disabledControlTextColor | Color |
> | macOS.NSColor.selectedControlColor | Color |
> | macOS.NSColor.selectedControlTextColor | Color |
> | macOS.NSColor.alternateSelectedControlTextColor | Color |
> | macOS.NSColor.currentControlTint | String |
> | macOS.NSColor.windowBackgroundColor | Color |
> | macOS.NSColor.windowFrameTextColor | Color |
> | macOS.NSColor.underPageBackgroundColor | Color |
> | macOS.NSColor.findHighlightColor | Color |
> | macOS.NSColor.highlightColor | Color |
> | macOS.NSColor.shadowColor | Color |
> | macOS.NSColor.systemBlueColor | Color |
> | macOS.NSColor.systemBrownColor | Color |
> | macOS.NSColor.systemGrayColor | Color |
> | macOS.NSColor.systemGreenColor | Color |
> | macOS.NSColor.systemIndigoColor | Color |
> | macOS.NSColor.systemOrangeColor | Color |
> | macOS.NSColor.systemPinkColor | Color |
> | macOS.NSColor.systemPurpleColor | Color |
> | macOS.NSColor.systemRedColor | Color |
> | macOS.NSColor.systemTealColor | Color |
> | macOS.NSColor.systemYellowColor | Color |
>
> On Linux, GTK's theme name and [public CSS colors](https://gitlab.gnome.org/GNOME/gtk/-/blob/gtk-3-22/gtk/theme/Adwaita/_colors-public.scss) are reported:
> | Linux preferences | Type |
> |----------------------------------------------------|---------|
> | Linux.GTK.ThemeName | String |
> | Linux.GTK.Colors.theme_fg_color | Color |
> | Linux.GTK.Colors.theme_bg_color | Color |
> | Linux.GTK.Colors.theme_base_color | Color |
> | Linux.GTK.Colors.theme_selected_bg_color | Color |
> | Linux.GTK.Colors.theme_selected_fg_color | Color |
> | Linux.GTK.Colors.insensitive_bg_color | Color |
> | Linux.GTK.Colors.insensitive_fg_color | Color |
> | Linux.GTK.Colors.insensitive_base_color | Color |
> | Linux.GTK.Colors.theme_unfocused_fg_color | Color |
> | Linux.GTK.Colors.theme_unfocused_bg_color | Color |
> | Linux.GTK.Colors.theme_unfocused_base_color | Color |
> | Linux.GTK.Colors.theme_unfocused_selected_bg_color | Color |
> | Linux.GTK.Colors.theme_unfocused_selected_fg_color | Color |
> | Linux.GTK.Colors.borders | Color |
> | Linux.GTK.Colors.unfocused_borders | Color |
> | Linux.GTK.Colors.warning_color | Color |
> | Linux.GTK.Colors.error_color | Color |
> | Linux.GTK.Colors.success_color | Color |
>
> ### Built-in themes
> The two built-in themes `CaspianTheme` and `ModenaTheme` are exposed as public API in the `javafx.scene.control.theme` package. Both classes extend `ThemeBase`, which is a simple `StyleTheme` implementation that allows developers to easily extend the built-in themes.
>
> ### 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");
> }
> });
Michael Strauß has updated the pull request incrementally with two additional commits since the last revision:
- Changed GTK preference keys
- Platform.Preferences implements ObservableMap
-------------
Changes:
- all: https://git.openjdk.org/jfx/pull/511/files
- new: https://git.openjdk.org/jfx/pull/511/files/1304d252..757f7ff5
Webrevs:
- full: https://webrevs.openjdk.org/?repo=jfx&pr=511&range=17
- incr: https://webrevs.openjdk.org/?repo=jfx&pr=511&range=16-17
Stats: 751 lines in 14 files changed: 275 ins; 399 del; 77 mod
Patch: https://git.openjdk.org/jfx/pull/511.diff
Fetch: git fetch https://git.openjdk.org/jfx pull/511/head:pull/511
PR: https://git.openjdk.org/jfx/pull/511
More information about the openjfx-dev
mailing list