Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

8267546: Add CSS themes as a first-class concept #511

Closed
wants to merge 25 commits into from

Conversation

mstr2
Copy link
Collaborator

@mstr2 mstr2 commented May 21, 2021

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 user-agent stylesheets that specify the appearance of UI controls and
 * other nodes in the application. {@code StyleTheme} is implicitly used by all JavaFX nodes in the scene graph,
 * unless it is overridden by any of the following properties:
 * <ul>
 *     <li>{@link Application#userAgentStylesheetProperty() Application.userAgentStylesheet}
 *     <li>{@link Scene#userAgentStylesheetProperty() Scene.userAgentStylesheet}
 *     <li>{@link SubScene#userAgentStylesheetProperty() SubScene.userAgentStylesheet}
 * </ul>
 * <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>
 * A {@code StyleTheme} can be applied using the {@link Application#setUserAgentStyleTheme(StyleTheme)} method:
 * <pre>{@code
 *     public class App extends Application {
 *         @Override
 *         public void start(Stage primaryStage) {
 *             setUserAgentStyleTheme(new MyCustomTheme());
 *
 *             primaryStage.setScene(...);
 *             primaryStage.show();
 *         }
 *     }
 * }</pre>
 *
 * @since 21
 */
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 userAgentStyleTheme 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 single user-agent stylesheet of the application.
     * <p>
     * The user-agent stylesheet is a global stylesheet that defines the appearance of the application.
     * It has the second-lowest precedence in the CSS cascade, and can be overridden in the scene graph
     * by setting the {@link Scene#userAgentStylesheetProperty() Scene.userAgentStylesheet} or
     * {@link SubScene#userAgentStylesheetProperty() SubScene.userAgentStylesheet} property.
     * <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 #userAgentStyleThemeProperty() userAgentStyleTheme}
     * 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 #userAgentStyleThemeProperty() userAgentStyleTheme}.
     *
     * @since 21
     */
    public static StringProperty userAgentStylesheetProperty();
    public static String getUserAgentStylesheet();
    public static void setUserAgentStylesheet(String url);

    /**
     * Specifies the user-agent {@link StyleTheme} of the application.
     * <p>
     * {@code StyleTheme} is a collection of user-agent stylesheets that define the appearance of the application.
     * {@code StyleTheme} has the lowest precedence in the CSS cascade, and can be overridden in the scene graph
     * by setting any of the following properties:
     * <ul>
     *     <li>{@link #userAgentStylesheetProperty() Application.userAgentStylesheet}
     *     <li>{@link Scene#userAgentStylesheetProperty() Scene.userAgentStylesheet}
     *     <li>{@link SubScene#userAgentStylesheetProperty() SubScene.userAgentStylesheet}
     * </ul>
     * <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}.
     *
     * @since 21
     */
    public static ObjectProperty<StyleTheme> userAgentStyleThemeProperty();
    public static StyleTheme getUserAgentStyleTheme();
    public static void setUserAgentStyleTheme(StyleTheme theme);
    ...
}

userAgentStyleTheme and userAgentStylesheet are correlated to preserve backwards compatibility: setting userAgentStylesheet to the magic values "CASPIAN" or "MODENA" will implicitly set userAgentStyleTheme to a new instance of the CaspianTheme or ModenaTheme class. In the CSS cascade, userAgentStylesheet has a higher precedence than userAgentStyleTheme.

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, GetSysColor and 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 and Adaptable System 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 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.setUserAgentStyleTheme(() -> 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.setUserAgentStyleTheme(new ModenaTheme() {
    {
        addFirst("stylesheet1.css");
        addLast("stylesheet2.css");
    }
});

Progress

  • Change must not contain extraneous whitespace
  • Change requires a CSR request matching fixVersion openjfx20 to be approved (needs to be created)
  • Commit message must refer to an issue
  • Change must be properly reviewed (3 reviews required, with at least 1 Reviewer, 2 Authors)

Integration blocker

 ⚠️ Too few reviewers with at least role reviewer found (have 0, need at least 1) (failed with the updated jcheck configuration)

Issue

Reviewing

Using git

Checkout this PR locally:
$ git fetch https://git.openjdk.org/jfx.git pull/511/head:pull/511
$ git checkout pull/511

Update a local copy of the PR:
$ git checkout pull/511
$ git pull https://git.openjdk.org/jfx.git pull/511/head

Using Skara CLI tools

Checkout this PR locally:
$ git pr checkout 511

View PR using the GUI difftool:
$ git pr show -t 511

Using diff file

Download this PR as a diff file:
https://git.openjdk.org/jfx/pull/511.diff

Webrev

Link to Webrev Comment

@bridgekeeper
Copy link

bridgekeeper bot commented May 21, 2021

👋 Welcome back mstrauss! A progress list of the required criteria for merging this PR into master will be added to the body of your pull request. There are additional pull request commands available for use with this pull request.

@mstr2 mstr2 changed the title Add themes as a first-class concept 8267546: Add themes as a first-class concept May 21, 2021
@mstr2 mstr2 changed the title 8267546: Add themes as a first-class concept 8267546: Add CSS themes as a first-class concept May 26, 2021
@mikehearn
Copy link

Looks great! I read through the code and nothing wrong jumped out - all the obvious missing parts were already raised on the mailing list and reasonable arguments provided for why it's about reducing maintenance costs.

The only thing that seemed odd is the way you set a theme by providing a special string syntax+class name as a "stylesheet". Is there some reason a more direct API cannot be added instead, where you pass the actual Class<T> constant instead, or simply construct the theme object yourself?

Also the Theme interface looks suspiciously like an SPI. Rather than use a special pseudo-URI that devs would have to fish out of the theme's documentation to trigger loading, it might be better to use ServiceLoader to locate themes and then have some notion of priority, such that simply adding a theme module to your modulepath would automatically cause it to override the platform defaults. This would allow apps to take the next obvious step and allow themes to be plugins. As is this API wouldn't support it out of the box, because the app would have no way to know what the class name of the theme actually is, so a bunch of ad-hoc mechanisms would emerge to fill that gap.

It seems Kevin wanted some more info on cost/benefit analysis on the mailing list, but it's a little unclear what sort of analysis or evidence would be wanted. I'll say that JavaFX apps I've written in the past would definitely have benefited from this. My CSS files always ended up being a mishmash of patches to Modena and actual app-specific styling. Additionally, whilst various JavaFX theme libraries exist and are popular, you normally have to do some manual integration work to use them which is a pity given that theming is, ultimately, entirely subjective and users frequently enjoy theming apps they work with a lot.

I'll also say that the whole JavaFX theming system was a point of confusion for me when I first learned the API. It clearly does support themes, yet there's no actual API point called "theme" anywhere and exactly how themes, CSS and so on relate isn't obvious. So this PR has a learning and usability benefit too.

Conclusion: to me the code looks really quite small, the benefits large (as most "real" JavaFX apps don't simply use Modena as-is), and it is at any rate mostly a refactoring and exposure of far more complex machinery already in the toolkit. Thumbs up!

@mstr2

This comment was marked as outdated.

@airsquared
Copy link

airsquared commented Nov 6, 2022

Are there any plans for this to be merged or is there any work remaining for this? I'd love to use this instead of what I'm currently doing which is accessing private API in JavaFX with --add-exports.

@mstr2 mstr2 marked this pull request as ready for review November 9, 2022 22:25
@openjdk openjdk bot added the rfr Ready for review label Nov 9, 2022
@mlbridge
Copy link

mlbridge bot commented Nov 9, 2022

@mstr2
Copy link
Collaborator Author

mstr2 commented Nov 9, 2022

I've iterated on this feature for several months, and I'm quite comfortable with the latest version. The API was changed considerably: there are no "theme URIs" masquerading as user-agent stylesheets any more. Instead there's an application-wide Application.styleTheme property. This simplifies the enhancement quite a lot. I've updated this PR with an overview of the API and some usage examples.

@kevinrushforth
Copy link
Member

This will take a fair bit of discussion regarding how various applications might use such a feature, what the API should look like, etc.

/reviewers 3
/csr

@openjdk
Copy link

openjdk bot commented Nov 9, 2022

@kevinrushforth
The total number of required reviews for this PR (including the jcheck configuration and the last /reviewers command) is now set to 3 (with at least 1 Reviewer, 2 Authors).

@openjdk openjdk bot added the csr Need approved CSR to integrate pull request label Nov 9, 2022
@openjdk
Copy link

openjdk bot commented Nov 9, 2022

@kevinrushforth has indicated that a compatibility and specification (CSR) request is needed for this pull request.

@mstr2 please create a CSR request for issue JDK-8267546 with the correct fix version. This pull request cannot be integrated until the CSR request is approved.

Copy link
Collaborator

@hjohn hjohn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like quite a nice feature, would love to see this in JavaFX.

*
* @since 20
*/
public interface PlatformPreferences extends Map<String, Object> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you sure it is a good idea to expose these as a Map? It seems to me that this isn't that good a practice any more to have classes implement or extend lists/maps/sets as they pull in a huge amount of API surface that is mostly useless or too general for the class's purpose.

The addition of the listener management methods also has me wondering, as ObservableMap does something similar.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All of the mutating methods are useless, since the implementation always returns a read-only map. However, the alternative would be to duplicate APIs to enumerate and inspect the contents of the PlatformPreferences map (applications might want to show a list of available preferences). I'm not sure that's preferable, mostly because PlatformPreferences does represent a mapping of keys to values.

It's true that listener management makes it look like an ObservableMap. The difference is that ObservableMap doesn't support batch change notifications, which the current implementation relies on to minimize the number of style theme resets. Of course, that could be solved in a different way.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The addition of the listener management methods also has me wondering, as ObservableMap does something similar.

I've switched to using ObservableMap.

@kevinrushforth kevinrushforth self-requested a review November 19, 2022 14:29
@mstr2
Copy link
Collaborator Author

mstr2 commented Nov 20, 2022

This will take a fair bit of discussion regarding how various applications might use such a feature, what the API should look like, etc.

I think there are basically three categories of how applications might use this feature:

  1. Extend a built-in theme.
  2. Create a new static theme.
  3. Create a dynamic theme that responds to OS preferences.

The proposed feature addresses all three categories, with the third option (a dynamic theme) being the most difficult to implement. It requires quite a bit of effort to create such a theme, and a good amount of that effort will be figuring out how OS-level preferences interact with themes (platform independence is a complicating factor).

I've made it an explicit non-goal of this feature to provide an API for higher-level concepts like dark mode, high contrast, etc., since I think APIs for OS design trends should be left to third-party libraries instead.

@dukke
Copy link

dukke commented Jan 13, 2023

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

@mstr2 mstr2 marked this pull request as draft January 29, 2023 01:34
@openjdk openjdk bot removed the rfr Ready for review label Jan 29, 2023
@bridgekeeper
Copy link

bridgekeeper bot commented Mar 31, 2023

@mstr2 This pull request has been inactive for more than 8 weeks and will be automatically closed if another 8 weeks passes without any activity. To avoid this, simply add a new comment to the pull request. Feel free to ask for assistance if you need help with progressing this pull request towards integration!

@openjdk openjdk bot added the merge-conflict Pull request has merge conflict with target branch label Apr 3, 2023
@bridgekeeper
Copy link

bridgekeeper bot commented May 27, 2023

@mstr2 This pull request has been inactive for more than 16 weeks and will now be automatically closed. If you would like to continue working on this pull request in the future, feel free to reopen it! This can be done using the /open pull request command.

@bridgekeeper bridgekeeper bot closed this May 27, 2023
@palexdev
Copy link

No don't, don't close this omg 🤦

@mstr2
Copy link
Collaborator Author

mstr2 commented May 28, 2023

No don't, don't close this omg 🤦

I’ll re-open once the prerequisite PRs are integrated. The discussion continues in #1014

@palexdev
Copy link

No don't, don't close this omg 🤦

I’ll re-open once the prerequisite PRs are integrated. The discussion continues in #1014

Oh I see, thanks for your work!

@palexdev palexdev mentioned this pull request May 29, 2023
4 tasks
@palexdev
Copy link

@mstr2 any news on this feature?

@mstr2
Copy link
Collaborator Author

mstr2 commented Feb 18, 2024

@mstr2 any news on this feature?

I've been working on this feature lately, and will soon be sharing more about it.

@dukke
Copy link

dukke commented Apr 11, 2024

@mstr2 First of all thank you very much for your work on this! 👍👍

Are there any news regarding StyleTheme?
This is a much needed feature for custom theme development.
You can't really create a full blown theme in JavaFX that's easy to override and works well with other libraries because this piece of API is missing from JavaFX (can't set more than 1 css file to be the user agent stylesheet, etc).

Thanks again!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
csr Need approved CSR to integrate pull request merge-conflict Pull request has merge conflict with target branch
Development

Successfully merging this pull request may close these issues.

8 participants