From c6717bd4e1165380f4c6eb4af694f8998ce11319 Mon Sep 17 00:00:00 2001 From: Armin Schrenk Date: Mon, 9 Feb 2026 17:48:23 +0100 Subject: [PATCH 01/15] Rely on javafx to change color theme --- src/main/java/module-info.java | 3 + .../ui/fxapp/FxApplicationStyle.java | 6 ++ .../ui/fxapp/JfxUiAppearanceProvider.java | 75 +++++++++++++++++++ 3 files changed, 84 insertions(+) create mode 100644 src/main/java/org/cryptomator/ui/fxapp/JfxUiAppearanceProvider.java diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 450cd4ca7..0ea231bdd 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -14,11 +14,13 @@ import org.cryptomator.common.locationpresets.OneDriveMacLocationPresetsProvider import org.cryptomator.common.locationpresets.OneDriveWindowsLocationPresetsProvider; import org.cryptomator.common.locationpresets.PCloudLocationPresetsProvider; import org.cryptomator.integrations.tray.TrayMenuController; +import org.cryptomator.integrations.uiappearance.UiAppearanceProvider; import org.cryptomator.logging.LogbackConfiguratorFactory; import org.cryptomator.networking.SSLContextProvider; import org.cryptomator.networking.SSLContextWithMacKeychain; import org.cryptomator.networking.SSLContextWithPKCS12TrustStore; import org.cryptomator.networking.SSLContextWithWindowsCertStore; +import org.cryptomator.ui.fxapp.JfxUiAppearanceProvider; import org.cryptomator.ui.traymenu.AwtTrayMenuController; open module org.cryptomator.desktop { @@ -61,6 +63,7 @@ open module org.cryptomator.desktop { uses SSLContextProvider; uses org.cryptomator.event.NotificationHandler; + provides UiAppearanceProvider with JfxUiAppearanceProvider; provides TrayMenuController with AwtTrayMenuController; provides Configurator with LogbackConfiguratorFactory; provides SSLContextProvider with SSLContextWithWindowsCertStore, SSLContextWithMacKeychain, SSLContextWithPKCS12TrustStore; diff --git a/src/main/java/org/cryptomator/ui/fxapp/FxApplicationStyle.java b/src/main/java/org/cryptomator/ui/fxapp/FxApplicationStyle.java index ee187fd65..76cb0671e 100644 --- a/src/main/java/org/cryptomator/ui/fxapp/FxApplicationStyle.java +++ b/src/main/java/org/cryptomator/ui/fxapp/FxApplicationStyle.java @@ -12,6 +12,7 @@ import org.slf4j.LoggerFactory; import javax.inject.Inject; import javafx.application.Application; +import javafx.application.Platform; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.beans.value.ObservableValue; @@ -36,6 +37,11 @@ public class FxApplicationStyle { } public void initialize() { + appearanceProvider.ifPresent(service -> { + if(service instanceof JfxUiAppearanceProvider fxService) { + fxService.setJavaFXPlatform(Platform.getPreferences()); + } + }); settings.theme.addListener(this::appThemeChanged); loadSelectedStyleSheet(settings.theme.get()); } diff --git a/src/main/java/org/cryptomator/ui/fxapp/JfxUiAppearanceProvider.java b/src/main/java/org/cryptomator/ui/fxapp/JfxUiAppearanceProvider.java new file mode 100644 index 000000000..bf1545b4d --- /dev/null +++ b/src/main/java/org/cryptomator/ui/fxapp/JfxUiAppearanceProvider.java @@ -0,0 +1,75 @@ +package org.cryptomator.ui.fxapp; + +import org.cryptomator.integrations.common.DisplayName; +import org.cryptomator.integrations.common.OperatingSystem; +import org.cryptomator.integrations.common.Priority; +import org.cryptomator.integrations.uiappearance.Theme; +import org.cryptomator.integrations.uiappearance.UiAppearanceException; +import org.cryptomator.integrations.uiappearance.UiAppearanceListener; +import org.cryptomator.integrations.uiappearance.UiAppearanceProvider; + +import javafx.application.ColorScheme; +import javafx.application.Platform; +import javafx.beans.value.ChangeListener; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicReference; + +@DisplayName("JavaFX Color Scheme switcher") +@OperatingSystem(OperatingSystem.Value.LINUX) +@OperatingSystem(OperatingSystem.Value.WINDOWS) +@OperatingSystem(OperatingSystem.Value.MAC) +@Priority(1050) +public class JfxUiAppearanceProvider implements UiAppearanceProvider { + + final ConcurrentHashMap> uiAppearanceListeners = new ConcurrentHashMap<>(); + final AtomicReference fxPreferencesContainer = new AtomicReference<>(); + + public void setJavaFXPlatform(Platform.Preferences preferences) { + fxPreferencesContainer.set(preferences); + } + + @Override + public Theme getSystemTheme() { + var pref = fxPreferencesContainer.get(); + if (pref != null) { + return switch (pref.getColorScheme()) { + case DARK -> Theme.DARK; + case LIGHT -> Theme.LIGHT; + }; + } + return Theme.LIGHT; + } + + @Override + public void adjustToTheme(Theme theme) { + //no-op + } + + @Override + public void addListener(UiAppearanceListener uiAppearanceListener) throws UiAppearanceException { + var pref = fxPreferencesContainer.get(); + if (pref != null) { + var fxChangeListener = (ChangeListener) (_, _, newScheme) -> { + var newTheme = switch (newScheme) { + case DARK -> Theme.DARK; + case LIGHT -> Theme.LIGHT; + }; + uiAppearanceListener.systemAppearanceChanged(newTheme); + }; + uiAppearanceListeners.compute(uiAppearanceListener, (k, v) -> { + pref.colorSchemeProperty().addListener(fxChangeListener); + return fxChangeListener; + }); + } + } + + @Override + public void removeListener(UiAppearanceListener uiAppearanceListener) throws UiAppearanceException { + var pref = fxPreferencesContainer.get(); + var fxChangeListener = uiAppearanceListeners.remove(uiAppearanceListener); + if (pref != null && fxChangeListener != null) { + pref.colorSchemeProperty().removeListener(fxChangeListener); + } + } + +} From a4eadd481707f1718305e0c9cd3a3f4a9cc1e002 Mon Sep 17 00:00:00 2001 From: Armin Schrenk Date: Tue, 10 Feb 2026 17:58:29 +0100 Subject: [PATCH 02/15] remove theme change restrictions --- .../java/org/cryptomator/common/settings/UiTheme.java | 8 -------- .../ui/preferences/InterfacePreferencesController.java | 2 +- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/src/main/java/org/cryptomator/common/settings/UiTheme.java b/src/main/java/org/cryptomator/common/settings/UiTheme.java index 9c3153060..a1c510f5a 100644 --- a/src/main/java/org/cryptomator/common/settings/UiTheme.java +++ b/src/main/java/org/cryptomator/common/settings/UiTheme.java @@ -10,14 +10,6 @@ public enum UiTheme { DARK("preferences.interface.theme.dark"), // AUTOMATIC("preferences.interface.theme.automatic"); - public static UiTheme[] applicableValues() { - if (SystemUtils.IS_OS_MAC || SystemUtils.IS_OS_WINDOWS) { - return values(); - } else { - return new UiTheme[]{LIGHT, DARK}; - } - } - private final String displayName; UiTheme(String displayName) { diff --git a/src/main/java/org/cryptomator/ui/preferences/InterfacePreferencesController.java b/src/main/java/org/cryptomator/ui/preferences/InterfacePreferencesController.java index 573bfc394..3eeb2f234 100644 --- a/src/main/java/org/cryptomator/ui/preferences/InterfacePreferencesController.java +++ b/src/main/java/org/cryptomator/ui/preferences/InterfacePreferencesController.java @@ -56,7 +56,7 @@ public class InterfacePreferencesController implements FxController { @FXML public void initialize() { - themeChoiceBox.getItems().addAll(UiTheme.applicableValues()); + themeChoiceBox.getItems().addAll(UiTheme.values()); if (!themeChoiceBox.getItems().contains(settings.theme.get())) { settings.theme.set(UiTheme.LIGHT); } From 46d1d605ad8a14e99006e56d9604182a2a759695 Mon Sep 17 00:00:00 2001 From: Armin Schrenk Date: Tue, 10 Feb 2026 18:01:36 +0100 Subject: [PATCH 03/15] refactor JfxUiAppearanceProvider class * use delegate pattern for initialization * add logging --- .../ui/fxapp/FxApplicationStyle.java | 2 +- .../ui/fxapp/JfxUiAppearanceProvider.java | 92 ++++++++++++++----- 2 files changed, 69 insertions(+), 25 deletions(-) diff --git a/src/main/java/org/cryptomator/ui/fxapp/FxApplicationStyle.java b/src/main/java/org/cryptomator/ui/fxapp/FxApplicationStyle.java index 76cb0671e..d8b7f540d 100644 --- a/src/main/java/org/cryptomator/ui/fxapp/FxApplicationStyle.java +++ b/src/main/java/org/cryptomator/ui/fxapp/FxApplicationStyle.java @@ -39,7 +39,7 @@ public class FxApplicationStyle { public void initialize() { appearanceProvider.ifPresent(service -> { if(service instanceof JfxUiAppearanceProvider fxService) { - fxService.setJavaFXPlatform(Platform.getPreferences()); + fxService.initialize(Platform.getPreferences()); } }); settings.theme.addListener(this::appThemeChanged); diff --git a/src/main/java/org/cryptomator/ui/fxapp/JfxUiAppearanceProvider.java b/src/main/java/org/cryptomator/ui/fxapp/JfxUiAppearanceProvider.java index bf1545b4d..151353ff2 100644 --- a/src/main/java/org/cryptomator/ui/fxapp/JfxUiAppearanceProvider.java +++ b/src/main/java/org/cryptomator/ui/fxapp/JfxUiAppearanceProvider.java @@ -7,6 +7,8 @@ import org.cryptomator.integrations.uiappearance.Theme; import org.cryptomator.integrations.uiappearance.UiAppearanceException; import org.cryptomator.integrations.uiappearance.UiAppearanceListener; import org.cryptomator.integrations.uiappearance.UiAppearanceProvider; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import javafx.application.ColorScheme; import javafx.application.Platform; @@ -21,34 +23,39 @@ import java.util.concurrent.atomic.AtomicReference; @Priority(1050) public class JfxUiAppearanceProvider implements UiAppearanceProvider { - final ConcurrentHashMap> uiAppearanceListeners = new ConcurrentHashMap<>(); - final AtomicReference fxPreferencesContainer = new AtomicReference<>(); + private static final Logger LOG = LoggerFactory.getLogger(JfxUiAppearanceProvider.class); - public void setJavaFXPlatform(Platform.Preferences preferences) { - fxPreferencesContainer.set(preferences); + private final AtomicReference realImpl = new AtomicReference<>(null); + + public void initialize(Platform.Preferences preferences) { + realImpl.compareAndSet(null, new JfxUiAppearanceImpl(preferences)); + LOG.debug("Initialized {} with JavaFX preferences", JfxUiAppearanceImpl.class); } - @Override - public Theme getSystemTheme() { - var pref = fxPreferencesContainer.get(); - if (pref != null) { - return switch (pref.getColorScheme()) { + private static class JfxUiAppearanceImpl implements UiAppearanceProvider { + + private final Platform.Preferences preferences; + private final ConcurrentHashMap> uiAppearanceListeners = new ConcurrentHashMap<>(); + + private JfxUiAppearanceImpl(Platform.Preferences preferences) { + this.preferences = preferences; + } + + @Override + public Theme getSystemTheme() { + return switch (preferences.getColorScheme()) { case DARK -> Theme.DARK; case LIGHT -> Theme.LIGHT; }; } - return Theme.LIGHT; - } - @Override - public void adjustToTheme(Theme theme) { - //no-op - } + @Override + public void adjustToTheme(Theme theme) { + //no-op + } - @Override - public void addListener(UiAppearanceListener uiAppearanceListener) throws UiAppearanceException { - var pref = fxPreferencesContainer.get(); - if (pref != null) { + @Override + public void addListener(UiAppearanceListener uiAppearanceListener) throws UiAppearanceException { var fxChangeListener = (ChangeListener) (_, _, newScheme) -> { var newTheme = switch (newScheme) { case DARK -> Theme.DARK; @@ -56,19 +63,56 @@ public class JfxUiAppearanceProvider implements UiAppearanceProvider { }; uiAppearanceListener.systemAppearanceChanged(newTheme); }; + LOG.debug("Register listener for OS theme changes"); uiAppearanceListeners.compute(uiAppearanceListener, (k, v) -> { - pref.colorSchemeProperty().addListener(fxChangeListener); + Platform.runLater(() -> preferences.colorSchemeProperty().addListener(fxChangeListener)); return fxChangeListener; }); } + + @Override + public void removeListener(UiAppearanceListener uiAppearanceListener) throws UiAppearanceException { + var fxChangeListener = uiAppearanceListeners.remove(uiAppearanceListener); + if (fxChangeListener != null) { + LOG.debug("Removing listener for OS theme changes"); + Platform.runLater(() -> preferences.colorSchemeProperty().removeListener(fxChangeListener)); + } + } + } + + + //just delegate methods + @Override + public Theme getSystemTheme() { + var impl = realImpl.get(); + if (impl != null) { + return impl.getSystemTheme(); + } else { + return Theme.LIGHT; + } + } + + @Override + public void adjustToTheme(Theme theme) { + var impl = realImpl.get(); + if (impl != null) { + impl.getSystemTheme(); + } + } + + @Override + public void addListener(UiAppearanceListener uiAppearanceListener) throws UiAppearanceException { + var impl = realImpl.get(); + if (impl != null) { + impl.addListener(uiAppearanceListener); + } } @Override public void removeListener(UiAppearanceListener uiAppearanceListener) throws UiAppearanceException { - var pref = fxPreferencesContainer.get(); - var fxChangeListener = uiAppearanceListeners.remove(uiAppearanceListener); - if (pref != null && fxChangeListener != null) { - pref.colorSchemeProperty().removeListener(fxChangeListener); + var impl = realImpl.get(); + if (impl != null) { + impl.removeListener(uiAppearanceListener); } } From e3433cb312b5128d994132448ae6d2ba969f3236 Mon Sep 17 00:00:00 2001 From: Armin Schrenk Date: Tue, 10 Feb 2026 18:17:09 +0100 Subject: [PATCH 04/15] Refactor FxApplicationStyle class --- .../ChooseExistingVaultController.java | 2 +- .../ui/fxapp/FxApplicationStyle.java | 106 ++++++++---------- 2 files changed, 49 insertions(+), 59 deletions(-) diff --git a/src/main/java/org/cryptomator/ui/addvaultwizard/ChooseExistingVaultController.java b/src/main/java/org/cryptomator/ui/addvaultwizard/ChooseExistingVaultController.java index be9ea15c7..7862c3b20 100644 --- a/src/main/java/org/cryptomator/ui/addvaultwizard/ChooseExistingVaultController.java +++ b/src/main/java/org/cryptomator/ui/addvaultwizard/ChooseExistingVaultController.java @@ -59,7 +59,7 @@ public class ChooseExistingVaultController implements FxController { this.vault = vault; this.vaultListManager = vaultListManager; this.resourceBundle = resourceBundle; - this.screenshot = applicationStyle.appliedThemeProperty().map(this::selectScreenshot); + this.screenshot = applicationStyle.appliedAppThemeProperty().map(this::selectScreenshot); } private Image selectScreenshot(Theme theme) { diff --git a/src/main/java/org/cryptomator/ui/fxapp/FxApplicationStyle.java b/src/main/java/org/cryptomator/ui/fxapp/FxApplicationStyle.java index d8b7f540d..cf524b449 100644 --- a/src/main/java/org/cryptomator/ui/fxapp/FxApplicationStyle.java +++ b/src/main/java/org/cryptomator/ui/fxapp/FxApplicationStyle.java @@ -38,86 +38,76 @@ public class FxApplicationStyle { public void initialize() { appearanceProvider.ifPresent(service -> { - if(service instanceof JfxUiAppearanceProvider fxService) { + if (service instanceof JfxUiAppearanceProvider fxService) { fxService.initialize(Platform.getPreferences()); } }); + applyTheme(settings.theme.get()); settings.theme.addListener(this::appThemeChanged); - loadSelectedStyleSheet(settings.theme.get()); } - private void appThemeChanged(@SuppressWarnings("unused") ObservableValue observable, @SuppressWarnings("unused") UiTheme oldValue, UiTheme newValue) { - if (appearanceProvider.isPresent() && oldValue == UiTheme.AUTOMATIC && newValue != UiTheme.AUTOMATIC) { + private void appThemeChanged(@SuppressWarnings("unused") ObservableValue observable, UiTheme oldValue, UiTheme newValue) { + if (oldValue == UiTheme.AUTOMATIC && newValue != UiTheme.AUTOMATIC) { + appearanceProvider.ifPresent(service -> { + try { + service.removeListener(systemInterfaceThemeListener); + } catch (UiAppearanceException e) { + LOG.warn("Failed to disable automatic theme switching."); + } + }); + } + applyTheme(newValue); + } + + private void applyTheme(UiTheme theme) { + if (!licenseHolder.isValidLicense()) { + loadAndApplyLightTheme(); + } else { + switch (theme) { + case AUTOMATIC -> registerAutomaticThemeChange(); + case LIGHT -> loadAndApplyLightTheme(); + case DARK -> loadAndApplyDarkTheme(); + } + } + } + + private void registerAutomaticThemeChange() { + appearanceProvider.ifPresent(provider -> { try { - appearanceProvider.get().removeListener(systemInterfaceThemeListener); + provider.addListener(systemInterfaceThemeListener); } catch (UiAppearanceException e) { - LOG.error("Failed to disable automatic theme switching."); + LOG.error("Failed to enable automatic theme switching."); } - } - loadSelectedStyleSheet(newValue); + systemInterfaceThemeChanged(provider.getSystemTheme()); + }); } - private void loadSelectedStyleSheet(UiTheme desiredTheme) { - UiTheme theme = licenseHolder.isValidLicense() ? desiredTheme : UiTheme.LIGHT; - switch (theme) { - case LIGHT -> applyLightTheme(); - case DARK -> applyDarkTheme(); - case AUTOMATIC -> { - appearanceProvider.ifPresent(provider -> { - try { - provider.addListener(systemInterfaceThemeListener); - } catch (UiAppearanceException e) { - LOG.error("Failed to enable automatic theme switching."); - } - }); - applySystemTheme(); - } + private void systemInterfaceThemeChanged(Theme osTheme) { + switch (osTheme) { + case LIGHT -> loadAndApplyLightTheme(); + case DARK -> loadAndApplyDarkTheme(); } } - private void systemInterfaceThemeChanged(Theme theme) { - switch (theme) { - case LIGHT -> applyLightTheme(); - case DARK -> applyDarkTheme(); - } + private void loadAndApplyLightTheme() { + loadAndApplyTheme(Theme.LIGHT, "/css/light_theme.css"); } - private void applySystemTheme() { - if (appearanceProvider.isPresent()) { - systemInterfaceThemeChanged(appearanceProvider.get().getSystemTheme()); - } else { - LOG.warn("No UiAppearanceProvider present, assuming LIGHT theme..."); - applyLightTheme(); - } + private void loadAndApplyDarkTheme() { + loadAndApplyTheme(Theme.DARK, "/css/dark_theme.css"); } - private void applyLightTheme() { - var stylesheet = Optional // - .ofNullable(getClass().getResource("/css/light_theme.bss")) // - .orElse(getClass().getResource("/css/light_theme.css")); + private void loadAndApplyTheme(Theme appTheme, String cssFile) { + var stylesheet = getClass().getResource(cssFile); if (stylesheet == null) { - LOG.warn("Failed to load light_theme stylesheet"); - } else { - Application.setUserAgentStylesheet(stylesheet.toString()); - appearanceProvider.ifPresent(provider -> provider.adjustToTheme(Theme.LIGHT)); - appliedTheme.set(Theme.LIGHT); + throw new IllegalStateException("Cannot find resource %s".formatted(cssFile)); } + Application.setUserAgentStylesheet(stylesheet.toString()); + appearanceProvider.ifPresent(provider -> provider.adjustToTheme(appTheme)); + appliedTheme.set(appTheme); } - private void applyDarkTheme() { - var stylesheet = Optional // - .ofNullable(getClass().getResource("/css/dark_theme.bss")) // - .orElse(getClass().getResource("/css/dark_theme.css")); - if (stylesheet == null) { - LOG.warn("Failed to load dark_theme stylesheet"); - } else { - Application.setUserAgentStylesheet(stylesheet.toString()); - appearanceProvider.ifPresent(provider -> provider.adjustToTheme(Theme.DARK)); - appliedTheme.set(Theme.DARK); - } - } - - public ObjectProperty appliedThemeProperty() { + public ObjectProperty appliedAppThemeProperty() { return appliedTheme; } } From b85780eae9f00964ee6fd88c6db5e0aa5ccda0d9 Mon Sep 17 00:00:00 2001 From: Armin Schrenk Date: Wed, 11 Feb 2026 12:22:22 +0100 Subject: [PATCH 05/15] Disable JavaFX based UiAppearanceProvider for macOS --- .../java/org/cryptomator/ui/fxapp/JfxUiAppearanceProvider.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/org/cryptomator/ui/fxapp/JfxUiAppearanceProvider.java b/src/main/java/org/cryptomator/ui/fxapp/JfxUiAppearanceProvider.java index 151353ff2..b9de27dc0 100644 --- a/src/main/java/org/cryptomator/ui/fxapp/JfxUiAppearanceProvider.java +++ b/src/main/java/org/cryptomator/ui/fxapp/JfxUiAppearanceProvider.java @@ -19,7 +19,6 @@ import java.util.concurrent.atomic.AtomicReference; @DisplayName("JavaFX Color Scheme switcher") @OperatingSystem(OperatingSystem.Value.LINUX) @OperatingSystem(OperatingSystem.Value.WINDOWS) -@OperatingSystem(OperatingSystem.Value.MAC) @Priority(1050) public class JfxUiAppearanceProvider implements UiAppearanceProvider { From fb54d96997a92467cacd9b095125ce5130727561 Mon Sep 17 00:00:00 2001 From: Armin Schrenk Date: Wed, 11 Feb 2026 14:49:56 +0100 Subject: [PATCH 06/15] fix wrong method delegation --- .../java/org/cryptomator/ui/fxapp/JfxUiAppearanceProvider.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/cryptomator/ui/fxapp/JfxUiAppearanceProvider.java b/src/main/java/org/cryptomator/ui/fxapp/JfxUiAppearanceProvider.java index b9de27dc0..a7ac9163e 100644 --- a/src/main/java/org/cryptomator/ui/fxapp/JfxUiAppearanceProvider.java +++ b/src/main/java/org/cryptomator/ui/fxapp/JfxUiAppearanceProvider.java @@ -95,7 +95,7 @@ public class JfxUiAppearanceProvider implements UiAppearanceProvider { public void adjustToTheme(Theme theme) { var impl = realImpl.get(); if (impl != null) { - impl.getSystemTheme(); + impl.adjustToTheme(theme); } } From 34e5d19a0465ad7b28490dc95ed7cc5625444edf Mon Sep 17 00:00:00 2001 From: Armin Schrenk Date: Wed, 11 Feb 2026 15:16:11 +0100 Subject: [PATCH 07/15] prevent resource leak --- .../java/org/cryptomator/ui/fxapp/JfxUiAppearanceProvider.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/cryptomator/ui/fxapp/JfxUiAppearanceProvider.java b/src/main/java/org/cryptomator/ui/fxapp/JfxUiAppearanceProvider.java index a7ac9163e..ca5bec7d4 100644 --- a/src/main/java/org/cryptomator/ui/fxapp/JfxUiAppearanceProvider.java +++ b/src/main/java/org/cryptomator/ui/fxapp/JfxUiAppearanceProvider.java @@ -63,7 +63,7 @@ public class JfxUiAppearanceProvider implements UiAppearanceProvider { uiAppearanceListener.systemAppearanceChanged(newTheme); }; LOG.debug("Register listener for OS theme changes"); - uiAppearanceListeners.compute(uiAppearanceListener, (k, v) -> { + uiAppearanceListeners.computeIfAbsent(uiAppearanceListener, k -> { Platform.runLater(() -> preferences.colorSchemeProperty().addListener(fxChangeListener)); return fxChangeListener; }); From 2a5ef5d99926088818505412c93854aed275f874 Mon Sep 17 00:00:00 2001 From: Armin Schrenk Date: Wed, 11 Feb 2026 15:21:55 +0100 Subject: [PATCH 08/15] log inconsistent state --- .../ui/fxapp/FxApplicationStyle.java | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/cryptomator/ui/fxapp/FxApplicationStyle.java b/src/main/java/org/cryptomator/ui/fxapp/FxApplicationStyle.java index cf524b449..6a7ef934c 100644 --- a/src/main/java/org/cryptomator/ui/fxapp/FxApplicationStyle.java +++ b/src/main/java/org/cryptomator/ui/fxapp/FxApplicationStyle.java @@ -72,14 +72,15 @@ public class FxApplicationStyle { } private void registerAutomaticThemeChange() { - appearanceProvider.ifPresent(provider -> { - try { - provider.addListener(systemInterfaceThemeListener); - } catch (UiAppearanceException e) { - LOG.error("Failed to enable automatic theme switching."); - } - systemInterfaceThemeChanged(provider.getSystemTheme()); - }); + appearanceProvider.ifPresentOrElse(provider -> { + try { + provider.addListener(systemInterfaceThemeListener); + } catch (UiAppearanceException e) { + LOG.error("Failed to enable automatic theme switching."); + } + systemInterfaceThemeChanged(provider.getSystemTheme()); + }, // + () -> LOG.warn("UI theme AUTOMATIC selected, but no supported UiAppearanceProvider present")); } private void systemInterfaceThemeChanged(Theme osTheme) { From 99898b74fb971c16f5a54564fd7188dd10634d2b Mon Sep 17 00:00:00 2001 From: Armin Schrenk Date: Wed, 11 Feb 2026 15:55:19 +0100 Subject: [PATCH 09/15] minor cleanup --- .../org/cryptomator/ui/fxapp/JfxUiAppearanceProvider.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/cryptomator/ui/fxapp/JfxUiAppearanceProvider.java b/src/main/java/org/cryptomator/ui/fxapp/JfxUiAppearanceProvider.java index ca5bec7d4..c121c38d5 100644 --- a/src/main/java/org/cryptomator/ui/fxapp/JfxUiAppearanceProvider.java +++ b/src/main/java/org/cryptomator/ui/fxapp/JfxUiAppearanceProvider.java @@ -27,8 +27,10 @@ public class JfxUiAppearanceProvider implements UiAppearanceProvider { private final AtomicReference realImpl = new AtomicReference<>(null); public void initialize(Platform.Preferences preferences) { - realImpl.compareAndSet(null, new JfxUiAppearanceImpl(preferences)); - LOG.debug("Initialized {} with JavaFX preferences", JfxUiAppearanceImpl.class); + var isSet = realImpl.compareAndSet(null, new JfxUiAppearanceImpl(preferences)); + if (isSet) { + LOG.debug("Initialized {} with JavaFX preferences", JfxUiAppearanceImpl.class); + } } private static class JfxUiAppearanceImpl implements UiAppearanceProvider { From 1629eae5d368c47e5fbf08b27b0bfb56534e1764 Mon Sep 17 00:00:00 2001 From: Armin Schrenk Date: Wed, 11 Feb 2026 15:55:34 +0100 Subject: [PATCH 10/15] [skip ci] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 10f1135e8..2a3cabfa7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ Changes to prior versions can be found on the [Github release page](https://gith * Mark files in-use for Hub vaults ([#4078](https://github.com/cryptomator/cryptomator/pull/4078)) * Accessibility: Adjust app to be used with a screen reader ([#547](https://github.com/cryptomator/cryptomator/issues/547)) * Show Archived Vault Dialog on unlock when Hub returns 410 ([#4081](https://github.com/cryptomator/cryptomator/pull/4081)) +* Automatic app color scheme selection according to OS ([#4134](https://github.com/cryptomator/cryptomator/pull/4134)) ### Changed * Built using JDK 25 ([#4031](https://github.com/cryptomator/cryptomator/issues/4031)) From cf0052b4f5627413ed073d44c4ef4f3df5fafa91 Mon Sep 17 00:00:00 2001 From: Armin Schrenk Date: Mon, 16 Feb 2026 11:07:04 +0100 Subject: [PATCH 11/15] revert lambda processing --- .../cryptomator/ui/fxapp/FxApplicationStyle.java | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/cryptomator/ui/fxapp/FxApplicationStyle.java b/src/main/java/org/cryptomator/ui/fxapp/FxApplicationStyle.java index 6a7ef934c..ec8967340 100644 --- a/src/main/java/org/cryptomator/ui/fxapp/FxApplicationStyle.java +++ b/src/main/java/org/cryptomator/ui/fxapp/FxApplicationStyle.java @@ -47,14 +47,12 @@ public class FxApplicationStyle { } private void appThemeChanged(@SuppressWarnings("unused") ObservableValue observable, UiTheme oldValue, UiTheme newValue) { - if (oldValue == UiTheme.AUTOMATIC && newValue != UiTheme.AUTOMATIC) { - appearanceProvider.ifPresent(service -> { - try { - service.removeListener(systemInterfaceThemeListener); - } catch (UiAppearanceException e) { - LOG.warn("Failed to disable automatic theme switching."); - } - }); + if (oldValue == UiTheme.AUTOMATIC && newValue != UiTheme.AUTOMATIC && appearanceProvider.isPresent()) { + try { + appearanceProvider.get().removeListener(systemInterfaceThemeListener); + } catch (UiAppearanceException e) { + LOG.warn("Failed to disable automatic theme switching."); + } } applyTheme(newValue); } From 83ef9d06d9a60b919dddea372cdc6e4eabd8a231 Mon Sep 17 00:00:00 2001 From: Armin Schrenk Date: Mon, 16 Feb 2026 11:55:03 +0100 Subject: [PATCH 12/15] simplify JfxUiAppearanceProvider and move loading of appearance service into the fx app --- .../launcher/CryptomatorModule.java | 6 -- .../ui/fxapp/FxApplicationModule.java | 10 +- .../ui/fxapp/FxApplicationStyle.java | 5 - .../ui/fxapp/JfxUiAppearanceProvider.java | 101 +++++------------- 4 files changed, 34 insertions(+), 88 deletions(-) diff --git a/src/main/java/org/cryptomator/launcher/CryptomatorModule.java b/src/main/java/org/cryptomator/launcher/CryptomatorModule.java index 42e908df2..5ff8d63ee 100644 --- a/src/main/java/org/cryptomator/launcher/CryptomatorModule.java +++ b/src/main/java/org/cryptomator/launcher/CryptomatorModule.java @@ -4,7 +4,6 @@ import dagger.Module; import dagger.Provides; import org.cryptomator.integrations.autostart.AutoStartProvider; import org.cryptomator.integrations.tray.TrayIntegrationProvider; -import org.cryptomator.integrations.uiappearance.UiAppearanceProvider; import org.cryptomator.ui.fxapp.FxApplicationComponent; import javax.inject.Named; @@ -30,11 +29,6 @@ class CryptomatorModule { return new ArrayBlockingQueue<>(10); } - @Provides - @Singleton - static Optional provideAppearanceProvider() { - return UiAppearanceProvider.get(); - } @Provides @Singleton diff --git a/src/main/java/org/cryptomator/ui/fxapp/FxApplicationModule.java b/src/main/java/org/cryptomator/ui/fxapp/FxApplicationModule.java index 80a261bb8..6b19429b2 100644 --- a/src/main/java/org/cryptomator/ui/fxapp/FxApplicationModule.java +++ b/src/main/java/org/cryptomator/ui/fxapp/FxApplicationModule.java @@ -7,6 +7,7 @@ package org.cryptomator.ui.fxapp; import dagger.Module; import dagger.Provides; +import org.cryptomator.integrations.uiappearance.UiAppearanceProvider; import org.cryptomator.ui.decryptname.DecryptNameComponent; import org.cryptomator.ui.error.ErrorComponent; import org.cryptomator.ui.eventview.EventViewComponent; @@ -26,6 +27,7 @@ import org.cryptomator.ui.vaultoptions.VaultOptionsComponent; import javafx.scene.image.Image; import java.io.IOException; import java.io.InputStream; +import java.util.Optional; @Module(subcomponents = {TrayMenuComponent.class, // DecryptNameComponent.class, // @@ -41,7 +43,7 @@ import java.io.InputStream; ShareVaultComponent.class, // EventViewComponent.class, // RecoveryKeyComponent.class, // - NotificationComponent.class }) + NotificationComponent.class}) abstract class FxApplicationModule { private static Image createImageFromResource(String resourceName) throws IOException { @@ -50,6 +52,12 @@ abstract class FxApplicationModule { } } + @Provides + @FxApplicationScoped + static Optional provideAppearanceProvider() { + return UiAppearanceProvider.get(); + } + @Provides @FxApplicationScoped static TrayMenuComponent provideTrayMenuComponent(TrayMenuComponent.Builder builder) { diff --git a/src/main/java/org/cryptomator/ui/fxapp/FxApplicationStyle.java b/src/main/java/org/cryptomator/ui/fxapp/FxApplicationStyle.java index ec8967340..bd9ae4405 100644 --- a/src/main/java/org/cryptomator/ui/fxapp/FxApplicationStyle.java +++ b/src/main/java/org/cryptomator/ui/fxapp/FxApplicationStyle.java @@ -37,11 +37,6 @@ public class FxApplicationStyle { } public void initialize() { - appearanceProvider.ifPresent(service -> { - if (service instanceof JfxUiAppearanceProvider fxService) { - fxService.initialize(Platform.getPreferences()); - } - }); applyTheme(settings.theme.get()); settings.theme.addListener(this::appThemeChanged); } diff --git a/src/main/java/org/cryptomator/ui/fxapp/JfxUiAppearanceProvider.java b/src/main/java/org/cryptomator/ui/fxapp/JfxUiAppearanceProvider.java index c121c38d5..ffe7093a2 100644 --- a/src/main/java/org/cryptomator/ui/fxapp/JfxUiAppearanceProvider.java +++ b/src/main/java/org/cryptomator/ui/fxapp/JfxUiAppearanceProvider.java @@ -14,7 +14,6 @@ import javafx.application.ColorScheme; import javafx.application.Platform; import javafx.beans.value.ChangeListener; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.atomic.AtomicReference; @DisplayName("JavaFX Color Scheme switcher") @OperatingSystem(OperatingSystem.Value.LINUX) @@ -24,97 +23,47 @@ public class JfxUiAppearanceProvider implements UiAppearanceProvider { private static final Logger LOG = LoggerFactory.getLogger(JfxUiAppearanceProvider.class); - private final AtomicReference realImpl = new AtomicReference<>(null); - - public void initialize(Platform.Preferences preferences) { - var isSet = realImpl.compareAndSet(null, new JfxUiAppearanceImpl(preferences)); - if (isSet) { - LOG.debug("Initialized {} with JavaFX preferences", JfxUiAppearanceImpl.class); - } - } - - private static class JfxUiAppearanceImpl implements UiAppearanceProvider { - - private final Platform.Preferences preferences; - private final ConcurrentHashMap> uiAppearanceListeners = new ConcurrentHashMap<>(); - - private JfxUiAppearanceImpl(Platform.Preferences preferences) { - this.preferences = preferences; - } - - @Override - public Theme getSystemTheme() { - return switch (preferences.getColorScheme()) { - case DARK -> Theme.DARK; - case LIGHT -> Theme.LIGHT; - }; - } - - @Override - public void adjustToTheme(Theme theme) { - //no-op - } - - @Override - public void addListener(UiAppearanceListener uiAppearanceListener) throws UiAppearanceException { - var fxChangeListener = (ChangeListener) (_, _, newScheme) -> { - var newTheme = switch (newScheme) { - case DARK -> Theme.DARK; - case LIGHT -> Theme.LIGHT; - }; - uiAppearanceListener.systemAppearanceChanged(newTheme); - }; - LOG.debug("Register listener for OS theme changes"); - uiAppearanceListeners.computeIfAbsent(uiAppearanceListener, k -> { - Platform.runLater(() -> preferences.colorSchemeProperty().addListener(fxChangeListener)); - return fxChangeListener; - }); - } - - @Override - public void removeListener(UiAppearanceListener uiAppearanceListener) throws UiAppearanceException { - var fxChangeListener = uiAppearanceListeners.remove(uiAppearanceListener); - if (fxChangeListener != null) { - LOG.debug("Removing listener for OS theme changes"); - Platform.runLater(() -> preferences.colorSchemeProperty().removeListener(fxChangeListener)); - } - } - } + private final ConcurrentHashMap> uiAppearanceListeners = new ConcurrentHashMap<>(); + private final Platform.Preferences preferences = Platform.getPreferences(); //Note: this service impl MUST be loaded in the fx application thread - //just delegate methods @Override public Theme getSystemTheme() { - var impl = realImpl.get(); - if (impl != null) { - return impl.getSystemTheme(); - } else { - return Theme.LIGHT; - } + return switch (preferences.getColorScheme()) { + case DARK -> Theme.DARK; + case LIGHT -> Theme.LIGHT; + }; } @Override public void adjustToTheme(Theme theme) { - var impl = realImpl.get(); - if (impl != null) { - impl.adjustToTheme(theme); - } + //no-op } @Override public void addListener(UiAppearanceListener uiAppearanceListener) throws UiAppearanceException { - var impl = realImpl.get(); - if (impl != null) { - impl.addListener(uiAppearanceListener); - } + var fxChangeListener = (ChangeListener) (_, _, newScheme) -> { + var newTheme = switch (newScheme) { + case DARK -> Theme.DARK; + case LIGHT -> Theme.LIGHT; + }; + uiAppearanceListener.systemAppearanceChanged(newTheme); + }; + LOG.debug("Register listener for OS theme changes"); + uiAppearanceListeners.computeIfAbsent(uiAppearanceListener, k -> { + Platform.runLater(() -> preferences.colorSchemeProperty().addListener(fxChangeListener)); + return fxChangeListener; + }); } @Override public void removeListener(UiAppearanceListener uiAppearanceListener) throws UiAppearanceException { - var impl = realImpl.get(); - if (impl != null) { - impl.removeListener(uiAppearanceListener); + var fxChangeListener = uiAppearanceListeners.remove(uiAppearanceListener); + if (fxChangeListener != null) { + LOG.debug("Removing listener for OS theme changes"); + Platform.runLater(() -> preferences.colorSchemeProperty().removeListener(fxChangeListener)); } } - } + + From 9e6bd913cb8b964ebbd9d341c8fff5ceca5365fd Mon Sep 17 00:00:00 2001 From: Armin Schrenk Date: Mon, 16 Feb 2026 12:37:46 +0100 Subject: [PATCH 13/15] refactor FxApplicationStyle use pattern (removeOldListeners, addNewListerner, applyTheme) --- .../ui/fxapp/FxApplicationStyle.java | 65 ++++++++++++------- 1 file changed, 42 insertions(+), 23 deletions(-) diff --git a/src/main/java/org/cryptomator/ui/fxapp/FxApplicationStyle.java b/src/main/java/org/cryptomator/ui/fxapp/FxApplicationStyle.java index bd9ae4405..ccda4a3f8 100644 --- a/src/main/java/org/cryptomator/ui/fxapp/FxApplicationStyle.java +++ b/src/main/java/org/cryptomator/ui/fxapp/FxApplicationStyle.java @@ -12,7 +12,6 @@ import org.slf4j.LoggerFactory; import javax.inject.Inject; import javafx.application.Application; -import javafx.application.Platform; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.beans.value.ObservableValue; @@ -37,45 +36,65 @@ public class FxApplicationStyle { } public void initialize() { - applyTheme(settings.theme.get()); + var uiTheme = settings.theme.get(); + if (uiTheme == UiTheme.AUTOMATIC) { + registerOsThemeListener(); + } + applyTheme(uiTheme); settings.theme.addListener(this::appThemeChanged); } private void appThemeChanged(@SuppressWarnings("unused") ObservableValue observable, UiTheme oldValue, UiTheme newValue) { - if (oldValue == UiTheme.AUTOMATIC && newValue != UiTheme.AUTOMATIC && appearanceProvider.isPresent()) { - try { - appearanceProvider.get().removeListener(systemInterfaceThemeListener); - } catch (UiAppearanceException e) { - LOG.warn("Failed to disable automatic theme switching."); - } + if (oldValue == UiTheme.AUTOMATIC && newValue != UiTheme.AUTOMATIC) { + removeOsThemeListener(); } + + if (newValue == UiTheme.AUTOMATIC) { + registerOsThemeListener(); + } + applyTheme(newValue); } - private void applyTheme(UiTheme theme) { + private void removeOsThemeListener() { + if (appearanceProvider.isPresent()) { + try { + appearanceProvider.get().removeListener(systemInterfaceThemeListener); + } catch (UiAppearanceException e) { + LOG.warn("Failed to disable automatic theme switching.", e); + } + } else { + LOG.debug("Unable to remove listener os theme changes: No supported UiAppearanceProvider present"); + } + } + + private void registerOsThemeListener() { + if (appearanceProvider.isPresent()) { + try { + appearanceProvider.get().addListener(systemInterfaceThemeListener); + } catch (UiAppearanceException e) { + LOG.warn("Failed to enable automatic theme switching.", e); + } + } else { + LOG.warn("Unable to register for os theme changes: No supported UiAppearanceProvider present"); + } + } + + private void applyTheme(UiTheme uiTheme) { if (!licenseHolder.isValidLicense()) { loadAndApplyLightTheme(); } else { - switch (theme) { - case AUTOMATIC -> registerAutomaticThemeChange(); + switch (uiTheme) { + case AUTOMATIC -> { + var osTheme = appearanceProvider.isPresent() ? appearanceProvider.get().getSystemTheme() : Theme.LIGHT; + systemInterfaceThemeChanged(osTheme); + } case LIGHT -> loadAndApplyLightTheme(); case DARK -> loadAndApplyDarkTheme(); } } } - private void registerAutomaticThemeChange() { - appearanceProvider.ifPresentOrElse(provider -> { - try { - provider.addListener(systemInterfaceThemeListener); - } catch (UiAppearanceException e) { - LOG.error("Failed to enable automatic theme switching."); - } - systemInterfaceThemeChanged(provider.getSystemTheme()); - }, // - () -> LOG.warn("UI theme AUTOMATIC selected, but no supported UiAppearanceProvider present")); - } - private void systemInterfaceThemeChanged(Theme osTheme) { switch (osTheme) { case LIGHT -> loadAndApplyLightTheme(); From f753ddc9bee6adcd72d270a3cf741b3c830b3f34 Mon Sep 17 00:00:00 2001 From: Armin Schrenk Date: Mon, 16 Feb 2026 16:05:26 +0100 Subject: [PATCH 14/15] Apply suggestions from code review Co-authored-by: Sebastian Stenzel --- .../org/cryptomator/ui/fxapp/FxApplicationStyle.java | 12 ++++++------ .../ui/fxapp/JfxUiAppearanceProvider.java | 1 - 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/main/java/org/cryptomator/ui/fxapp/FxApplicationStyle.java b/src/main/java/org/cryptomator/ui/fxapp/FxApplicationStyle.java index ccda4a3f8..bfdb22196 100644 --- a/src/main/java/org/cryptomator/ui/fxapp/FxApplicationStyle.java +++ b/src/main/java/org/cryptomator/ui/fxapp/FxApplicationStyle.java @@ -45,12 +45,12 @@ public class FxApplicationStyle { } private void appThemeChanged(@SuppressWarnings("unused") ObservableValue observable, UiTheme oldValue, UiTheme newValue) { - if (oldValue == UiTheme.AUTOMATIC && newValue != UiTheme.AUTOMATIC) { - removeOsThemeListener(); - } - - if (newValue == UiTheme.AUTOMATIC) { + if (oldValue == newValue) { + // no-op + } else if (newValue == UiTheme.AUTOMATIC) { registerOsThemeListener(); + } else if (oldValue == UiTheme.AUTOMATIC) { + removeOsThemeListener(); } applyTheme(newValue); @@ -86,7 +86,7 @@ public class FxApplicationStyle { } else { switch (uiTheme) { case AUTOMATIC -> { - var osTheme = appearanceProvider.isPresent() ? appearanceProvider.get().getSystemTheme() : Theme.LIGHT; + var osTheme = appearanceProvider.map(UiAppearanceProvider::getSystemTheme).orElse(Theme.LIGHT); systemInterfaceThemeChanged(osTheme); } case LIGHT -> loadAndApplyLightTheme(); diff --git a/src/main/java/org/cryptomator/ui/fxapp/JfxUiAppearanceProvider.java b/src/main/java/org/cryptomator/ui/fxapp/JfxUiAppearanceProvider.java index ffe7093a2..56562fc9c 100644 --- a/src/main/java/org/cryptomator/ui/fxapp/JfxUiAppearanceProvider.java +++ b/src/main/java/org/cryptomator/ui/fxapp/JfxUiAppearanceProvider.java @@ -26,7 +26,6 @@ public class JfxUiAppearanceProvider implements UiAppearanceProvider { private final ConcurrentHashMap> uiAppearanceListeners = new ConcurrentHashMap<>(); private final Platform.Preferences preferences = Platform.getPreferences(); //Note: this service impl MUST be loaded in the fx application thread - @Override public Theme getSystemTheme() { return switch (preferences.getColorScheme()) { From a79fd636340bef6fe97469df740b7ec997b8348d Mon Sep 17 00:00:00 2001 From: Armin Schrenk Date: Mon, 16 Feb 2026 16:15:05 +0100 Subject: [PATCH 15/15] [skip ci] update changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6163df43f..4b6e1ea74 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +17,7 @@ Changes to prior versions can be found on the [Github release page](https://gith * Mark files in-use for Hub vaults ([#4078](https://github.com/cryptomator/cryptomator/pull/4078)) * Accessibility: Adjust app to be used with a screen reader ([#547](https://github.com/cryptomator/cryptomator/issues/547)) * Show Archived Vault Dialog on unlock when Hub returns 410 ([#4081](https://github.com/cryptomator/cryptomator/pull/4081)) -* Automatic app color scheme selection according to OS ([#4134](https://github.com/cryptomator/cryptomator/pull/4134)) +* Support automatic app theme selection according to OS theme on Linux ([#4027](https://github.com/cryptomator/cryptomator/issues/4027)) * Admin configuration: Allow overwriting certain app properties by external config file ([#4105](https://github.com/cryptomator/cryptomator/pull/4105)) ### Changed