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

8301302: Platform preferences API #1014

Closed
wants to merge 62 commits into from

Conversation

mstr2
Copy link
Collaborator

@mstr2 mstr2 commented Jan 29, 2023

Please read this document for an introduction to the Platform Preferences API, and how it interacts with the proposed style theme and stage appearance features.


Progress

  • Change requires CSR request JDK-8319138 to be approved
  • Change must not contain extraneous whitespace
  • Commit message must refer to an issue
  • Change must be properly reviewed (3 reviews required, with at least 1 Reviewer, 2 Authors)

Issues

Reviewers

Reviewing

Using git

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

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

Using Skara CLI tools

Checkout this PR locally:
$ git pr checkout 1014

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

Using diff file

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

Webrev

Link to Webrev Comment

@bridgekeeper
Copy link

bridgekeeper bot commented Jan 29, 2023

👋 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.

@openjdk openjdk bot added the rfr Ready for review label Jan 29, 2023
@mlbridge
Copy link

mlbridge bot commented Jan 29, 2023

Webrevs

@hjohn
Copy link
Collaborator

hjohn commented Jan 29, 2023

Just commenting on the convenience API part. I think in modern Java you'd use Optional to avoid creating multiple methods for each type (Optional supports far more options as "fallback") and to avoid having to return null as a special value.

 /**
  * Gets an optional value associated with the given key. If the key is not present or not of the correct type
  * this optional will be empty.
  */
 Optional<String> getString(String key);

In this way you could get a certain key that may be platform specific like this (names made up):

 int mousePointerWidth = prefs.getInteger("windows-cursor-size")
       .or(() -> prefs.getInteger("linux-mouse-pointer-width"))
       .or(() -> prefs.getInteger("mac-pointer-radius").map(x -> x * 2))  // multiply radius by 2 to get width
       .orElse(16);

@mstr2
Copy link
Collaborator Author

mstr2 commented Jan 29, 2023

Just commenting on the convenience API part. I think in modern Java you'd use Optional to avoid creating multiple methods for each type (Optional supports far more options as "fallback") and to avoid having to return null as a special value.

That's a good suggestion, I've updated the API accordingly.

@kevinrushforth
Copy link
Member

It is premature to review this, since we have not had a discussion of whether we want such an API in the core of JavaFX. I am moving it back to Draft.

When/if it is ready to be reviewed it will need a CSR.

/csr needed
/reviewers 2

@kevinrushforth kevinrushforth marked this pull request as draft January 31, 2023 18:24
@openjdk openjdk bot added csr Need approved CSR to integrate pull request and removed rfr Ready for review labels Jan 31, 2023
@openjdk
Copy link

openjdk bot commented Jan 31, 2023

@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-8301302 with the correct fix version. This pull request cannot be integrated until the CSR request is approved.

@openjdk
Copy link

openjdk bot commented Jan 31, 2023

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

@andy-goryachev-oracle
Copy link
Contributor

In the context of adding theme support in javafx, I think this PR is a step in the right direction. I also like a small set of platform-independent properties like fg and bg colors. I wonder if the same approach can be extended for other aspects of platform L&F, like fonts, spacing, and may be other aspects (like, hiding scrollbars setting on Mac?)

I agree with @kevinrushforth - we'd need more discussion on the actual implementation. There are a few items that I feel are unnecessary, or might be done differently, and I'd like to learn what other people think.

Specifically:

  1. Appearance enum seems unnecessary - there might be more choices in a specific platform (Mac Ventura has three: dark/light/auto). Perhaps using fg/bg color intensity is sufficient to find out whether the overall theme is "dark" or "light".
  2. ObservableMap. Similarly to Node.getProperties(), I wonder if there might be a better way to observe the changes. May be a different metaphor (subscription?), like adding a value change listener to a specific key. We do need a set of keys (perhaps that can be an ObservableSet). Having said that, ObservableMap is good enough solution, and forgive me for stating the obvious, it should not initialize anything if the platform properties have not been requested by the application code.

What do you think?

@mstr2
Copy link
Collaborator Author

mstr2 commented Jan 31, 2023

In the context of adding theme support in javafx, I think this PR is a step in the right direction. I also like a small set of platform-independent properties like fg and bg colors. I wonder if the same approach can be extended for other aspects of platform L&F, like fonts, spacing, and may be other aspects (like, hiding scrollbars setting on Mac?)

That could indeed be a useful enhancement.

  1. Appearance enum seems unnecessary - there might be more choices in a specific platform (Mac Ventura has three: dark/light/auto). Perhaps using fg/bg color intensity is sufficient to find out whether the overall theme is "dark" or "light".

While dark/light mode can indeed be detected just by comparing foreground and background color, the reason for the Appearance enumeration and the Preferences.appearance property is to support dark window frames. In this gist (see "Stage appearance"), I've described how the stage appearance and platform appearance APIs interact.

By the way: the "auto" appearance of macOS is not actually a third appearance. It just swiches automatically from light to dark (and back) depending on the time of day. When a JavaFX application binds its stage apperance to the platform appearance (as described in the gist), this works as expected.

  1. ObservableMap. Similarly to Node.getProperties(), I wonder if there might be a better way to observe the changes. May be a different metaphor (subscription?), like adding a value change listener to a specific key. We do need a set of keys (perhaps that can be an ObservableSet). Having said that, ObservableMap is good enough solution, and forgive me for stating the obvious, it should not initialize anything if the platform properties have not been requested by the application code.

The use of ObservableMap is debatable.
I think that always initializing platform properties makes it easier to reason about the code. Saving the allocation of just a single map with 20-or-so key-value pairs is not a convincing reason to complicate the implementation, especially since most of the platform preferences implementation lives in the Glass toolkit, and not in the user-facing framework. Additionally, the built-in themes always susbcribe to platform preferences.

@swpalmer
Copy link
Collaborator

swpalmer commented Jan 31, 2023 via email

@andy-goryachev-oracle
Copy link
Contributor

andy-goryachev-oracle commented Jan 31, 2023 via email

@andy-goryachev-oracle
Copy link
Contributor

andy-goryachev-oracle commented Jan 31, 2023 via email

@andy-goryachev-oracle
Copy link
Contributor

mailing list ate a screenshot from the last message. here it is:

Screenshot 2023-01-31 at 15 10 38

@mstr2
Copy link
Collaborator Author

mstr2 commented Jan 31, 2023

I think that, by default, the FX frame decorations should pick up the platform theme (dark, light, accent color, etc.). It should be possible to override this behavior somehow - possibly by setting the base/accent color - but using an enum for this purpose feels unnecessary.

I think it shouldn't do that automatically, because the default themes do not support a dark mode out of the box. No theme will support multiple appearances without being specifically designed to do so.

If a theme supports multiple appearances, the "just use the platform appearance" behavior only requires a single line of code, which also takes care of macOS's "auto" appearance:

    var stage = new Stage();
    stage.appearanceProperty().bind(Platform.getPreferences().appearanceProperty());

By the way, Stage.appearance doesn't really have anything to do with StyleTheme. A style theme is the proposed collection of stylesheets that define the look of a JavaFX application, while the stage appearance only determines the appearance of native window decorations.

On Windows, the dark appearance adds the DWMA_USE_IMMERSIVE_DARK_MODE flag to the native window (see Support Dark and Light themes in Win32 apps), on macOS it chooses the "DarkAqua" appearance instead of the "Aqua" appearance (see Choosing a Specific Appearance for Your macOS App). There is no middle ground, it's either dark or it's not. Therefore I think the enumeration is a useful representation of this distinction.

@mlbridge
Copy link

mlbridge bot commented Feb 1, 2023

Mailing list message from Scott Palmer on openjfx-dev:

On Tue, Jan 31, 2023 at 6:15 PM Andy Goryachev <angorya at openjdk.org> wrote:

On Tue, 31 Jan 2023 23:04:50 GMT, Scott Palmer <swpalmer at openjdk.org>
wrote:

Is it necessary for any application to know about "auto"?

I actually don't know how the "auto" behaves. From the description it
seems the colors might actually change gradually throughout the day, thus
making an enum useless and necessitating the use of the actual colors.

From https://support.apple.com/en-ca/guide/mac-help/mchl52e1c2d2/mac

Auto just changes from light to dark based on the "Night Shift" setting,
which uses either a fixed schedule, or sunset. It doesn't have any "in
between" modes. Though it does animate the transition of the colors over
1/2 second or so.

The point being that as far as making the application aware of the current
state, auto isn't needed. As a preference for within the application
(force light or dark, or 'auto' = track the OS setting) it might be
useful. I would only include it if the Theme proposal is also going to
dynamically track the OS preference and switch Themes for you. It looks
like you have that covered by having the application bind to the Platform
Preferences value, in which case "auto" would be an application setting
rather than a JavaFX platform setting.

Scott
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.org/pipermail/openjfx-dev/attachments/20230131/20b1408b/attachment.htm>

@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!

@hjohn
Copy link
Collaborator

hjohn commented Apr 6, 2023

I think this would still be good to add to JavaFX as it will allow better integration with the platforms it runs on, without requiring the user to query native libraries to good integration.

@mstr2
Copy link
Collaborator Author

mstr2 commented Apr 7, 2023

  1. ObservableMap. Similarly to Node.getProperties(), I wonder if there might be a better way to observe the changes. May be a different metaphor (subscription?), like adding a value change listener to a specific key. We do need a set of keys (perhaps that can be an ObservableSet). Having said that, ObservableMap is good enough solution, and forgive me for stating the obvious, it should not initialize anything if the platform properties have not been requested by the application code.

I've pulled on that string a little more:

1. Optional API + listeners

This would remove the ObservableMap implementation, and add the following methods instead:

interface Preferences {
    ...
    void addListener(String key, InvalidationListener listener);
    void removeListener(String key, InvalidationListener listener);

    void addListener(String key, ChangeListener<?> listener);
    void removeListener(String key, ChangeListener<?> listener);

    Optional<Integer> getInteger(String key);
    Optional<Double> getDouble(String key);
    ...
}

I don't quite like this idea though, since it would allow developers to either add listeners to non-existent keys, or require developers to probe whether a key exists before adding a listener for it (maybe by also adding a boolean keyExists(String key) method. It also doesn't allow developers to enumerate the keys that are available on a given platform.
If we added a set of keys (maybe as an ObservableSet), the result is basically a map. We're better off implementing ObservableMap in this case.

2. Property API

interface Preferences {
    ...
    ReadOnlyIntegerProperty getIntegerProperty(String key);
    ReadOnlyDoubleProperty getDoubleProperty(String key);
    ...
}

With this idea, instead of having manual listener management and an API to query value mappings, we'd expose read-only properties for keys. We might also need a method like boolean keyExists(String key), but we can't enumerate all platform preferences with this approach.

3. Optional + Property API

interface Preferences {
    ...
    Optional<ReadOnlyIntegerProperty> getIntegerProperty(String key);
    Optional<ReadOnlyDoubleProperty> getDoubleProperty(String key);
    ...
}

This API combines all aspects into a single method call. We also can't enumerate the platform preferences.

I still think that implementing ObservableMap is preferable to all of these alternatives.

@mstr2
Copy link
Collaborator Author

mstr2 commented Apr 9, 2023

In general, platform preferences correspond to OS-level settings and are updated dynamically. Third-party themes might integrate platform preferences into their look and feel, which is often what users expect to see. But consider a scenario where an application uses a third-party theme that adapts to the OS appearance, but the application author only wants to support a dark appearance (independent from the OS appearance).

For this scenario, platform preferences should be overridable from application code. I've considered several potential approaches:

1. Provide two sets of preferences:

class Platform {
    Preferences getUserPreferences();
    Preferences getPlatformPreferences();
}

In this idea, getUserPreferences returns a mutable version of the immutable getPlatformPreferences. However, this requires theme authors to always use user preferences instead of platform preferences (or combine both); if they fail to do so, application authors are out of luck.

2. Add a separate, overridable property for each of the convenience API properties:

interface Preferences {
    ...
    ReadOnlyObjectProperty<Appearance> appearanceProperty();
    ObjectProperty<Appearance> appearanceOverrideProperty();
    ...
}

The value of the read-only appearanceProperty() then corresponds to the value of appearanceOverrideProperty() if the property is set to a non-null value. Otherwise, it corresponds to the platform-provided value.

3. Add a special setter for each of the convenience API properties:

interface Preferences {
    ...
    ReadOnlyObjectProperty<Appearance> appearanceProperty();
    void setAppearance(Appearance appearance);
    ...
}

The setAppearance method can be used to override the value of appearanceProperty(). When null is passed into this method, the platform default value is restored (or, put differently, the override is cleared). There is some precedence in JavaFX for a "special setter" pattern: for example, while the Stage.widthProperty() is read-only, Stage.setWidth is a special setter that attempts to set the stage width to the specified value (and may fail to do so).

I prefer the third option (the special setter), as it seems to be the cleanest approach that doesn't unnecessarily expand the API.

@hjohn
Copy link
Collaborator

hjohn commented Apr 9, 2023

  1. ObservableMap. Similarly to Node.getProperties(), I wonder if there might be a better way to observe the changes. May be a different metaphor (subscription?), like adding a value change listener to a specific key. We do need a set of keys (perhaps that can be an ObservableSet). Having said that, ObservableMap is good enough solution, and forgive me for stating the obvious, it should not initialize anything if the platform properties have not been requested by the application code.

I've pulled on that string a little more:

1. Optional API + listeners

This would remove the ObservableMap implementation, and add the following methods instead:

interface Preferences {
    ...
    void addListener(String key, InvalidationListener listener);
    void removeListener(String key, InvalidationListener listener);

    void addListener(String key, ChangeListener<?> listener);
    void removeListener(String key, ChangeListener<?> listener);

    Optional<Integer> getInteger(String key);
    Optional<Double> getDouble(String key);
    ...
}

I don't quite like this idea though, since it would allow developers to either add listeners to non-existent keys, or require developers to probe whether a key exists before adding a listener for it (maybe by also adding a boolean keyExists(String key) method.

This kind of functionality already exists, see for example Bindings#stringValueAt; you can bind a key of a map (even if it doesn't exist, in which case it will be null). It's provided as separate from ObservableMap, although it could have been integrated as well.

It also doesn't allow developers to enumerate the keys that are available on a given platform. If we added a set of keys (maybe as an ObservableSet), the result is basically a map. We're better off implementing ObservableMap in this case.

This API combines all aspects into a single method call. We also can't enumerate the platform preferences.

I still think that implementing ObservableMap is preferable to all of these alternatives.

I was on the fence about this before as well, but I think ObservableMap may be the way to go now. The thing that mainly irked me is all the mutation methods that will also be available, but I suppose that's inherent in the design of collections in Java.

@hjohn
Copy link
Collaborator

hjohn commented Apr 9, 2023

In general, platform preferences correspond to OS-level settings and are updated dynamically. Third-party themes might integrate platform preferences into their look and feel, which is often what users expect to see. But consider a scenario where an application uses a third-party theme that adapts to the OS appearance, but the application author only wants to support a dark appearance (independent from the OS appearance).

For this scenario, platform preferences should be overridable from application code. I've considered several potential approaches:

Would an approach similar to how Stylesheets do it be useful here? A property of a control can be overridden programmatically as well as set by CSS. In either case, the getter returns the current value (overridden or set by CSS). When set to a non-null value, the value takes precedence over CSS, and when set to null the CSS provided value takes over again.

With the preferences Map, this could work similar perhaps; set a key to whatever you want with put, and restore it to its original value by setting it to null.

@mstr2
Copy link
Collaborator Author

mstr2 commented Apr 10, 2023

With the preferences Map, this could work similar perhaps; set a key to whatever you want with put, and restore it to its original value by setting it to null.

That's how the current API works, with a little bit of added complexity:

interface Preferences {
    ...

    /**
     * Overrides the value of the {@link #appearanceProperty() appearance} property.
     * <p>
     * Specifying {@code null} clears the override, which restores the value of the
     * {@code appearance} property to the platform-provided value.
     * <p>
     * Calling this method does not update the {@code appearance} property instantaneously;
     * instead, the property is only updated after calling {@link #commit()}, or after the
     * occurrence of an operating system event that causes the {@code appearance} property
     * to be recomputed.
     *
     * @param appearance the platform appearance override, or {@code null} to clear the override
     */
    void setAppearance(Appearance appearance);

    ...

    /**
     * Overrides a key-value mapping.
     * <p>
     * If a platform-provided mapping for the key already exists, calling this method overrides
     * the value that is mapped to the key. If a platform-provided mapping for the key doesn't
     * exist, this method creates a new mapping.
     * <p>
     * Specifying a {@code null} value clears the override, which restores the value mapped to
     * the key to the platform-provided value. If the platform does not provide a mapping for
     * the specified key, the mapping is effectively removed.
     * <p>
     * Calling this method does not update the mapping instantaneously; instead, the mapping
     * is only updated after calling {@link #commit()}, or after the occurrence of an operating
     * system event that causes the mapped value to be recomputed.
     *
     * @param key the key
     * @param value the new value, or {@code null} to clear the override
     * @throws NullPointerException if {@code key} is null
     * @throws IllegalArgumentException if a platform-provided mapping for the key exists, and
     *                                  the specified value is an instance of a different class
     *                                  than the platform-provided value
     * @return the previous value associated with {@code key}
     */
    <T> T override(String key, T value);

    /**
     * Commits outstanding overridden preferences, which also causes the values of derived
     * properties to be recomputed.
     */
    void commit();
}

It is very likely the case that changing preferences can lead to very expensive operations in large real-world applications. For example, style themes or the entire user interface may be recreated, icons/images may be loaded, etc.

The Preferences implementation accounts for this by firing only a single invalidation notification, even when multiple platform preferences have changed. The documentation of Preferences contains guidance for users of this API:

     * @implNote In many cases, multiple platform preferences can change at the same time.
     *           For example, switching from light to dark mode changes various foreground elements to light
     *           colors, and various background elements to dark colors.
     *           <p>
     *           The {@code Preferences} implementation returned from this method fires a single invalidation
     *           event for such bulk changes. If a listener performs potentially heavy work, such as recreating
     *           and applying CSS themes, it might be beneficial to use {@link javafx.beans.InvalidationListener}
     *           instead of {@link javafx.collections.MapChangeListener} to prevent the listener from firing
     *           multiple times in bulk change scenarios.

This works when the changes originate from the OS, but it doesn't work when an application overrides preference mappings manually. ObservableMap doesn't support bulk changes, so repeatedly calling override(...) would end up firing multiple change notifications, and subscribers would have no way of knowing when the bulk change transaction has ended.

That's where the concept of uncommitted modifications comes into play: calling override(...) or any of the property setters like setAppearance(...) doesn't apply the changes immediately, it delays those changes until commit() is called or until an OS event causes the preference to be recomputed. When that happens, a single invalidation notification is fired, the same as it would have if the change originated from the OS.

@mstr2
Copy link
Collaborator Author

mstr2 commented Apr 19, 2023

@swpalmer @hjohn @andy-goryachev-oracle @kevinrushforth
I think that the Preferences API has evolved quite well. There were quite some discussions on the mailing list, as well as here on GitHub. Given that there haven't been new arguments in favor or against this feature in a while, I'd like to wrap up the discussion on whether we want this in JavaFX.

The main reason why I think this should go into core JavaFX is that it is adding a useful feature that can only be reasonably provided by the native windowing toolkits, and not by third-party libraries.

@hjohn
Copy link
Collaborator

hjohn commented Apr 24, 2023

With the preferences Map, this could work similar perhaps; set a key to whatever you want with put, and restore it to its original value by setting it to null.

This works when the changes originate from the OS, but it doesn't work when an application overrides preference mappings manually. ObservableMap doesn't support bulk changes, so repeatedly calling override(...) would end up firing multiple change notifications, and subscribers would have no way of knowing when the bulk change transaction has ended.

That's where the concept of uncommitted modifications comes into play: calling override(...) or any of the property setters like setAppearance(...) doesn't apply the changes immediately, it delays those changes until commit() is called or until an OS event causes the preference to be recomputed. When that happens, a single invalidation notification is fired, the same as it would have if the change originated from the OS.

I'm not convinced that a delayed change + commit system is the correct way to do this. Properties should behave the same everywhere in JavaFX and this seems to change how they work quite a bit.

Instead, I propose to look at how layout in JavaFX is handling this problem. Layout looks at thousands of properties, yet changing one or many of the involved properties does not involve an expensive layout recalculation per change. Instead, changes are tracked by marking certain aspects of the involved controls dirty. On the next pulse, the layout code notices that something that would influence layout and CSS decisions has changed, and performs the required changes. The properties involved are all normal properties, that can be changed quickly, reflect their current value immediately and that can be overridden by the user or reset back to defaults. There is no override or commit system needed.

Have you considered allowing users to change preference values directly, but not acting on those changes until the next pulse occurs? Users can still listen for keys/properties, just like they can for layout properties, but the major changes that involve recomputing CSS is only done once per pulse.

This would make it possible to change several preference values without penalty (which happens on the FX thread anyway, so pulses are on hold during that time), and they're automatically "committed" once the user is done on the FX thread and the next pulse fires. I think it would be a very good fit.

@mstr2
Copy link
Collaborator Author

mstr2 commented Apr 24, 2023

I'm not convinced that a delayed change + commit system is the correct way to do this. Properties should behave the same everywhere in JavaFX and this seems to change how they work quite a bit.

Instead, I propose to look at how layout in JavaFX is handling this problem. Layout looks at thousands of properties, yet changing one or many of the involved properties does not involve an expensive layout recalculation per change. Instead, changes are tracked by marking certain aspects of the involved controls dirty. On the next pulse, the layout code notices that something that would influence layout and CSS decisions has changed, and performs the required changes. The properties involved are all normal properties, that can be changed quickly, reflect their current value immediately and that can be overridden by the user or reset back to defaults. There is no override or commit system needed.

Have you considered allowing users to change preference values directly, but not acting on those changes until the next pulse occurs? Users can still listen for keys/properties, just like they can for layout properties, but the major changes that involve recomputing CSS is only done once per pulse.

This would make it possible to change several preference values without penalty (which happens on the FX thread anyway, so pulses are on hold during that time), and they're automatically "committed" once the user is done on the FX thread and the next pulse fires. I think it would be a very good fit.

I think this could work, but it also means giving up on instant change notifications. A call to setAppearance or override(key, value) will not instantly fire a change notification (after the code that modifies the properties is done), but delay it until the next pulse. Resetting the value to its original value before the next pulse would probably also elide the change notification entirely. It's basically the same as change+commit, but with an implicit commit in the next pulse.

@hjohn
Copy link
Collaborator

hjohn commented Apr 24, 2023

I'm not convinced that a delayed change + commit system is the correct way to do this. Properties should behave the same everywhere in JavaFX and this seems to change how they work quite a bit.
Instead, I propose to look at how layout in JavaFX is handling this problem. Layout looks at thousands of properties, yet changing one or many of the involved properties does not involve an expensive layout recalculation per change. Instead, changes are tracked by marking certain aspects of the involved controls dirty. On the next pulse, the layout code notices that something that would influence layout and CSS decisions has changed, and performs the required changes. The properties involved are all normal properties, that can be changed quickly, reflect their current value immediately and that can be overridden by the user or reset back to defaults. There is no override or commit system needed.
Have you considered allowing users to change preference values directly, but not acting on those changes until the next pulse occurs? Users can still listen for keys/properties, just like they can for layout properties, but the major changes that involve recomputing CSS is only done once per pulse.
This would make it possible to change several preference values without penalty (which happens on the FX thread anyway, so pulses are on hold during that time), and they're automatically "committed" once the user is done on the FX thread and the next pulse fires. I think it would be a very good fit.

I think this could work, but it also means giving up on instant change notifications. A call to setAppearance or override(key, value) will not instantly fire a change notification (after the code that modifies the properties is done), but delay it until the next pulse. Resetting the value to its original value before the next pulse would probably also elide the change notification entirely. It's basically the same as change+commit, but with an implicit commit in the next pulse.

That's not quite what I meant. You can add listeners still and get instant change notifications. Just like when I listen to the backgroundProperty of a control, and someone changes it, I get notified instantly and the change is reflected (in the property) instantly. Only when the next pulse fires is the change actually rendered.

I would think the same is possible with say the appearance property. When I change it from LIGHT to DARK, everyone interested gets this notification immediately. On the next pulse, the change is noticed and only then do we change the stylesheets or make other adjustments that are high impact. Basically, the computationally expensive stuff happens during a pulse; it could register invalidation listeners on properties of interest which just set a flag, that is checked and reset on the next pulse.

I'm not 100% sure, but it seems you want to add listeners to these properties yourself to instantly trigger the Theme updating code -- I'm saying, only set a boolean that they've changed, check it on the next pulse using Scene#addPreLayoutPulseListener (or perhaps there is another mechanism you can tap into internally), and then trigger the Theme change.

@mstr2
Copy link
Collaborator Author

mstr2 commented Dec 7, 2023

Note I get the preference Windows.SPI.HighContrastOn, which is not listed in the javadoc of javafx.application.Platform.Preferences, but I don't get these two preferences which are listed:

Windows.SPI.HighContrast
Windows.SPI.HighContrastColorScheme

The javadoc for Platform.Preferences indeed says Windows.SPI.HighContrast, while the implementation says Windows.SPI.HighContrastOn. I've changed the implementation to match the spec.

Copy link
Collaborator

@nlisker nlisker left a comment

Choose a reason for hiding this comment

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

There are classes such as PlatformPreferences, PreferenceProperties, and ColorSchemeProperty that are effectively singletons. Does it makes sense to just write them in a singleton pattern to avoid misuse?

Comment on lines 71 to 74
public static WindowsHighContrastScheme fromThemeName(String themeName) {
if (themeName == null) {
return null;
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

This method is called only from PlatformImpl that already does the null check on the the string. In general, null checks should be done on the "outer most layer" and then all the inner layers can rely on the value being non-null.

Is this method expected to be called from other places as well? If not, the method can be made package visible.

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 method now returns NONE when another constant doesn't apply. I've removed the public modifier as you've suggested.

Comment on lines +78 to +79
private final List<InvalidationListener> invalidationListeners = new CopyOnWriteArrayList<>();
private final List<MapChangeListener<? super String, Object>> mapChangeListeners = new CopyOnWriteArrayList<>();
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can these be modified concurrently? What is the need for CopyOnWriteArrayList?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

It's to prevent ConcurrentModificationException if a listener implementation adds or removes itself (or another listener).

*
* @since 22
*/
public interface Preferences extends ObservableMap<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.

Note that this will be the first case where a public API implementation of ObservableMap is not for a general use type, but for a specific use case. All previous implementations are:
MapBinding, MapExpression, MapProperty, MapPropertyBase, ReadOnlyMapProperty, ReadOnlyMapPropertyBase, ReadOnlyMapWrapper, SimpleMapProperty, and all are from the base module.

This might be fine, but consider composition here.

@kevinrushforth what do you think?

Copy link
Member

Choose a reason for hiding this comment

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

I think this is OK. Given what it is being used for, I'm satisfied with the API. One thing we could consider is an @implNote indicating that applications are not expected to implement this. This could be done as a follow-up.

* always available.
* <p>
* The following preferences are potentially available on the specified platforms:
* <table id="preferences-table">
Copy link
Collaborator

Choose a reason for hiding this comment

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

Long tables can benefit from alternating colored rows. This can be achieved with <table class="striped"> I think.

Copy link
Member

Choose a reason for hiding this comment

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

That might be a good enhancement for other tables as well (GraphicsContext comes to mind). I would recommend this as a follow-up.

Color getForegroundColor();

/**
* The accent color.
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think that this needs to explanation on what the accent color is.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I'll create a follow-up issue for that.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Here it is: JDK-8321573

/**
* The color used for background regions.
* <p>
* If the platform does not report a background color, this property defaults to {@code Color.WHITE}.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is there value in writing this sentence on every property if they specify the @defaultValue?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

It might make it a bit more clear that this is the default value even on platforms that don't even have this preference.

ColorScheme getColorScheme();

/**
* The color used for background regions.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Maybe "The color used for background of regions"?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

But not all regions are backgrounds in this sense.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I wonder if it's worth specifying the corresponding properties in JavaFX that match the ones in this class. For example, that the background property is used for Region#backgroundProperty(), and the foreground color is used for Shape#fillProperty() (or however TextField colors its text).

Copy link
Member

Choose a reason for hiding this comment

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

With the exception of the high-contrast theme on Windows, JavaFX doesn't make any use of these platform-specific properties. It's up to the application to do that.

* If the platform does not report a foreground color, this property defaults to {@code Color.BLACK}.
*
* @return the {@code foregroundColor} property
* @defaultValue {@code Color.BLACK}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is BLACK a good default? From what I remember, because some devices have a difficulty with pure black, some dark gray color is used instead.

Copy link
Member

Choose a reason for hiding this comment

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

I would expect this to be one of the properties that is set on all platforms anyway, but if not, I think this BLACK is a reasonable default.

Copy link
Member

@kevinrushforth kevinrushforth left a comment

Choose a reason for hiding this comment

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

With the latest set of changes, this looks good to me. I left a couple questions / minor comments (mostly for follow-up) and answered a couple of Nir's questions.

Comment on lines 777 to 779
if (highContrastScheme == null) {
return;
}
Copy link
Member

Choose a reason for hiding this comment

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

Minor: You could eliminate the null check if you defined a new "NONE" or "UNKNOWN" enum and restored the (no-op) default: on line 837. It's fine the way you have it, if you prefer.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I've added a NONE constant.

Comment on lines +79 to +80
for (WindowsHighContrastScheme item : values()) {
for (ResourceBundle resourceBundle : resourceBundles) {
Copy link
Member

Choose a reason for hiding this comment

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

Depending on how often this is called, might it be worth caching a Map<String,WindowsHighContrastScheme> whose keys are the OS theme names and values are the corresponding enum values? This could be a follow-up enhancement if it were deemed important enough, but maybe it doesn't matter.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This class will probably change a bit and be moved around if and when we add style themes, so I think we can revisit it then.

*
* @since 22
*/
public interface Preferences extends ObservableMap<String, Object> {
Copy link
Member

Choose a reason for hiding this comment

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

I think this is OK. Given what it is being used for, I'm satisfied with the API. One thing we could consider is an @implNote indicating that applications are not expected to implement this. This could be done as a follow-up.

* always available.
* <p>
* The following preferences are potentially available on the specified platforms:
* <table id="preferences-table">
Copy link
Member

Choose a reason for hiding this comment

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

That might be a good enhancement for other tables as well (GraphicsContext comes to mind). I would recommend this as a follow-up.

* @see javafx.application.Platform.Preferences#colorSchemeProperty()
* @since 22
*/
public enum ColorScheme {
Copy link
Member

Choose a reason for hiding this comment

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

I suspect this is limited to the current choices, but it's a good question.

Copy link
Member

Choose a reason for hiding this comment

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

With the exception of the high-contrast theme on Windows, JavaFX doesn't make any use of these platform-specific properties. It's up to the application to do that.

* If the platform does not report a foreground color, this property defaults to {@code Color.BLACK}.
*
* @return the {@code foregroundColor} property
* @defaultValue {@code Color.BLACK}
Copy link
Member

Choose a reason for hiding this comment

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

I would expect this to be one of the properties that is set on all platforms anyway, but if not, I think this BLACK is a reasonable default.

@jperedadnr
Copy link
Collaborator

Windows high contrast seems to work fine now.

However, I'm building your PR (./gradlew sdk) and running the test on Mac, but I get:

Exception in Application start method 
java.lang.reflect.InvocationTargetException
... 
Caused by: java.util.MissingResourceException: Can't find bundle for base name com/sun/glass/ui/win/themes, locale 
        at java.base/java.util.ResourceBundle.throwMissingResourceException(ResourceBundle.java:2045)
        at java.base/java.util.ResourceBundle.getBundleImpl(ResourceBundle.java:1683)
        at java.base/java.util.ResourceBundle.getBundleImpl(ResourceBundle.java:1575)
        at java.base/java.util.ResourceBundle.getBundleImpl(ResourceBundle.java:1549)
        at java.base/java.util.ResourceBundle.getBundle(ResourceBundle.java:932)
        at javafx.graphics@22-internal/com.sun.javafx.application.WindowsHighContrastScheme.lambda$static$0(WindowsHighContrastScheme.java:50)
        at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:197)
        at java.base/java.util.Spliterators$ArraySpliterator.forEachRemaining(Spliterators.java:992)
        at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:509)
        at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499)
        at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:575)
        at java.base/java.util.stream.AbstractPipeline.evaluateToArrayNode(AbstractPipeline.java:260)
        at java.base/java.util.stream.ReferencePipeline.toArray(ReferencePipeline.java:616)
        at java.base/java.util.stream.ReferencePipeline.toArray(ReferencePipeline.java:622)
        at java.base/java.util.stream.ReferencePipeline.toList(ReferencePipeline.java:627)
        at javafx.graphics@22-internal/com.sun.javafx.application.WindowsHighContrastScheme.<clinit>(WindowsHighContrastScheme.java:52)
        ... 14 more

Note that files (classes, resources) from com/sun/glass/ui/win/ are not added to the graphics jar when building for Linux or macOS (see build.gradle line 5103)

@kevinrushforth
Copy link
Member

Good catch. I hadn't tested the latest update on Mac or Linux this time.

Copy link
Member

@kevinrushforth kevinrushforth left a comment

Choose a reason for hiding this comment

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

Revoking my approval since the current patch is DOA on Mac and Linux.

Once fixed, I'll fire off a headful test build prior to approving it.

@mstr2
Copy link
Collaborator Author

mstr2 commented Dec 7, 2023

Windows high contrast seems to work fine now.

However, I'm building your PR (./gradlew sdk) and running the test on Mac, but I get:

Exception in Application start method 
java.lang.reflect.InvocationTargetException
... 
Caused by: java.util.MissingResourceException: Can't find bundle for base name com/sun/glass/ui/win/themes, locale 
        at java.base/java.util.ResourceBundle.throwMissingResourceException(ResourceBundle.java:2045)
        at java.base/java.util.ResourceBundle.getBundleImpl(ResourceBundle.java:1683)
        at java.base/java.util.ResourceBundle.getBundleImpl(ResourceBundle.java:1575)
        at java.base/java.util.ResourceBundle.getBundleImpl(ResourceBundle.java:1549)
        at java.base/java.util.ResourceBundle.getBundle(ResourceBundle.java:932)
        at javafx.graphics@22-internal/com.sun.javafx.application.WindowsHighContrastScheme.lambda$static$0(WindowsHighContrastScheme.java:50)
        at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:197)
        at java.base/java.util.Spliterators$ArraySpliterator.forEachRemaining(Spliterators.java:992)
        at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:509)
        at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499)
        at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:575)
        at java.base/java.util.stream.AbstractPipeline.evaluateToArrayNode(AbstractPipeline.java:260)
        at java.base/java.util.stream.ReferencePipeline.toArray(ReferencePipeline.java:616)
        at java.base/java.util.stream.ReferencePipeline.toArray(ReferencePipeline.java:622)
        at java.base/java.util.stream.ReferencePipeline.toList(ReferencePipeline.java:627)
        at javafx.graphics@22-internal/com.sun.javafx.application.WindowsHighContrastScheme.<clinit>(WindowsHighContrastScheme.java:52)
        ... 14 more

Note that files (classes, resources) from com/sun/glass/ui/win/ are not added to the graphics jar when building for Linux or macOS (see build.gradle line 5103)

I've changed this so that these resources are only queried on Windows.

@mstr2
Copy link
Collaborator Author

mstr2 commented Dec 7, 2023

There are classes such as PlatformPreferences, PreferenceProperties, and ColorSchemeProperty that are effectively singletons. Does it makes sense to just write them in a singleton pattern to avoid misuse?

If we add user-modifiable preferences, these won't be singletons.

Copy link
Collaborator

@jperedadnr jperedadnr left a comment

Choose a reason for hiding this comment

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

Manual test on macOS and Windows looks good now. I've left one comment

* <p>
* The high contrast feature may not be available on all platforms.
*/
enum WindowsHighContrastScheme {
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'd rather keep the old name: HighContrastScheme: It is in a non-platform specific package, and it applies to all platforms (even only with NONE).
You could add a comment about HIGH_CONTRAST_* enum constants being only available on Windows.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

But it never applied to all platforms, it reflects exactly the Windows implementation of high-contrast schemes. It will be moved once we add style themes.

Copy link
Collaborator

Choose a reason for hiding this comment

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

ok

Copy link
Member

@kevinrushforth kevinrushforth left a comment

Choose a reason for hiding this comment

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

I retested on all platforms. All green.

@openjdk
Copy link

openjdk bot commented Dec 7, 2023

@mstr2 This change now passes all automated pre-integration checks.

ℹ️ This project also has non-automated pre-integration requirements. Please see the file CONTRIBUTING.md for details.

After integration, the commit message for the final commit will be:

8301302: Platform preferences API

Reviewed-by: kcr, angorya, jpereda

You can use pull request commands such as /summary, /contributor and /issue to adjust it as needed.

At the time when this comment was updated there had been 33 new commits pushed to the master branch:

  • 2108ecf: 8282290: TextField Cursor Position one off
  • 43448dd: 8284544: [Win] Name-Property of Spinner cannot be changed
  • 09922d5: 8318388: Update libxslt to 1.1.39
  • aedc887: 8320359: ImageView: add styleable fitWidth, fitHeight, preserveRatio, smooth properties
  • 092d5d2: 8314597: Deprecate for removal protected access methods in converters
  • 036d81b: 8269921: TextFlow: listeners on bounds can throw NPE while computing text bounds
  • 0d33417: 8320267: WebView crashes on macOS 11 with WebKit 616.1
  • d65d8a6: 8316518: javafx.print.Paper getWidth / getHeight rounds values, causing errors.
  • c46c172: 8320773: [macOS] All IME input blocked
  • b80ec39: 8313648: JavaFX application continues to show a black screen after graphic card driver crash
  • ... and 23 more: https://git.openjdk.org/jfx/compare/9b93c962f45e5cf0b3a9f1090f307603be130a0e...master

As there are no conflicts, your changes will automatically be rebased on top of these commits when integrating. If you prefer to avoid this automatic rebasing, please check the documentation for the /integrate command for further details.

➡️ To integrate this PR with the above commit message to the master branch, type /integrate in a new comment.

@openjdk openjdk bot added the ready Ready to be integrated label Dec 7, 2023
@mstr2
Copy link
Collaborator Author

mstr2 commented Dec 7, 2023

Thanks to all the people who were involved in developing this feature and helping to bring this across the finish line, especially @andy-goryachev-oracle @kevinrushforth @hjohn @nlisker and @jperedadnr 👍

@mstr2
Copy link
Collaborator Author

mstr2 commented Dec 7, 2023

/integrate

@openjdk
Copy link

openjdk bot commented Dec 7, 2023

Going to push as commit 170a4c1.
Since your change was applied there have been 33 commits pushed to the master branch:

  • 2108ecf: 8282290: TextField Cursor Position one off
  • 43448dd: 8284544: [Win] Name-Property of Spinner cannot be changed
  • 09922d5: 8318388: Update libxslt to 1.1.39
  • aedc887: 8320359: ImageView: add styleable fitWidth, fitHeight, preserveRatio, smooth properties
  • 092d5d2: 8314597: Deprecate for removal protected access methods in converters
  • 036d81b: 8269921: TextFlow: listeners on bounds can throw NPE while computing text bounds
  • 0d33417: 8320267: WebView crashes on macOS 11 with WebKit 616.1
  • d65d8a6: 8316518: javafx.print.Paper getWidth / getHeight rounds values, causing errors.
  • c46c172: 8320773: [macOS] All IME input blocked
  • b80ec39: 8313648: JavaFX application continues to show a black screen after graphic card driver crash
  • ... and 23 more: https://git.openjdk.org/jfx/compare/9b93c962f45e5cf0b3a9f1090f307603be130a0e...master

Your commit was automatically rebased without conflicts.

@openjdk openjdk bot added the integrated Pull request has been integrated label Dec 7, 2023
@openjdk openjdk bot closed this Dec 7, 2023
@openjdk openjdk bot removed ready Ready to be integrated rfr Ready for review labels Dec 7, 2023
@openjdk
Copy link

openjdk bot commented Dec 7, 2023

@mstr2 Pushed as commit 170a4c1.

💡 You may see a message that your pull request was closed with unmerged commits. This can be safely ignored.

@nlisker
Copy link
Collaborator

nlisker commented Dec 8, 2023

On to phase 2 :)

@satsen
Copy link

satsen commented Dec 21, 2023

Thank you for your work @mstr2. It's nice to see something new and shiny added to JavaFX. I'm looking forward for your other PRs to get integrated as well, I really like your work. Thanks.

@mstr2 mstr2 deleted the feature/platform-preferences branch November 7, 2024 18:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
integrated Pull request has been integrated
Development

Successfully merging this pull request may close these issues.