Skip to content

8357067: Platform preference change can emit multiple notifications #1810

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

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2010, 2024, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2010, 2025, 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
Expand Down Expand Up @@ -84,7 +84,7 @@ public void handleOpenFilesAction(Application app, long time, String files[]) {
// currently used only on Mac OS X
public void handleQuitAction(Application app, long time) {
}
public void handlePreferencesChanged(Map<String, Object> preferences) {
public void handlePreferencesChanged(Map<String, Object> preferences, int suggestedDelayMillis) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I wonder if we should always debounce the changes with a short delay?
A delay of 100-150 ms will be acceptable from the user perspective.
What do you think?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Most changed settings are not correlated with other settings, so no waiting is necessary (for example, we wouldn't wait after a reducedMotion change). We only need to wait a bit when a single user-facing setting can affect several correlated preferences. This only seems to be the case on Windows, and only when changing high-contrast themes.

}
}

Expand Down Expand Up @@ -255,10 +255,10 @@ protected void notifyWillResignActive() {
}
}

protected void notifyPreferencesChanged(Map<String, Object> preferences) {
protected void notifyPreferencesChanged(Map<String, Object> preferences, int suggestedDelayMillis) {
EventHandler handler = getEventHandler();
if (handler != null) {
handler.handlePreferencesChanged(preferences);
handler.handlePreferencesChanged(preferences, suggestedDelayMillis);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import com.sun.javafx.PlatformUtil;
import com.sun.javafx.PreviewFeature;
import com.sun.javafx.SecurityUtil;
import com.sun.javafx.application.preferences.DelayedChangeAggregator;
import com.sun.javafx.application.preferences.PlatformPreferences;
import com.sun.javafx.application.preferences.PreferenceMapping;
import com.sun.javafx.css.StyleManager;
Expand Down Expand Up @@ -57,6 +58,7 @@
import javafx.application.ConditionalFeature;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.collections.MapChangeListener;
import javafx.scene.Scene;

public class PlatformImpl {
Expand Down Expand Up @@ -918,6 +920,7 @@ private static boolean isSupportedImpl(ConditionalFeature feature) {
}

private static PlatformPreferences platformPreferences;
private static DelayedChangeAggregator platformPreferencesAggregator;

public static PlatformPreferences getPlatformPreferences() {
if (platformPreferences == null) {
Expand All @@ -937,7 +940,12 @@ public static void initPreferences(Map<String, Class<?>> platformKeys,
Map<String, PreferenceMapping<?, ?>> platformKeyMappings,
Map<String, Object> preferences) {
platformPreferences = new PlatformPreferences(platformKeys, platformKeyMappings);
platformPreferences.addListener(PlatformImpl::checkHighContrastThemeChanged);
platformPreferences.update(preferences);
platformPreferencesAggregator = new DelayedChangeAggregator(
platformPreferences::update,
Toolkit.getToolkit().getPrimaryTimer()::nanos,
PlatformImpl::runLater);
}

/**
Expand All @@ -948,17 +956,19 @@ public static void initPreferences(Map<String, Class<?>> platformKeys,
* was removed, the corresponding key is mapped to {@code null}.
*
* @param preferences a map that includes the changed preferences
* @param suggestedDelayMillis a suggestion from the native implementation to delay the publication of
* changed preferences for the specified amount of time, because more changes
* may be coming
*/
public static void updatePreferences(Map<String, Object> preferences) {
public static void updatePreferences(Map<String, Object> preferences, int suggestedDelayMillis) {
if (isFxApplicationThread()) {
checkHighContrastThemeChanged(preferences);
platformPreferences.update(preferences);
platformPreferencesAggregator.update(preferences, suggestedDelayMillis);
} else {
// Make a defensive copy in case the caller of this method decides to re-use or
// modify its preferences map after the method returns. Don't use Map.copyOf
// because the preferences map may contain null values.
Map<String, Object> preferencesCopy = new HashMap<>(preferences);
runLater(() -> updatePreferences(preferencesCopy));
runLater(() -> updatePreferences(preferencesCopy, suggestedDelayMillis));
}
}

Expand All @@ -974,11 +984,15 @@ public static void checkPreferencesSupport() {
}

// This method will be removed when StyleThemes are added.
private static void checkHighContrastThemeChanged(Map<String, Object> preferences) {
if (Boolean.TRUE.equals(preferences.get("Windows.SPI.HighContrast"))) {
setAccessibilityTheme(preferences.get("Windows.SPI.HighContrastColorScheme") instanceof String s ? s : null);
} else {
setAccessibilityTheme(null);
private static void checkHighContrastThemeChanged(MapChangeListener.Change<? extends String, ?> change) {
if (change.getKey().equals("Windows.SPI.HighContrast")
|| change.getKey().equals("Windows.SPI.HighContrastColorScheme")) {
setAccessibilityTheme(
platformPreferences
.getBoolean("Windows.SPI.HighContrast")
.filter(Boolean::booleanValue)
.flatMap(_ -> platformPreferences.getString("Windows.SPI.HighContrastColorScheme"))
.orElse(null));
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/*
* Copyright (c) 2025, 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 java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Executor;
import java.util.function.Consumer;
import java.util.function.LongSupplier;

/**
* Aggregates multiple subsequent sets of changes into a single changeset, and notifies a consumer.
* Due to its delayed nature, the consumer may not be notified immediately when a changeset arrives.
* <p>
* This class is not thread-safe and can only safely be used on a single thread; this applies to the
* {@link #update(Map, int)}} method as well as the delayed executor.
*/
public final class DelayedChangeAggregator {

private final Executor delayedExecutor;
private final LongSupplier nanoTimeSupplier;
private final Consumer<Map<String, Object>> changeConsumer;
private final Map<String, Object> currentChangeSet;
private long elapsedTimeNanos;
private int serial;

public DelayedChangeAggregator(Consumer<Map<String, Object>> changeConsumer,
LongSupplier nanoTimeSupplier,
Executor delayedExecutor) {
this.changeConsumer = changeConsumer;
this.nanoTimeSupplier = nanoTimeSupplier;
this.delayedExecutor = delayedExecutor;
this.currentChangeSet = new HashMap<>();
}

/**
* Integrates the specified changeset into the current changeset, and applies the current changeset
* after the specified delay period. The delay is added to the current time, but will not elapse
* before any previous delays are scheduled to elapse.
*
* @param changeset the changeset
* @param delayMillis the delay period, in milliseconds
*/
public void update(Map<String, Object> changeset, int delayMillis) {
if (delayMillis > 0 || !currentChangeSet.isEmpty()) {
int currentSerial = ++serial;
long newElapsedTimeNanos = nanoTimeSupplier.getAsLong() + (long)delayMillis * 1000000;
elapsedTimeNanos = Math.max(elapsedTimeNanos, newElapsedTimeNanos);
currentChangeSet.putAll(changeset);
delayedExecutor.execute(() -> update(currentSerial));
} else {
changeConsumer.accept(changeset);
}
}

private void update(int expectedSerial) {
if (expectedSerial == serial) {
if (nanoTimeSupplier.getAsLong() < elapsedTimeNanos) {
delayedExecutor.execute(() -> update(expectedSerial));
} else {
changeConsumer.accept(currentChangeSet);
currentChangeSet.clear();
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -365,8 +365,8 @@ void runToolkit() {
}

@Override
public void handlePreferencesChanged(Map<String, Object> preferences) {
PlatformImpl.updatePreferences(preferences);
public void handlePreferencesChanged(Map<String, Object> preferences, int suggestedDelayMillis) {
PlatformImpl.updatePreferences(preferences, suggestedDelayMillis);
}
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ void PlatformSupport::updatePreferences() const {
jCollectionsCls, jCollectionsUnmodifiableMap, newPreferences);

if (!EXCEPTION_OCCURED(env)) {
env->CallVoidMethod(application, jApplicationNotifyPreferencesChanged, unmodifiablePreferences);
env->CallVoidMethod(application, jApplicationNotifyPreferencesChanged, unmodifiablePreferences, 0);
EXCEPTION_OCCURED(env);

env->DeleteLocalRef(unmodifiablePreferences);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -350,7 +350,7 @@ JNI_OnLoad(JavaVM *jvm, void *reserved)
if (env->ExceptionCheck()) return JNI_ERR;
jApplicationGetName = env->GetMethodID(jApplicationCls, "getName", "()Ljava/lang/String;");
if (env->ExceptionCheck()) return JNI_ERR;
jApplicationNotifyPreferencesChanged = env->GetMethodID(jApplicationCls, "notifyPreferencesChanged", "(Ljava/util/Map;)V");
jApplicationNotifyPreferencesChanged = env->GetMethodID(jApplicationCls, "notifyPreferencesChanged", "(Ljava/util/Map;I)V");
if (env->ExceptionCheck()) return JNI_ERR;

clazz = env->FindClass("java/lang/Object");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ struct jni_exception: public std::exception {
extern jmethodID jApplicationReportException; // reportException(Ljava/lang/Throwable;)V
extern jmethodID jApplicationGetApplication; // GetApplication()()Lcom/sun/glass/ui/Application;
extern jmethodID jApplicationGetName; // getName()Ljava/lang/String;
extern jmethodID jApplicationNotifyPreferencesChanged; // notifyPreferencesChanged(Ljava/util/Map;)V
extern jmethodID jApplicationNotifyPreferencesChanged; // notifyPreferencesChanged(Ljava/util/Map;I)V

extern jclass jObjectCls; // java.lang.Object
extern jmethodID jObjectEquals; // java.lang.Object#equals(Ljava/lang/Object;)Z
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -967,7 +967,7 @@ + (BOOL)syncRenderingDisabled {
if ((*env)->ExceptionCheck(env)) return;

javaIDs.MacApplication.notifyPreferencesChanged = (*env)->GetMethodID(
env, jClass, "notifyPreferencesChanged", "(Ljava/util/Map;)V");
env, jClass, "notifyPreferencesChanged", "(Ljava/util/Map;I)V");
if ((*env)->ExceptionCheck(env)) return;

if (jRunnableRun == NULL)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,7 @@ - (void)updatePreferences {
(*env)->CallVoidMethod(
env, application,
javaIDs.MacApplication.notifyPreferencesChanged,
unmodifiablePreferences);
unmodifiablePreferences, 0);
GLASS_CHECK_EXCEPTION(env);

(*env)->DeleteLocalRef(env, unmodifiablePreferences);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2011, 2024, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2011, 2025, 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
Expand Down Expand Up @@ -182,9 +182,11 @@ LRESULT GlassApplication::WindowProc(UINT msg, WPARAM wParam, LPARAM lParam)
case WM_THEMECHANGED:
case WM_SYSCOLORCHANGE:
case WM_DWMCOLORIZATIONCOLORCHANGED:
// Usually, the WM_THEMECHANGED and WM_SYSCOLORCHANGE messages are followed by other
// messages or WinRT callbacks that may change platform preferences.
if (m_platformSupport.updatePreferences(
PlatformSupport::PreferenceType(PlatformSupport::PT_SYSTEM_COLORS |
PlatformSupport::PT_UI_SETTINGS))) {
PlatformSupport::PreferenceType(PlatformSupport::PT_SYSTEM_COLORS | PlatformSupport::PT_SYSTEM_PARAMS),
/* delayedChangesExpected: */ msg == WM_THEMECHANGED || msg == WM_SYSCOLORCHANGE)) {
return 0;
}
break;
Expand Down Expand Up @@ -332,7 +334,7 @@ JNIEXPORT void JNICALL Java_com_sun_glass_ui_win_WinApplication_initIDs
if (CheckAndClearException(env)) return;

javaIDs.Application.notifyPreferencesChangedMID =
env->GetMethodID(cls, "notifyPreferencesChanged", "(Ljava/util/Map;)V");
env->GetMethodID(cls, "notifyPreferencesChanged", "(Ljava/util/Map;I)V");
ASSERT(javaIDs.Application.notifyPreferencesChangedMID);
if (CheckAndClearException(env)) return;

Expand Down
Loading