RFR: 8267546: Add CSS themes as a first-class concept [v18]
Pedro Duque Vieira
duke at openjdk.org
Fri Jan 13 21:06:34 UTC 2023
On Mon, 9 Jan 2023 18:33:40 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.
>> *
>> * @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. Preferences
>> `javafx.application.Platform.Preferences` 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 `ObservableMap` 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 Preferences} extends {@link ObservableMap} 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 Preferences extends ObservableMap<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);
>> }
>>
>> An instance of `Preferences` 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 |
>> |----------------------------------------------------|---------|
>> | GTK.theme_name | String |
>> | GTK.theme_fg_color | Color |
>> | GTK.theme_bg_color | Color |
>> | GTK.theme_base_color | Color |
>> | GTK.theme_selected_bg_color | Color |
>> | GTK.theme_selected_fg_color | Color |
>> | GTK.insensitive_bg_color | Color |
>> | GTK.insensitive_fg_color | Color |
>> | GTK.insensitive_base_color | Color |
>> | GTK.theme_unfocused_fg_color | Color |
>> | GTK.theme_unfocused_bg_color | Color |
>> | GTK.theme_unfocused_base_color | Color |
>> | GTK.theme_unfocused_selected_bg_color | Color |
>> | GTK.theme_unfocused_selected_fg_color | Color |
>> | GTK.borders | Color |
>> | GTK.unfocused_borders | Color |
>> | GTK.warning_color | Color |
>> | GTK.error_color | Color |
>> | GTK.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
Hi all,
I’ve seen in the mailing list a request for commenting on this PR and as a long-time theme developer (JMetro and other themes) I'd thought I'd give my 2 cents.
I think it’s a good idea to add the concept of a theme to the JavaFX API! So, thanks for this.
1 - Reading through the javadocs in this PR and the description, I think it’s not clear whether the stylesheets of a StyleTheme will be used as user agent stylesheets or as author stylesheets. It says that StyleThemes have higher precedence than a user agent stylesheet so I suppose they are going to be author stylesheets (?), but there’s no point in the Javadoc where that is explicitly said. If that’s not the case, then I think the opposite should then be explicitly written. I.e. that it will have higher precedence than a user agent stylesheet but it’s not an author stylesheet.
2 – I think the ability to specify in the StyleTheme whether it is a user agent stylesheet, or an author stylesheet could be of interest. Most of the themes I know are composed of author stylesheets (they use the getStylesheets() API in Scene) so migration to using the StyleTheme API would be easier if one could specify this. Conversely specifying that the StyleTheme is a user agent stylesheet will also be very useful in the cases where we’re creating a theme but don’t want it to override styles specified through code, etc.
3 – I would really love for JavaFX to have first class support for dark and light modes, and I think this would be the ideal place for it. One of the problems with how things are right now is that if you create a dark theme with this API or the previous API (using stylesheets) the frames of windows (main windows and dialogs) will still show with a light theme (I see this on Windows, haven’t tested on Mac but I suppose it will be the same).
So as it is, you can’t fully create a dark theme in JavaFX. The only way would be to call on native code to change the frame of windows (by making a request for the native window to change its appearance to dark mode) which isn’t trivial and would have to be done for the various operating systems and it’s subject to break.
The other way would be to create your own frames of the windows which I think would be even worse. You’d have to create the frame buttons yourself and all other decorations. If they don’t visually exactly match the ones from the native platform they’re going to look off. You’d also have to do this for all operating systems and for their various versions (various versions of the same OS might have different frame decorations, e.g. win10 vs win11).
Given that dark and light theme concepts are pervasive across all major operating systems (Windows, Mac, iOS, Android, etc) and it’s a concept that has lasted for years and continues to exist, I think it would be of value to fully support this.
Thanks
-------------
PR: https://git.openjdk.org/jfx/pull/511
More information about the openjfx-dev
mailing list