RFR: 8301302: Platform preferences API [v3]

Michael Strauß mstrauss at openjdk.org
Mon Jul 24 17:45:45 UTC 2023


On Mon, 24 Apr 2023 22:08:20 GMT, Andy Goryachev <angorya at openjdk.org> wrote:

> I meant
> 
> ```
> ObjectProperty<?> Preferences.getProperty(String key);
> Set<String> listPropertyKeys();
> ```
> 
> it would also be nice to obtain a typed property right away. That might be more difficult, since one needs to use a dedicated class for the key what contains the value type, which might be clunky.

I discussed this briefly in a previous [comment](https://github.com/openjdk/jfx/pull/1014#issuecomment-1500535347). The problem with this idea is that you need to do two things: first, check whether a given preference is available on the current platform, and second, get the property for the key.

What happens if you try to retrieve a property for a key that's not available? Do you get an exception? Do you get a property implementation that holds a default value? What if you want to control the default value?

The `Optional` approach, as currently proposed, solves this problem with an API that Java users already know. For example, if you are targeting macOS and you are interested in the `macOS.NSColor.findHighlightColor` preference, which is only available starting with macOS 10.13, you could provide a suitable fallback like this:

var findHighlightColor = Platform.getPreferences()
    .getColor("macOS.NSColor.findHighlightColor")
    .orElse(Color.BLUE);

> Map.containsValue() would have to resolve values for all the keys. Is this expected? If the code already resolves all values, I presume on the first call to `getPreferences`, the Map is probably ok.
> 
> Does querying for all the values take long? Any possibility of getting blocked waiting for something?

Yes, the `Map` contract holds, which is why all values will have to be resolved by the time you're calling `Map.containsValue()`. From an API perspective, it is unspecified how that works, i.e. whether values are lazily or eagerly resolved. From an implementation perspective, all values are queried on application startup, and after that the values are updated as updates come in.

For example, in the Windows toolkit, this works by intercepting several events in the main window loop, including `WM_SETTINGCHANGE`, `WM_THEMECHANGED`, or `WM_SYSCOLORCHANGE`. On macOS, the native toolkit subscribes to the `AppleColorPreferencesChangedNotification` and `AppleInterfaceThemeChangedNotification` events. After receiving one of these events, the updated values are sent back to the `PlatformImpl` class, where they are integrated into the `Preferences` map.

The event-based approach of the current implementation doesn't run any risk of blocking calls.

> > What I've been calling `Appearance` in this PR is _one_ of those knobs.
> 
> I think we are on the same page here.
> 
> My point here is that, from the API design standpoint, it is better to think ahead and consider controlling multiple attributes instead of a singularly invented Appearance enum. Why not have `Stage.setAttributes(Set)` instead of `setAppearance()` then?
> 
> We can still have an Apperance enum, but this time it will be just one of many possible attributes. it could be platform-independent if we agree that it's well defined, or it could be platform-specific like DWMWA_USE_IMMERSIVE_DARK_MODE.

There's a reason why `Appearance` is kind of singled out among all of the potential other knobs, and that is because it represents a concept that interacts very strongly with the upcoming Style Themes feature.

In total, I propose three PRs:
[JDK-8301302: Platform preferences API](https://bugs.openjdk.org/browse/JDK-8301302)
[JDK-8301303: Allow Stage to control the appearance of window decorations](https://bugs.openjdk.org/browse/JDK-8301303)
[JDK-8267546: Add CSS themes as a first-class concept](https://bugs.openjdk.org/browse/JDK-8267546)

`Appearance` is central to _all_ of those features, as it encodes the "light mode or dark mode" knob. This is the most significant knob that users will interact with, as it represents:
1. An OS-wide system setting
2. The general "light or dark" appearance of window borders
3. The colors used in application stylesheets, icons, images, etc.

For a style theme author, this bit of information is central, as the entire look and feel of the theme will be designed around it. Most likely there will be at least two hand-picked sets of colors for a style theme depending on whether light or dark mode is enabled.

I don't think we'll want the style theme author to bother figuring out whether some obscure platform-specific `DWMWA_USE_IMMERSIVE_DARK_MODE` flag is set, or whether the current macOS appearance is named `NSAppearanceNameDarkAqua`. So we need a platform-independent way of encoding this bit, and `Appearance` is pretty easy to use for style theme authors:

// Note that a real implementation would need to subscribe to Appearance changes,
// and update the stylesheets accordingly
public class MyTheme implements StyleTheme {
    public MyTheme() {
        if (Platform.getPreferences().getAppearance() == Appearance.LIGHT) {
            // compose light stylesheets
        } else {
            // compose dark stylesheets
        }
    }
}


Note that the water actually gets a bit muddy when you factor in accessibility themes. Is a high-contrast accessibility theme a separate appearance? What about an inverted color mode? My best answer to this is: no, it's not.

On macOS, inverted color mode works without the application knowing about it. On Windows, accessibility themes certainly don't work without the application knowing about it, so a theme that works with high-contrast accessibility needs to monitor several very platform-specific keys, and then integrate those special colors into the theme. That's not something that we can easily represent or condense down, or even represent in a platform-agnostic way.

I agree with you that we should carefully plan ahead and see how we might extend this in the future to control more knobs. The `Preferences` API is not the right place for this, since `Preferences` can only query settings from the OS, it cannot change them.

My current thinking on this is that there are three categories of knobs:
1. Knobs that significantly affect the window border *and* the window content
2. Knobs that significantly affect *only* the window border
3. Knobs that affect OS-specific details of the window border

The first category is represented by `Appearance`, that's why it gets to have its own enumeration.
The second category is represented by `StageStyle`, because we have broad support on _most_ operating systems.
The third category is currently unrepresented, and it includes knobs like `DWM_WINDOW_CORNER_PREFERENCE`, which is only available on Windows 11.

I can imagine that we could have something like a `Stage.hints` property, where developers can specify those cat3 knobs. The name "hint" strongly suggests that we're talking about optional things. Yes, an app developer might want to have rounded window corners. But when the app runs on Windows 10, there simply won't be rounded corners, and that's not a big deal.

-------------

PR Comment: https://git.openjdk.org/jfx/pull/1014#issuecomment-1520924295
PR Comment: https://git.openjdk.org/jfx/pull/1014#issuecomment-1520948693
PR Comment: https://git.openjdk.org/jfx/pull/1014#issuecomment-1520968329


More information about the openjfx-dev mailing list