On Fri, 21 May 2021 04:30:28 GMT, Michael Strauß <mstra...@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

Reply via email to