From 076b4018de1a6fd659778b77d66d2478def315a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Strau=C3=9F?= Date: Tue, 22 Oct 2024 16:07:16 +0000 Subject: [PATCH] 8341514: Add reducedMotion and reducedTransparency preferences Reviewed-by: kcr, angorya --- .../java/com/sun/glass/ui/Application.java | 10 +- .../com/sun/glass/ui/gtk/GtkApplication.java | 12 +- .../com/sun/glass/ui/mac/MacApplication.java | 15 +- .../com/sun/glass/ui/win/WinApplication.java | 15 +- .../sun/javafx/application/PlatformImpl.java | 3 +- .../preferences/PlatformPreferences.java | 30 +++- .../preferences/PreferenceMapping.java | 63 ++++++++ .../preferences/PreferenceProperties.java | 139 ++++++++++-------- .../java/javafx/application/Platform.java | 36 ++++- .../native-glass/gtk/GlassApplication.cpp | 8 +- .../main/native-glass/gtk/PlatformSupport.cpp | 18 ++- .../main/native-glass/gtk/PlatformSupport.h | 5 + .../main/native-glass/mac/GlassApplication.m | 6 + .../main/native-glass/mac/PlatformSupport.m | 21 +++ .../native-glass/win/GlassApplication.cpp | 7 +- .../main/native-glass/win/PlatformSupport.cpp | 52 ++++++- .../main/native-glass/win/PlatformSupport.h | 11 +- .../preferences/PlatformPreferencesTest.java | 46 +++++- .../PlatformPreferencesChangedTest.java | 8 +- 19 files changed, 396 insertions(+), 109 deletions(-) create mode 100644 modules/javafx.graphics/src/main/java/com/sun/javafx/application/preferences/PreferenceMapping.java diff --git a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/Application.java b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/Application.java index 8d5f981030c..192ccd7e651 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/Application.java +++ b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/Application.java @@ -27,6 +27,7 @@ import com.sun.glass.events.KeyEvent; import com.sun.glass.ui.CommonDialogs.ExtensionFilter; import com.sun.glass.ui.CommonDialogs.FileChooserResult; +import com.sun.javafx.application.preferences.PreferenceMapping; import java.io.File; import java.nio.ByteBuffer; @@ -771,22 +772,25 @@ public Map getPlatformPreferences() { } /** - * Returns a map of platform-specific keys to platform-independent keys defined by JavaFX. + * Returns a map of platform-specific keys to platform-independent keys defined by JavaFX, including a + * function that maps the platform-specific value to the platform-independent value. *

* For example, the platform-specific key "Windows.UIColor.Foreground" is mapped to the key "foregroundColor", * which makes it easier to write shared code without depending on platform-specific details. *

- * The following platform-independent keys are currently supported, which correspond to the names of color + * The following platform-independent keys are currently supported, which correspond to the names of * properties on the {@link com.sun.javafx.application.preferences.PreferenceProperties} class: *

* * @return a map of platform-specific keys to well-known keys */ - public Map getPlatformKeyMappings() { + public Map> getPlatformKeyMappings() { return Map.of(); } diff --git a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/gtk/GtkApplication.java b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/gtk/GtkApplication.java index fb1a1f8797d..22771407f28 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/gtk/GtkApplication.java +++ b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/gtk/GtkApplication.java @@ -36,6 +36,7 @@ import com.sun.glass.ui.Timer; import com.sun.glass.ui.View; import com.sun.glass.ui.Window; +import com.sun.javafx.application.preferences.PreferenceMapping; import com.sun.javafx.util.Logging; import com.sun.glass.utils.NativeLibLoader; import com.sun.prism.impl.PrismSettings; @@ -472,10 +473,12 @@ protected boolean _supportsInputMethods() { public native Map getPlatformPreferences(); @Override - public Map getPlatformKeyMappings() { + public Map> getPlatformKeyMappings() { return Map.of( - "GTK.theme_fg_color", "foregroundColor", - "GTK.theme_bg_color", "backgroundColor" + "GTK.theme_fg_color", new PreferenceMapping<>("foregroundColor", Color.class), + "GTK.theme_bg_color", new PreferenceMapping<>("backgroundColor", Color.class), + "GTK.theme_selected_bg_color", new PreferenceMapping<>("accentColor", Color.class), + "GTK.enable_animations", new PreferenceMapping<>("reducedMotion", Boolean.class, b -> !b) ); } @@ -501,7 +504,8 @@ public Map> getPlatformKeys() { Map.entry("GTK.unfocused_borders", Color.class), Map.entry("GTK.warning_color", Color.class), Map.entry("GTK.error_color", Color.class), - Map.entry("GTK.success_color", Color.class) + Map.entry("GTK.success_color", Color.class), + Map.entry("GTK.enable_animations", Boolean.class) ); } } diff --git a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/mac/MacApplication.java b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/mac/MacApplication.java index fedd40bef09..e2199814195 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/mac/MacApplication.java +++ b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/mac/MacApplication.java @@ -28,6 +28,7 @@ import com.sun.glass.ui.*; import com.sun.glass.ui.CommonDialogs.ExtensionFilter; import com.sun.glass.ui.CommonDialogs.FileChooserResult; +import com.sun.javafx.application.preferences.PreferenceMapping; import com.sun.javafx.util.Logging; import javafx.scene.paint.Color; @@ -439,11 +440,13 @@ public String getDataDirectory() { public native Map getPlatformPreferences(); @Override - public Map getPlatformKeyMappings() { + public Map> getPlatformKeyMappings() { return Map.of( - "macOS.NSColor.textColor", "foregroundColor", - "macOS.NSColor.textBackgroundColor", "backgroundColor", - "macOS.NSColor.controlAccentColor", "accentColor" + "macOS.NSColor.textColor", new PreferenceMapping<>("foregroundColor", Color.class), + "macOS.NSColor.textBackgroundColor", new PreferenceMapping<>("backgroundColor", Color.class), + "macOS.NSColor.controlAccentColor", new PreferenceMapping<>("accentColor", Color.class), + "macOS.NSWorkspace.accessibilityDisplayShouldReduceMotion", new PreferenceMapping<>("reducedMotion", Boolean.class), + "macOS.NSWorkspace.accessibilityDisplayShouldReduceTransparency", new PreferenceMapping<>("reducedTransparency", Boolean.class) ); } @@ -496,7 +499,9 @@ public Map> getPlatformKeys() { Map.entry("macOS.NSColor.systemPurpleColor", Color.class), Map.entry("macOS.NSColor.systemRedColor", Color.class), Map.entry("macOS.NSColor.systemTealColor", Color.class), - Map.entry("macOS.NSColor.systemYellowColor", Color.class) + Map.entry("macOS.NSColor.systemYellowColor", Color.class), + Map.entry("macOS.NSWorkspace.accessibilityDisplayShouldReduceMotion", Boolean.class), + Map.entry("macOS.NSWorkspace.accessibilityDisplayShouldReduceTransparency", Boolean.class) ); } diff --git a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/win/WinApplication.java b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/win/WinApplication.java index 2c7b3ff7c2c..9ab914a910d 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/glass/ui/win/WinApplication.java +++ b/modules/javafx.graphics/src/main/java/com/sun/glass/ui/win/WinApplication.java @@ -27,6 +27,7 @@ import com.sun.glass.ui.*; import com.sun.glass.ui.CommonDialogs.ExtensionFilter; import com.sun.glass.ui.CommonDialogs.FileChooserResult; +import com.sun.javafx.application.preferences.PreferenceMapping; import com.sun.prism.impl.PrismSettings; import com.sun.javafx.tk.Toolkit; import javafx.scene.paint.Color; @@ -374,11 +375,13 @@ public String getDataDirectory() { // This list needs to be kept in sync with PlatformSupport.cpp in the Glass toolkit for Windows. @Override - public Map getPlatformKeyMappings() { + public Map> getPlatformKeyMappings() { return Map.of( - "Windows.UIColor.Foreground", "foregroundColor", - "Windows.UIColor.Background", "backgroundColor", - "Windows.UIColor.Accent", "accentColor" + "Windows.UIColor.Foreground", new PreferenceMapping<>("foregroundColor", Color.class), + "Windows.UIColor.Background", new PreferenceMapping<>("backgroundColor", Color.class), + "Windows.UIColor.Accent", new PreferenceMapping<>("accentColor", Color.class), + "Windows.UISettings.AdvancedEffectsEnabled", new PreferenceMapping<>("reducedTransparency", Boolean.class, b -> !b), + "Windows.SPI.ClientAreaAnimation", new PreferenceMapping<>("reducedMotion", Boolean.class, b -> !b) ); } @@ -388,6 +391,7 @@ public Map> getPlatformKeys() { return Map.ofEntries( Map.entry("Windows.SPI.HighContrast", Boolean.class), Map.entry("Windows.SPI.HighContrastColorScheme", String.class), + Map.entry("Windows.SPI.ClientAreaAnimation", Boolean.class), Map.entry("Windows.SysColor.COLOR_3DFACE", Color.class), Map.entry("Windows.SysColor.COLOR_BTNTEXT", Color.class), Map.entry("Windows.SysColor.COLOR_GRAYTEXT", Color.class), @@ -404,7 +408,8 @@ public Map> getPlatformKeys() { Map.entry("Windows.UIColor.Accent", Color.class), Map.entry("Windows.UIColor.AccentLight1", Color.class), Map.entry("Windows.UIColor.AccentLight2", Color.class), - Map.entry("Windows.UIColor.AccentLight3", Color.class) + Map.entry("Windows.UIColor.AccentLight3", Color.class), + Map.entry("Windows.UISettings.AdvancedEffectsEnabled", Boolean.class) ); } } diff --git a/modules/javafx.graphics/src/main/java/com/sun/javafx/application/PlatformImpl.java b/modules/javafx.graphics/src/main/java/com/sun/javafx/application/PlatformImpl.java index 4be457a2df2..2b2d79c3726 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/javafx/application/PlatformImpl.java +++ b/modules/javafx.graphics/src/main/java/com/sun/javafx/application/PlatformImpl.java @@ -28,6 +28,7 @@ import static com.sun.javafx.FXPermissions.CREATE_TRANSPARENT_WINDOW_PERMISSION; import com.sun.javafx.PlatformUtil; import com.sun.javafx.application.preferences.PlatformPreferences; +import com.sun.javafx.application.preferences.PreferenceMapping; import com.sun.javafx.css.StyleManager; import com.sun.javafx.tk.TKListener; import com.sun.javafx.tk.TKStage; @@ -1013,7 +1014,7 @@ public static PlatformPreferences getPlatformPreferences() { * @param preferences the initial set of platform preferences */ public static void initPreferences(Map> platformKeys, - Map platformKeyMappings, + Map> platformKeyMappings, Map preferences) { platformPreferences = new PlatformPreferences(platformKeys, platformKeyMappings); platformPreferences.update(preferences); diff --git a/modules/javafx.graphics/src/main/java/com/sun/javafx/application/preferences/PlatformPreferences.java b/modules/javafx.graphics/src/main/java/com/sun/javafx/application/preferences/PlatformPreferences.java index dbb396d269c..286c56ad9aa 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/javafx/application/preferences/PlatformPreferences.java +++ b/modules/javafx.graphics/src/main/java/com/sun/javafx/application/preferences/PlatformPreferences.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2023, 2024, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -29,6 +29,7 @@ import javafx.application.ColorScheme; import javafx.application.Platform; import javafx.beans.InvalidationListener; +import javafx.beans.property.ReadOnlyBooleanProperty; import javafx.beans.property.ReadOnlyObjectProperty; import javafx.collections.MapChangeListener; import javafx.scene.paint.Color; @@ -50,7 +51,7 @@ * When the operating system signals that a preference has changed, the mappings are updated * by calling the {@link #update(Map)} method. */ -public class PlatformPreferences extends AbstractMap implements Platform.Preferences { +public final class PlatformPreferences extends AbstractMap implements Platform.Preferences { /** * Contains mappings from platform-specific keys to their types. This information is @@ -63,7 +64,7 @@ public class PlatformPreferences extends AbstractMap implements * Contains mappings from platform-specific keys to well-known keys, which are used * in the implementation of the property-based API in {@link PreferenceProperties}. */ - private final Map platformKeyMappings; + private final Map> platformKeyMappings; /** * Contains the current set of effective preferences, i.e. the set of preferences that @@ -86,7 +87,8 @@ public class PlatformPreferences extends AbstractMap implements * @throws NullPointerException if {@code platformKeys} or {@code platformKeyMappings} is {@code null} or * contains {@code null} keys or values */ - public PlatformPreferences(Map> platformKeys, Map platformKeyMappings) { + public PlatformPreferences(Map> platformKeys, + Map> platformKeyMappings) { this.platformKeys = Map.copyOf(platformKeys); this.platformKeyMappings = Map.copyOf(platformKeyMappings); } @@ -201,6 +203,26 @@ public Optional getColor(String key) { return getValue(key, Color.class); } + @Override + public ReadOnlyBooleanProperty reducedMotionProperty() { + return properties.reducedMotionProperty(); + } + + @Override + public boolean isReducedMotion() { + return properties.isReducedMotion(); + } + + @Override + public ReadOnlyBooleanProperty reducedTransparencyProperty() { + return properties.reducedTransparencyProperty(); + } + + @Override + public boolean isReducedTransparency() { + return properties.isReducedTransparency(); + } + @Override public ReadOnlyObjectProperty colorSchemeProperty() { return properties.colorSchemeProperty(); diff --git a/modules/javafx.graphics/src/main/java/com/sun/javafx/application/preferences/PreferenceMapping.java b/modules/javafx.graphics/src/main/java/com/sun/javafx/application/preferences/PreferenceMapping.java new file mode 100644 index 00000000000..bb1275b768c --- /dev/null +++ b/modules/javafx.graphics/src/main/java/com/sun/javafx/application/preferences/PreferenceMapping.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.sun.javafx.application.preferences; + +import com.sun.javafx.util.Logging; +import java.util.Objects; +import java.util.function.Function; + +/** + * A mapping from platform-specific keys to platform-independent keys defined by JavaFX, including a + * function that maps the platform-specific value to the platform-independent value. + */ +public record PreferenceMapping(String keyName, Class valueType, Function valueMapper) { + + public PreferenceMapping { + Objects.requireNonNull(keyName, "keyName cannot be null"); + Objects.requireNonNull(valueType, "valueType cannot be null"); + Objects.requireNonNull(valueMapper, "valueMapper cannot be null"); + } + + public PreferenceMapping(String keyName, Class valueType) { + this(keyName, valueType, Function.identity()); + } + + @SuppressWarnings("unchecked") + public T map(Object value) { + if (valueType.isInstance(value)) { + return valueMapper.apply((T)value); + } + + if (value != null) { + Logging.getJavaFXLogger().warning( + "Unexpected value of " + keyName + " platform preference, " + + "using default value instead (expected = " + valueType.getName() + + ", actual = " + value.getClass().getName() + ")"); + } + + return null; + } +} diff --git a/modules/javafx.graphics/src/main/java/com/sun/javafx/application/preferences/PreferenceProperties.java b/modules/javafx.graphics/src/main/java/com/sun/javafx/application/preferences/PreferenceProperties.java index a119153ba31..f556f1fff7a 100644 --- a/modules/javafx.graphics/src/main/java/com/sun/javafx/application/preferences/PreferenceProperties.java +++ b/modules/javafx.graphics/src/main/java/com/sun/javafx/application/preferences/PreferenceProperties.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2023, 2024, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,16 +25,17 @@ package com.sun.javafx.application.preferences; -import com.sun.javafx.util.Logging; import com.sun.javafx.util.Utils; import javafx.application.ColorScheme; import javafx.beans.InvalidationListener; import javafx.beans.property.Property; +import javafx.beans.property.ReadOnlyBooleanProperty; +import javafx.beans.property.ReadOnlyBooleanWrapper; import javafx.beans.property.ReadOnlyObjectProperty; import javafx.beans.property.ReadOnlyObjectPropertyBase; import javafx.beans.property.ReadOnlyObjectWrapper; import javafx.scene.paint.Color; -import java.util.List; +import java.util.HashMap; import java.util.Map; import java.util.Objects; @@ -43,15 +44,49 @@ */ final class PreferenceProperties { - private final ColorProperty backgroundColor = new ColorProperty("backgroundColor", Color.WHITE); - private final ColorProperty foregroundColor = new ColorProperty("foregroundColor", Color.BLACK); - private final ColorProperty accentColor = new ColorProperty("accentColor", Color.rgb(21, 126, 251)); - private final List allColors = List.of(backgroundColor, foregroundColor, accentColor); + private final Map> deferredProperties = new HashMap<>(); + private final DeferredProperty backgroundColor = new DeferredProperty<>("backgroundColor", Color.WHITE); + private final DeferredProperty foregroundColor = new DeferredProperty<>("foregroundColor", Color.BLACK); + private final DeferredProperty accentColor = new DeferredProperty<>("accentColor", Color.rgb(21, 126, 251)); private final ColorSchemeProperty colorScheme = new ColorSchemeProperty(); + private final DeferredProperty reducedMotion = new DeferredProperty<>("reducedMotion", false); + private final DeferredProperty reducedTransparency = new DeferredProperty<>("reducedTransparency", false); + private final ReadOnlyBooleanWrapper reducedMotionFlag; + private final ReadOnlyBooleanWrapper reducedTransparencyFlag; private final Object bean; PreferenceProperties(Object bean) { this.bean = bean; + + reducedMotionFlag = new ReadOnlyBooleanWrapper(bean, reducedMotion.getName()); + reducedMotionFlag.bind(reducedMotion); + + reducedTransparencyFlag = new ReadOnlyBooleanWrapper(bean, reducedTransparency.getName()); + reducedTransparencyFlag.bind(reducedTransparency); + } + + public ReadOnlyBooleanProperty reducedMotionProperty() { + return reducedMotionFlag.getReadOnlyProperty(); + } + + public boolean isReducedMotion() { + return reducedMotion.get(); + } + + public void setReducedMotion(boolean value) { + reducedMotion.setValueOverride(value); + } + + public ReadOnlyBooleanProperty reducedTransparencyProperty() { + return reducedTransparencyFlag.getReadOnlyProperty(); + } + + public boolean isReducedTransparency() { + return reducedTransparency.get(); + } + + public void setReducedTransparency(boolean value) { + reducedTransparency.setValueOverride(value); } public ReadOnlyObjectProperty colorSchemeProperty() { @@ -102,60 +137,38 @@ public void setAccentColor(Color color) { accentColor.setValueOverride(color); } - public void update(Map changedPreferences, Map platformKeyMappings) { + public void update(Map changedPreferences, + Map> platformKeyMappings) { for (Map.Entry entry : changedPreferences.entrySet()) { - String key = platformKeyMappings.get(entry.getKey()); - if (key != null) { - for (ColorProperty colorProperty : allColors) { - if (colorProperty.getName().equals(key)) { - updateColorProperty(colorProperty, entry.getValue().newValue()); - break; - } - } - } - } - - fireValueChangedIfNecessary(); - } - - private void updateColorProperty(ColorProperty property, Object value) { - if (value instanceof Color color) { - property.setValue(color); - } else { - if (value != null) { - Logging.getJavaFXLogger().warning( - "Unexpected value of " + property.getName() + " platform preference, " + - "using default value instead (expected = " + Color.class.getName() + - ", actual = " + value.getClass().getName() + ")"); + if (platformKeyMappings.get(entry.getKey()) instanceof PreferenceMapping mapping + && deferredProperties.get(mapping.keyName()) instanceof DeferredProperty property) { + property.setPlatformValue(mapping.map(entry.getValue().newValue())); } - - // Setting a ColorProperty to 'null' restores its platform value or default value. - property.setValue(null); } - } - private void fireValueChangedIfNecessary() { - for (ColorProperty colorProperty : allColors) { - colorProperty.fireValueChangedIfNecessary(); + for (DeferredProperty property : deferredProperties.values()) { + property.fireValueChangedIfNecessary(); } } /** - * ColorProperty implements a deferred notification mechanism, where change notifications - * are only fired after changes of all color properties have been applied. - * This ensures that observers will never see a transient state where two color properties + * DeferredProperty implements a deferred notification mechanism, where change notifications + * are only fired after changes of all properties have been applied. + * This ensures that observers will never see a transient state where two properties * are inconsistent (for example, both foreground and background could be the same color * when going from light to dark mode). */ - private final class ColorProperty extends ReadOnlyObjectPropertyBase { + private final class DeferredProperty extends ReadOnlyObjectPropertyBase { private final String name; - private final Color defaultValue; - private Color overrideValue; - private Color platformValue; - private Color effectiveValue; - private Color lastEffectiveValue; - - ColorProperty(String name, Color initialValue) { + private final T defaultValue; + private T overrideValue; + private T platformValue; + private T effectiveValue; + private T lastEffectiveValue; + + DeferredProperty(String name, T initialValue) { + Objects.requireNonNull(initialValue); + PreferenceProperties.this.deferredProperties.put(name, this); this.name = name; this.defaultValue = initialValue; this.platformValue = initialValue; @@ -174,38 +187,40 @@ public String getName() { } @Override - public Color get() { + public T get() { return effectiveValue; } /** - * Only called by {@link #updateColorProperty}, this method doesn't fire a change notification. - * Change notifications are fired after the new values of all color properties have been set. + * Only called from {@link PreferenceProperties#update}, this method doesn't fire a change notification. + * Change notifications are fired after the new values of all deferred properties have been set. */ - public void setValue(Color value) { - this.platformValue = value; + @SuppressWarnings("unchecked") + public void setPlatformValue(Object value) { + Class expectedType = defaultValue.getClass(); + this.platformValue = expectedType.isInstance(value) ? (T)value : null; updateEffectiveValue(); } - public void setValueOverride(Color value) { + public void setValueOverride(T value) { this.overrideValue = value; updateEffectiveValue(); fireValueChangedEvent(); } + public void fireValueChangedIfNecessary() { + if (!Objects.equals(lastEffectiveValue, effectiveValue)) { + lastEffectiveValue = effectiveValue; + fireValueChangedEvent(); + } + } + private void updateEffectiveValue() { // Choose the first non-null value in this order: overrideValue, platformValue, defaultValue. effectiveValue = Objects.requireNonNullElse( overrideValue != null ? overrideValue : platformValue, defaultValue); } - - void fireValueChangedIfNecessary() { - if (!Objects.equals(lastEffectiveValue, effectiveValue)) { - lastEffectiveValue = effectiveValue; - fireValueChangedEvent(); - } - } } private class ColorSchemeProperty extends ReadOnlyObjectWrapper { diff --git a/modules/javafx.graphics/src/main/java/javafx/application/Platform.java b/modules/javafx.graphics/src/main/java/javafx/application/Platform.java index 568ef2f4aca..bed4cf6d608 100644 --- a/modules/javafx.graphics/src/main/java/javafx/application/Platform.java +++ b/modules/javafx.graphics/src/main/java/javafx/application/Platform.java @@ -478,6 +478,7 @@ public static Preferences getPreferences() { * * {@code Windows.SPI.HighContrast}{@link Boolean} * {@code Windows.SPI.HighContrastColorScheme}{@link String} + * {@code Windows.SPI.ClientAreaAnimation}{@link Boolean} * {@code Windows.SysColor.COLOR_3DFACE}{@link Color} * {@code Windows.SysColor.COLOR_BTNTEXT}{@link Color} * {@code Windows.SysColor.COLOR_GRAYTEXT}{@link Color} @@ -495,6 +496,7 @@ public static Preferences getPreferences() { * {@code Windows.UIColor.AccentLight1}{@link Color} * {@code Windows.UIColor.AccentLight2}{@link Color} * {@code Windows.UIColor.AccentLight3}{@link Color} + * {@code Windows.UISettings.AdvancedEffectsEnabled}{@link Boolean} * * * @@ -547,6 +549,8 @@ public static Preferences getPreferences() { * {@code macOS.NSColor.systemRedColor}{@link Color} * {@code macOS.NSColor.systemTealColor}{@link Color} * {@code macOS.NSColor.systemYellowColor}{@link Color} + * {@code macOS.NSWorkspace.accessibilityDisplayShouldReduceMotion}{@link Boolean} + * {@code macOS.NSWorkspace.accessibilityDisplayShouldReduceTransparency}{@link Boolean} * * * @@ -572,13 +576,43 @@ public static Preferences getPreferences() { * {@code GTK.warning_color}{@link Color} * {@code GTK.error_color}{@link Color} * {@code GTK.success_color}{@link Color} + * {@code GTK.enable_animations}{@link Boolean} * * * * * @since 22 */ - public interface Preferences extends ObservableMap { + public sealed interface Preferences extends ObservableMap + permits com.sun.javafx.application.preferences.PlatformPreferences { + + /** + * Specifies whether applications should minimize the amount of non-essential animations, + * reducing discomfort for users who experience motion sickness or vertigo. + *

+ * If the platform does not report this preference, this property defaults to {@code false}. + * + * @return the {@code reducedMotion} property + * @defaultValue {@code false} + * @since 24 + */ + ReadOnlyBooleanProperty reducedMotionProperty(); + + boolean isReducedMotion(); + + /** + * Specifies whether applications should minimize the amount of transparent or translucent + * layer effects, which can help to increase contrast and readability for some users. + *

+ * If the platform does not report this preference, this property defaults to {@code false}. + * + * @return the {@code reducedTransparency} property + * @defaultValue {@code false} + * @since 24 + */ + ReadOnlyBooleanProperty reducedTransparencyProperty(); + + boolean isReducedTransparency(); /** * The platform color scheme, which specifies whether applications should prefer light text on diff --git a/modules/javafx.graphics/src/main/native-glass/gtk/GlassApplication.cpp b/modules/javafx.graphics/src/main/native-glass/gtk/GlassApplication.cpp index 8413aeaf474..6e9675e9e84 100644 --- a/modules/javafx.graphics/src/main/native-glass/gtk/GlassApplication.cpp +++ b/modules/javafx.graphics/src/main/native-glass/gtk/GlassApplication.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2011, 2024, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -200,8 +200,10 @@ JNIEXPORT void JNICALL Java_com_sun_glass_ui_gtk_GtkApplication__1init GtkSettings* settings = gtk_settings_get_default(); if (settings != NULL) { - g_signal_connect(G_OBJECT(settings), "notify::gtk-theme-name", - G_CALLBACK(call_update_preferences), NULL); + for (const auto& setting : PlatformSupport::observedSettings) { + g_signal_connect_after(G_OBJECT(settings), setting, + G_CALLBACK(call_update_preferences), NULL); + } } } diff --git a/modules/javafx.graphics/src/main/native-glass/gtk/PlatformSupport.cpp b/modules/javafx.graphics/src/main/native-glass/gtk/PlatformSupport.cpp index e5b2425c403..cf6174fd17f 100644 --- a/modules/javafx.graphics/src/main/native-glass/gtk/PlatformSupport.cpp +++ b/modules/javafx.graphics/src/main/native-glass/gtk/PlatformSupport.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2023, 2024, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -60,6 +60,17 @@ namespace env->CallObjectMethod(preferences, jMapPut, prefKey, prefValue); CHECK_JNI_EXCEPTION(env); } + + void putBoolean(JNIEnv* env, jobject preferences, const char* name, bool value) { + jobject prefKey = env->NewStringUTF(name); + if (EXCEPTION_OCCURED(env) || prefKey == NULL) return; + + jobject prefValue = env->GetStaticObjectField(jBooleanCls, value ? jBooleanTRUE : jBooleanFALSE); + if (EXCEPTION_OCCURED(env) || prefValue == NULL) return; + + env->CallObjectMethod(preferences, jMapPut, prefKey, prefValue); + CHECK_JNI_EXCEPTION(env); + } } PlatformSupport::PlatformSupport(JNIEnv* env, jobject application) @@ -105,6 +116,11 @@ jobject PlatformSupport::collectPreferences() const { gchar* themeName; g_object_get(settings, "gtk-theme-name", &themeName, NULL); putString(env, prefs, "GTK.theme_name", themeName); + g_free(themeName); + + gboolean enableAnimations = true; + g_object_get(settings, "gtk-enable-animations", &enableAnimations, NULL); + putBoolean(env, prefs, "GTK.enable_animations", enableAnimations); } return prefs; diff --git a/modules/javafx.graphics/src/main/native-glass/gtk/PlatformSupport.h b/modules/javafx.graphics/src/main/native-glass/gtk/PlatformSupport.h index c5695873d3c..9a8a0863007 100644 --- a/modules/javafx.graphics/src/main/native-glass/gtk/PlatformSupport.h +++ b/modules/javafx.graphics/src/main/native-glass/gtk/PlatformSupport.h @@ -30,6 +30,11 @@ class PlatformSupport final { public: + static constexpr const char* observedSettings[] = { + "notify::gtk-theme-name", + "notify::gtk-enable-animations" + }; + PlatformSupport(JNIEnv*, jobject); ~PlatformSupport(); PlatformSupport(PlatformSupport const&) = delete; diff --git a/modules/javafx.graphics/src/main/native-glass/mac/GlassApplication.m b/modules/javafx.graphics/src/main/native-glass/mac/GlassApplication.m index b6194226bb7..aff9fce9fb2 100644 --- a/modules/javafx.graphics/src/main/native-glass/mac/GlassApplication.m +++ b/modules/javafx.graphics/src/main/native-glass/mac/GlassApplication.m @@ -245,6 +245,12 @@ - (void)applicationWillFinishLaunching:(NSNotification *)aNotification name:@"AppleColorPreferencesChangedNotification" object:nil]; + [[[NSWorkspace sharedWorkspace] notificationCenter] + addObserver:self + selector:@selector(platformPreferencesDidChange) + name:NSWorkspaceAccessibilityDisplayOptionsDidChangeNotification + object:nil]; + // localMonitor = [NSEvent addLocalMonitorForEventsMatchingMask: NSRightMouseDownMask // handler:^(NSEvent *incomingEvent) { // NSEvent *result = incomingEvent; diff --git a/modules/javafx.graphics/src/main/native-glass/mac/PlatformSupport.m b/modules/javafx.graphics/src/main/native-glass/mac/PlatformSupport.m index 11c17e7a75a..03f11b00743 100644 --- a/modules/javafx.graphics/src/main/native-glass/mac/PlatformSupport.m +++ b/modules/javafx.graphics/src/main/native-glass/mac/PlatformSupport.m @@ -106,6 +106,14 @@ + (jobject)collectPreferences { [PlatformSupport queryNSColors:preferences]; [NSAppearance setCurrentAppearance:lastAppearance]; + [PlatformSupport putBoolean:preferences + key:"macOS.NSWorkspace.accessibilityDisplayShouldReduceMotion" + value:[[NSWorkspace sharedWorkspace] accessibilityDisplayShouldReduceMotion]]; + + [PlatformSupport putBoolean:preferences + key:"macOS.NSWorkspace.accessibilityDisplayShouldReduceTransparency" + value:[[NSWorkspace sharedWorkspace] accessibilityDisplayShouldReduceTransparency]]; + return preferences; } @@ -222,6 +230,19 @@ + (void)updatePreferences:(jobject)application { (*env)->DeleteLocalRef(env, newPreferences); } ++ (void)putBoolean:(jobject)preferences key:(const char*)key value:(bool)value { + GET_MAIN_JENV; + + jobject prefKey = (*env)->NewStringUTF(env, key); + GLASS_CHECK_NONNULL_EXCEPTION_RETURN(env, prefKey); + + jobject prefValue = (*env)->GetStaticObjectField(env, jBooleanClass, value ? jBooleanTRUE : jBooleanFALSE); + GLASS_CHECK_NONNULL_EXCEPTION_RETURN(env, prefValue); + + (*env)->CallObjectMethod(env, preferences, jMapPutMethod, prefKey, prefValue); + GLASS_CHECK_EXCEPTION(env); +} + + (void)putString:(jobject)preferences key:(const char*)key value:(const char*)value { GET_MAIN_JENV; diff --git a/modules/javafx.graphics/src/main/native-glass/win/GlassApplication.cpp b/modules/javafx.graphics/src/main/native-glass/win/GlassApplication.cpp index 525e43b114e..c72b4350654 100644 --- a/modules/javafx.graphics/src/main/native-glass/win/GlassApplication.cpp +++ b/modules/javafx.graphics/src/main/native-glass/win/GlassApplication.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2011, 2024, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -168,11 +168,10 @@ LRESULT GlassApplication::WindowProc(UINT msg, WPARAM wParam, LPARAM lParam) } break; case WM_SETTINGCHANGE: - if (((UINT)wParam == SPI_GETHIGHCONTRAST || - lParam != NULL && wcscmp(LPCWSTR(lParam), L"ImmersiveColorSet") == 0) && - m_platformSupport.updatePreferences(m_grefThis)) { + if (m_platformSupport.onSettingChanged(m_grefThis, wParam, lParam)) { return 0; } + if ((UINT)wParam != SPI_SETWORKAREA) { break; } diff --git a/modules/javafx.graphics/src/main/native-glass/win/PlatformSupport.cpp b/modules/javafx.graphics/src/main/native-glass/win/PlatformSupport.cpp index e090a6495c5..b5ed8514b62 100644 --- a/modules/javafx.graphics/src/main/native-glass/win/PlatformSupport.cpp +++ b/modules/javafx.graphics/src/main/native-glass/win/PlatformSupport.cpp @@ -87,9 +87,9 @@ jobject PlatformSupport::collectPreferences() const jobject prefs = env->NewObject(javaClasses.HashMap, javaIDs.HashMap.init); if (CheckAndClearException(env)) return NULL; - queryHighContrastScheme(prefs); + querySystemParameters(prefs); querySystemColors(prefs); - queryUIColors(prefs); + queryUISettings(prefs); return prefs; } @@ -124,7 +124,22 @@ bool PlatformSupport::updatePreferences(jobject application) const return false; } -void PlatformSupport::queryHighContrastScheme(jobject properties) const +bool PlatformSupport::onSettingChanged(jobject application, WPARAM wParam, LPARAM lParam) const +{ + switch ((UINT)wParam) { + case SPI_SETHIGHCONTRAST: + case SPI_SETCLIENTAREAANIMATION: + return updatePreferences(application); + } + + if (lParam != NULL && wcscmp(LPCWSTR(lParam), L"ImmersiveColorSet") == 0) { + return updatePreferences(application); + } + + return false; +} + +void PlatformSupport::querySystemParameters(jobject properties) const { HIGHCONTRAST contrastInfo; contrastInfo.cbSize = sizeof(HIGHCONTRAST); @@ -138,6 +153,10 @@ void PlatformSupport::queryHighContrastScheme(jobject properties) const putBoolean(properties, "Windows.SPI.HighContrast", false); putString(properties, "Windows.SPI.HighContrastColorScheme", (const char*)NULL); } + + BOOL value; + ::SystemParametersInfo(SPI_GETCLIENTAREAANIMATION, 0, &value, 0); + putBoolean(properties, "Windows.SPI.ClientAreaAnimation", value); } void PlatformSupport::querySystemColors(jobject properties) const @@ -153,17 +172,25 @@ void PlatformSupport::querySystemColors(jobject properties) const putColor(properties, "Windows.SysColor.COLOR_WINDOWTEXT", GetSysColor(COLOR_WINDOWTEXT)); } -void PlatformSupport::queryUIColors(jobject properties) const +void PlatformSupport::queryUISettings(jobject properties) const { if (!isRoActivationSupported()) { return; } + ComPtr settings; + try { - ComPtr settings; RO_CHECKED("RoActivateInstance", RoActivateInstance(hstring("Windows.UI.ViewManagement.UISettings"), (IInspectable**)&settings)); + } catch (RoException const&) { + // If an activation exception occurs, it probably means that we're on a Windows system + // that doesn't support the UISettings API. This is not a problem, it simply means that + // we don't report the UISettings properties back to the JavaFX application. + return; + } + try { ComPtr settings3; RO_CHECKED("IUISettings::QueryInterface", settings->QueryInterface(&settings3)); @@ -192,9 +219,18 @@ void PlatformSupport::queryUIColors(jobject properties) const putColor(properties, "Windows.UIColor.AccentLight2", accentLight2); putColor(properties, "Windows.UIColor.AccentLight3", accentLight3); } catch (RoException const&) { - // If an activation exception occurs, it probably means that we're on a Windows system - // that doesn't support the UISettings API. This is not a problem, it simply means that - // we don't report the UISettings properties back to the JavaFX application. + return; + } + + try { + ComPtr settings4; + RO_CHECKED("IUISettings::QueryInterface", + settings->QueryInterface(&settings4)); + + unsigned char value; + settings4->get_AdvancedEffectsEnabled(&value); + putBoolean(properties, "Windows.UISettings.AdvancedEffectsEnabled", value); + } catch (RoException const&) { return; } } diff --git a/modules/javafx.graphics/src/main/native-glass/win/PlatformSupport.h b/modules/javafx.graphics/src/main/native-glass/win/PlatformSupport.h index 75654736670..22dd52ba7d8 100644 --- a/modules/javafx.graphics/src/main/native-glass/win/PlatformSupport.h +++ b/modules/javafx.graphics/src/main/native-glass/win/PlatformSupport.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2023, 2024, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -48,6 +48,11 @@ class PlatformSupport final */ bool updatePreferences(jobject application) const; + /** + * Handles the WM_SETTINGCHANGE message. + */ + bool onSettingChanged(jobject application, WPARAM, LPARAM) const; + private: JNIEnv* env; bool initialized; @@ -63,8 +68,8 @@ class PlatformSupport final } javaClasses; void querySystemColors(jobject properties) const; - void queryHighContrastScheme(jobject properties) const; - void queryUIColors(jobject properties) const; + void querySystemParameters(jobject properties) const; + void queryUISettings(jobject properties) const; void putString(jobject properties, const char* key, const char* value) const; void putString(jobject properties, const char* key, const wchar_t* value) const; diff --git a/modules/javafx.graphics/src/test/java/test/com/sun/javafx/application/preferences/PlatformPreferencesTest.java b/modules/javafx.graphics/src/test/java/test/com/sun/javafx/application/preferences/PlatformPreferencesTest.java index 170b3dd9e72..6eff27478ce 100644 --- a/modules/javafx.graphics/src/test/java/test/com/sun/javafx/application/preferences/PlatformPreferencesTest.java +++ b/modules/javafx.graphics/src/test/java/test/com/sun/javafx/application/preferences/PlatformPreferencesTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2023, 2024, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -26,6 +26,7 @@ package test.com.sun.javafx.application.preferences; import com.sun.javafx.application.preferences.PlatformPreferences; +import com.sun.javafx.application.preferences.PreferenceMapping; import javafx.animation.Interpolatable; import javafx.scene.paint.CycleMethod; import javafx.scene.paint.LinearGradient; @@ -65,9 +66,11 @@ void setup() { ), // Platform-specific key mappings Map.of( - "test.foregroundColor", "foregroundColor", - "test.backgroundColor", "backgroundColor", - "test.accentColor", "accentColor" + "test.foregroundColor", new PreferenceMapping<>("foregroundColor", Color.class), + "test.backgroundColor", new PreferenceMapping<>("backgroundColor", Color.class), + "test.accentColor", new PreferenceMapping<>("accentColor", Color.class), + "test.reducedMotion", new PreferenceMapping<>("reducedMotion", Boolean.class), + "test.enableTransparency", new PreferenceMapping<>("reducedTransparency", Boolean.class, b -> !b) )); } @@ -327,4 +330,39 @@ private void testColorPropertyChangesAreAtomic(List trace, int listener } } + @Test + void testReducedMotionProperty() { + var trace = new ArrayList(); + prefs.reducedMotionProperty().addListener((observable, ov, nv) -> trace.add(nv)); + + assertFalse(prefs.isReducedMotion()); + prefs.update(Map.of("test.reducedMotion", true)); + + assertEquals(1, trace.size()); + assertEquals(Boolean.TRUE, trace.get(0)); + assertTrue(prefs.isReducedMotion()); + + prefs.update(new HashMap<>() {{ put("test.reducedMotion", null); }}); + assertEquals(2, trace.size()); + assertEquals(Boolean.FALSE, trace.get(1)); + assertFalse(prefs.isReducedMotion()); + } + + @Test + void testReducedTransparencyPropertyWithInverseMapping() { + var trace = new ArrayList(); + prefs.reducedTransparencyProperty().addListener((observable, ov, nv) -> trace.add(nv)); + + assertFalse(prefs.isReducedTransparency()); + prefs.update(Map.of("test.enableTransparency", false)); + + assertEquals(1, trace.size()); + assertEquals(Boolean.TRUE, trace.get(0)); + assertTrue(prefs.isReducedTransparency()); + + prefs.update(new HashMap<>() {{ put("test.enableTransparency", null); }}); + assertEquals(2, trace.size()); + assertEquals(Boolean.FALSE, trace.get(1)); + assertFalse(prefs.isReducedTransparency()); + } } diff --git a/tests/manual/events/PlatformPreferencesChangedTest.java b/tests/manual/events/PlatformPreferencesChangedTest.java index 7d4c174d9e8..fc2dde8fc65 100644 --- a/tests/manual/events/PlatformPreferencesChangedTest.java +++ b/tests/manual/events/PlatformPreferencesChangedTest.java @@ -64,6 +64,8 @@ public void start(Stage stage) { var foregroundColorLabel = new Label(); var accentColorLabel = new Label(); var colorSchemeLabel = new Label(); + var reducedMotionLabel = new Label(); + var reducedTransparencyLabel = new Label(); Runnable updateColorProperties = () -> { var preferences = Platform.getPreferences(); @@ -71,6 +73,8 @@ public void start(Stage stage) { foregroundColorLabel.setText(preferences.getForegroundColor().toString()); accentColorLabel.setText(preferences.getAccentColor().toString()); colorSchemeLabel.setText(preferences.getColorScheme().toString()); + reducedMotionLabel.setText(Boolean.toString(preferences.isReducedMotion())); + reducedTransparencyLabel.setText(Boolean.toString(preferences.isReducedTransparency())); }; var box = new VBox(); @@ -87,7 +91,9 @@ public void start(Stage stage) { new HBox(new BoldLabel(" backgroundColor: "), backgroundColorLabel), new HBox(new BoldLabel(" foregroundColor: "), foregroundColorLabel), new HBox(new BoldLabel(" accentColor: "), accentColorLabel), - new HBox(new BoldLabel(" colorScheme: "), colorSchemeLabel)), + new HBox(new BoldLabel(" colorScheme: "), colorSchemeLabel), + new HBox(new BoldLabel(" reducedMotion: "), reducedMotionLabel), + new HBox(new BoldLabel(" reducedTransparency: "), reducedTransparencyLabel)), new Label("4. Click \"Pass\" if the changes were correctly reported, otherwise click \"Fail\"."), new HBox(5, passButton, failButton, clearButton) ));