JDK-8319138 : Platform preferences API
  • Type: CSR
  • Component: javafx
  • Sub-Component: graphics
  • Priority: P4
  • Status: Closed
  • Resolution: Approved
  • Fix Versions: jfx22
  • Submitted: 2023-10-30
  • Updated: 2023-11-27
  • Resolved: 2023-11-27
Related Reports
CSR :  
Description
Summary
-------
Add an API to JavaFX that exposes platform-specific preferences. A common subset of preferences is exposed in a platform-independent way.

Problem
-------
Platform preferences are the preferred UI settings of the operating system. For example, on Windows this includes the color values identified by the `Windows.UI.ViewManagement.UIColorType` enumeration; on macOS this includes the system color values of the `NSColor` class. Exposing these dynamic values to JavaFX applications allows developers to integrate their applications more closely with the operating system, for example by creating themes that match the platform's look and feel.

Solution
--------
Platform preferences are exposed as a read-only `ObservableMap` of platform-specific key-value pairs, which means that the preferences available on Windows are different from macOS or Linux. JavaFX provides a small, curated list of preferences that are available on most platforms, and are therefore exposed with a platform-independent API. Preference collection and change detection is implemented in the Glass toolkit.

Specification
-------------
Add the `javafx.application.ColorScheme` enumeration:

    /**
     * Defines the color scheme of the user interface, which specifies whether applications
     * should prefer light text on dark backgrounds, or dark text on light backgrounds.
     *
     * @see javafx.application.Platform.Preferences#colorSchemeProperty()
     * @since 22
     */
    public enum ColorScheme {
        /**
         * A light color scheme uses bright backgrounds and dark text.
         */
        LIGHT,

        /**
         * A dark color scheme uses dark backgrounds and bright text.
         */
        DARK
    }

Add the `javafx.application.Platform.Preferences` interface:

    /**
     * Contains UI preferences of the current platform.
     * <p>
     * {@code Preferences} extends {@link ObservableMap} to expose platform preferences as key-value pairs.
     * The map is unmodifiable, which means that keys and values cannot be added, removed, or updated.
     * Calling any mutator method on the map will always cause {@code UnsupportedOperationException} to be thrown.
     * However, the mappings will be updated by JavaFX when the operating system reports that a platform
     * preference has changed.
     * <p>
     * For convenience, {@link #getInteger}, {@link #getDouble}, {@link #getBoolean}, {@link #getString},
     * {@link #getColor}, and {@link #getValue} 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
     * and its current configuration, so applications should not assume that a particular preference is
     * always available.
     * <p>
     * The following preferences are potentially available on the specified platforms:
     * <table id="preferences-table">
     *     <caption>List of platform preferences</caption>
     *     <tbody>
     *         <tr><th colspan="2" scope="colgroup">Windows</th></tr>
     *         <tr><td>{@code Windows.SPI.HighContrast}</td><td>{@link Boolean}</td></tr>
     *         <tr><td>{@code Windows.SPI.HighContrastColorScheme}</td><td>{@link String}</td></tr>
     *         <tr><td>{@code Windows.SysColor.COLOR_3DFACE}</td><td>{@link Color}</td></tr>
     *         <tr><td>{@code Windows.SysColor.COLOR_BTNTEXT}</td><td>{@link Color}</td></tr>
     *         <tr><td>{@code Windows.SysColor.COLOR_GRAYTEXT}</td><td>{@link Color}</td></tr>
     *         <tr><td>{@code Windows.SysColor.COLOR_HIGHLIGHT}</td><td>{@link Color}</td></tr>
     *         <tr><td>{@code Windows.SysColor.COLOR_HIGHLIGHTTEXT}</td><td>{@link Color}</td></tr>
     *         <tr><td>{@code Windows.SysColor.COLOR_HOTLIGHT}</td><td>{@link Color}</td></tr>
     *         <tr><td>{@code Windows.SysColor.COLOR_WINDOW}</td><td>{@link Color}</td></tr>
     *         <tr><td>{@code Windows.SysColor.COLOR_WINDOWTEXT}</td><td>{@link Color}</td></tr>
     *         <tr><td>{@code Windows.UIColor.Background}</td><td>{@link Color}</td></tr>
     *         <tr><td>{@code Windows.UIColor.Foreground}</td><td>{@link Color}</td></tr>
     *         <tr><td>{@code Windows.UIColor.AccentDark3}</td><td>{@link Color}</td></tr>
     *         <tr><td>{@code Windows.UIColor.AccentDark2}</td><td>{@link Color}</td></tr>
     *         <tr><td>{@code Windows.UIColor.AccentDark1}</td><td>{@link Color}</td></tr>
     *         <tr><td>{@code Windows.UIColor.Accent}</td><td>{@link Color}</td></tr>
     *         <tr><td>{@code Windows.UIColor.AccentLight1}</td><td>{@link Color}</td></tr>
     *         <tr><td>{@code Windows.UIColor.AccentLight2}</td><td>{@link Color}</td></tr>
     *         <tr><td>{@code Windows.UIColor.AccentLight3}</td><td>{@link Color}</td></tr>
     *         <tr></tr>
     *         <tr><th colspan="2" scope="colgroup">macOS</th></tr>
     *         <tr><td>{@code macOS.NSColor.labelColor}</td><td>{@link Color}</td></tr>
     *         <tr><td>{@code macOS.NSColor.secondaryLabelColor}</td><td>{@link Color}</td></tr>
     *         <tr><td>{@code macOS.NSColor.tertiaryLabelColor}</td><td>{@link Color}</td></tr>
     *         <tr><td>{@code macOS.NSColor.quaternaryLabelColor}</td><td>{@link Color}</td></tr>
     *         <tr><td>{@code macOS.NSColor.textColor}</td><td>{@link Color}</td></tr>
     *         <tr><td>{@code macOS.NSColor.placeholderTextColor}</td><td>{@link Color}</td></tr>
     *         <tr><td>{@code macOS.NSColor.selectedTextColor}</td><td>{@link Color}</td></tr>
     *         <tr><td>{@code macOS.NSColor.textBackgroundColor}</td><td>{@link Color}</td></tr>
     *         <tr><td>{@code macOS.NSColor.selectedTextBackgroundColor}</td><td>{@link Color}</td></tr>
     *         <tr><td>{@code macOS.NSColor.keyboardFocusIndicatorColor}</td><td>{@link Color}</td></tr>
     *         <tr><td>{@code macOS.NSColor.unemphasizedSelectedTextColor}</td><td>{@link Color}</td></tr>
     *         <tr><td>{@code macOS.NSColor.unemphasizedSelectedTextBackgroundColor}</td><td>{@link Color}</td></tr>
     *         <tr><td>{@code macOS.NSColor.linkColor}</td><td>{@link Color}</td></tr>
     *         <tr><td>{@code macOS.NSColor.separatorColor}</td><td>{@link Color}</td></tr>
     *         <tr><td>{@code macOS.NSColor.selectedContentBackgroundColor}</td><td>{@link Color}</td></tr>
     *         <tr><td>{@code macOS.NSColor.unemphasizedSelectedContentBackgroundColor}</td><td>{@link Color}</td></tr>
     *         <tr><td>{@code macOS.NSColor.selectedMenuItemTextColor}</td><td>{@link Color}</td></tr>
     *         <tr><td>{@code macOS.NSColor.gridColor}</td><td>{@link Color}</td></tr>
     *         <tr><td>{@code macOS.NSColor.headerTextColor}</td><td>{@link Color}</td></tr>
     *         <tr><td>{@code macOS.NSColor.alternatingContentBackgroundColors}</td><td>{@link Color}{@code []}</td></tr>
     *         <tr><td>{@code macOS.NSColor.controlAccentColor}</td><td>{@link Color}</td></tr>
     *         <tr><td>{@code macOS.NSColor.controlColor}</td><td>{@link Color}</td></tr>
     *         <tr><td>{@code macOS.NSColor.controlBackgroundColor}</td><td>{@link Color}</td></tr>
     *         <tr><td>{@code macOS.NSColor.controlTextColor}</td><td>{@link Color}</td></tr>
     *         <tr><td>{@code macOS.NSColor.disabledControlTextColor}</td><td>{@link Color}</td></tr>
     *         <tr><td>{@code macOS.NSColor.selectedControlColor}</td><td>{@link Color}</td></tr>
     *         <tr><td>{@code macOS.NSColor.selectedControlTextColor}</td><td>{@link Color}</td></tr>
     *         <tr><td>{@code macOS.NSColor.alternateSelectedControlTextColor}</td><td>{@link Color}</td></tr>
     *         <tr><td>{@code macOS.NSColor.currentControlTint}</td><td>{@link String}</td></tr>
     *         <tr><td>{@code macOS.NSColor.windowBackgroundColor}</td><td>{@link Color}</td></tr>
     *         <tr><td>{@code macOS.NSColor.windowFrameTextColor}</td><td>{@link Color}</td></tr>
     *         <tr><td>{@code macOS.NSColor.underPageBackgroundColor}</td><td>{@link Color}</td></tr>
     *         <tr><td>{@code macOS.NSColor.findHighlightColor}</td><td>{@link Color}</td></tr>
     *         <tr><td>{@code macOS.NSColor.highlightColor}</td><td>{@link Color}</td></tr>
     *         <tr><td>{@code macOS.NSColor.shadowColor}</td><td>{@link Color}</td></tr>
     *         <tr><td>{@code macOS.NSColor.systemBlueColor}</td><td>{@link Color}</td></tr>
     *         <tr><td>{@code macOS.NSColor.systemBrownColor}</td><td>{@link Color}</td></tr>
     *         <tr><td>{@code macOS.NSColor.systemGrayColor}</td><td>{@link Color}</td></tr>
     *         <tr><td>{@code macOS.NSColor.systemGreenColor}</td><td>{@link Color}</td></tr>
     *         <tr><td>{@code macOS.NSColor.systemIndigoColor}</td><td>{@link Color}</td></tr>
     *         <tr><td>{@code macOS.NSColor.systemOrangeColor}</td><td>{@link Color}</td></tr>
     *         <tr><td>{@code macOS.NSColor.systemPinkColor}</td><td>{@link Color}</td></tr>
     *         <tr><td>{@code macOS.NSColor.systemPurpleColor}</td><td>{@link Color}</td></tr>
     *         <tr><td>{@code macOS.NSColor.systemRedColor}</td><td>{@link Color}</td></tr>
     *         <tr><td>{@code macOS.NSColor.systemTealColor}</td><td>{@link Color}</td></tr>
     *         <tr><td>{@code macOS.NSColor.systemYellowColor}</td><td>{@link Color}</td></tr>
     *         <tr></tr>
     *         <tr><th colspan="2" scope="colgroup">Linux</th></tr>
     *         <tr><td>{@code GTK.theme_name}</td><td>{@link String}</td></tr>
     *         <tr><td>{@code GTK.theme_fg_color}</td><td>{@link Color}</td></tr>
     *         <tr><td>{@code GTK.theme_bg_color}</td><td>{@link Color}</td></tr>
     *         <tr><td>{@code GTK.theme_base_color}</td><td>{@link Color}</td></tr>
     *         <tr><td>{@code GTK.theme_selected_bg_color}</td><td>{@link Color}</td></tr>
     *         <tr><td>{@code GTK.theme_selected_fg_color}</td><td>{@link Color}</td></tr>
     *         <tr><td>{@code GTK.theme_unfocused_fg_color}</td><td>{@link Color}</td></tr>
     *         <tr><td>{@code GTK.theme_unfocused_bg_color}</td><td>{@link Color}</td></tr>
     *         <tr><td>{@code GTK.theme_unfocused_base_color}</td><td>{@link Color}</td></tr>
     *         <tr><td>{@code GTK.theme_unfocused_selected_bg_color}</td><td>{@link Color}</td></tr>
     *         <tr><td>{@code GTK.theme_unfocused_selected_fg_color}</td><td>{@link Color}</td></tr>
     *         <tr><td>{@code GTK.insensitive_bg_color}</td><td>{@link Color}</td></tr>
     *         <tr><td>{@code GTK.insensitive_fg_color}</td><td>{@link Color}</td></tr>
     *         <tr><td>{@code GTK.insensitive_base_color}</td><td>{@link Color}</td></tr>
     *         <tr><td>{@code GTK.borders}</td><td>{@link Color}</td></tr>
     *         <tr><td>{@code GTK.unfocused_borders}</td><td>{@link Color}</td></tr>
     *         <tr><td>{@code GTK.warning_color}</td><td>{@link Color}</td></tr>
     *         <tr><td>{@code GTK.error_color}</td><td>{@link Color}</td></tr>
     *         <tr><td>{@code GTK.success_color}</td><td>{@link Color}</td></tr>
     *         <tr></tr>
     *     </tbody>
     * </table>
     *
     * @since 22
     */
    public interface Preferences extends ObservableMap<String, Object> {

        /**
         * The platform color scheme, which specifies whether applications should prefer light text on
         * dark backgrounds, or dark text on light backgrounds. The value of this property defaults to
         * {@link ColorScheme#LIGHT} if the platform does not report color preferences.
         *
         * @return the {@code colorScheme} property
         * @defaultValue {@link ColorScheme#LIGHT}
         */
        ReadOnlyObjectProperty<ColorScheme> colorSchemeProperty();

        ColorScheme getColorScheme();

        /**
         * The color used for background regions.
         * <p>
         * If the platform does not report a background color, this property defaults to {@code Color.WHITE}.
         *
         * @return the {@code backgroundColor} property
         * @defaultValue {@link Color#WHITE}
         */
        ReadOnlyObjectProperty<Color> backgroundColorProperty();

        Color getBackgroundColor();

        /**
         * The color used for foreground elements like text.
         * <p>
         * 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}
         */
        ReadOnlyObjectProperty<Color> foregroundColorProperty();

        Color getForegroundColor();

        /**
         * The accent color.
         * <p>
         * If the platform does not report an accent color, this property defaults to vivid blue
         * (corresponding to the hex color value {@code #157EFB}).
         *
         * @return the {@code accentColor} property
         * @defaultValue {@code #157EFB}
         */
        ReadOnlyObjectProperty<Color> accentColorProperty();

        Color getAccentColor();

        /**
         * Returns an optional {@code Integer} to which the specified key is mapped.
         *
         * @param key the key
         * @throws NullPointerException if {@code key} is null
         * @throws IllegalArgumentException if the key is not mappable to an {@code Integer}
         * @return the optional {@code Integer} to which the key is mapped
         */
        Optional<Integer> getInteger(String key);

        /**
         * Returns an optional {@code Double} to which the specified key is mapped.
         *
         * @param key the key
         * @throws NullPointerException if {@code key} is null
         * @throws IllegalArgumentException if the key is not mappable to a {@code Double}
         * @return the optional {@code Double} to which the key is mapped
         */
        Optional<Double> getDouble(String key);

        /**
         * Returns an optional {@code Boolean} to which the specified key is mapped.
         *
         * @param key the key
         * @throws NullPointerException if {@code key} is null
         * @throws IllegalArgumentException if the key is not mappable to a {@code Boolean}
         * @return the optional {@code Boolean} to which the key is mapped
         */
        Optional<Boolean> getBoolean(String key);

        /**
         * Returns an optional {@code String} to which the specified key is mapped.
         *
         * @param key the key
         * @throws NullPointerException if {@code key} is null
         * @throws IllegalArgumentException if the key is not mappable to a {@code String}
         * @return the optional {@code String} to which the key is mapped
         */
        Optional<String> getString(String key);

        /**
         * Returns an optional {@code Color} to which the specified key is mapped.
         *
         * @param key the key
         * @throws NullPointerException if {@code key} is null
         * @throws IllegalArgumentException if the key is not mappable to a {@code Color}
         * @return the optional {@code Color} instance to which the key is mapped
         */
        Optional<Color> getColor(String key);

        /**
         * Returns an optional value to which the specified key is mapped.
         *
         * @param <T> the type of the value
         * @param key the key
         * @param type the type of the value
         * @throws NullPointerException if {@code key} or {@code type} is null
         * @throws IllegalArgumentException if the key is not mappable to a value of type {@code T}
         * @return the optional value to which the key is mapped
         */
        <T> Optional<T> getValue(String key, Class<T> type);
    }

Add the `javafx.application.Platform.getPreferences()` method:

    /**
     * Gets the preferences of the current platform.
     * <p>
     * The map returned from this method is unmodifiable, which means that keys and values cannot
     * be added, removed, or updated. Calling any mutator method on the map will always cause
     * {@code UnsupportedOperationException} to be thrown. However, the mappings will be updated
     * by JavaFX when the operating system reports that a platform preference has changed.
     *
     * @return the {@code Preferences} instance
     * @see <a href="Platform.Preferences.html#preferences-table">List of platform preferences</a>
     * @since 22
     */
    public static Preferences getPreferences();
Comments
Moving to Approved.
27-11-2023

The latest version looks good. To answer one of Joe's questions: > I don't know if it would be prudent to provide more wiggle room for the set of properties that may be returned. For example, if there is a new version of an OS released and FX is updated to run on it, presumably the set of properties of interest could change? I had the same concern, so Michael added the suggested wiggle room. With this change, I am satisfied that we will be able to evolve the list of supported preferences without undue concern over compatibility. And now that Appearance has changed to be a more narrowly focused ColorScheme, I don't have any concerns about that one either. Minor: Regarding the Compatibility Risk, the current text describes possible concerns about our implementation misbehaving or having bugs, which isn't really the purview of the CSR. I think the only compatibility risks are the ones we've talked about for around evolution, and those would be risks of a future CSR (I think). So for *this* CSR, I think there really isn't any compatibility risk, since the only API change touching on existing classes is to add a static method to a final class.
18-11-2023

[~mstrauss], I'm more familiar with the details of the cross-release compatibility policies of the JDK as opposed to FX. If this kind of change were going into the JDK, we would have concerns about adding or removing items from the set of preferences in an update release vs a feature release, and in some cases even removing items in a feature release. However, FX has different policies so if any changes you'd might want to make to the set of preferences are within policy and fine by your users, then that is fine for this CSR too. As a stylistic point, it would also be acceptable to have a general "Throws NPE on null argument unless otherwise specified" disclaimer at the type level as opposed to repeating that for every method.
13-11-2023

@cdea This enhancement is a purely informational API, and does not include any new user-facing features like background blur. If a background blur feature were to be developed, and it needed additional information from the OS, the API proposed in this enhancement would easily allow the required information to be exposed to the JavaFX application.
13-11-2023

A user created a request in regards to window & background blur capability. I feel they might be related to this request in terms of the appearance & preferences: https://bugs.openjdk.org/browse/JDK-8305116 Are they related?
12-11-2023

[~mstrauss] I'll formally Review this after completing the review of the API documentation in the PR. Can you add a short risk summary (I agree that the risk is minimal since the only additions are two new types and a new static method in an existing class that is final).
11-11-2023

1. The term `Appearance` might have been too broad. I've renamed the enumeration to `ColorScheme`, which is also the term the CSS specification uses for the same concept (light-on-dark vs. dark-on-light flag). A future enhancement is now free to define `Appearance` to encompass more than the color scheme. 2. What do you mean by allowing more wiggle room? It would be quite easy to add more preferences (for future OS versions) to the list of preferences that is already specified. Note that this list is only exhaustive for the specified toolkits (Windows, macOS, Linux). Other platform toolkits might return different preferences (or none at all).
10-11-2023

Moving to Provisional, not Approved. For the Appearance enum, having the enum implement and interface and using the interface type in the rest of the API may provide more flexibility. I don't know if it would be prudent to provide more wiggle room for the set of properties that may be returned. For example, if there is a new version of an OS released and FX is updated to run on it, presumably the set of properties of interest could change?
10-11-2023