diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a071b2fa..4b6e1ea74 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)) +* 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 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/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/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/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/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 ee187fd65..bfdb22196 100644 --- a/src/main/java/org/cryptomator/ui/fxapp/FxApplicationStyle.java +++ b/src/main/java/org/cryptomator/ui/fxapp/FxApplicationStyle.java @@ -36,82 +36,91 @@ public class FxApplicationStyle { } public void initialize() { + var uiTheme = settings.theme.get(); + if (uiTheme == UiTheme.AUTOMATIC) { + registerOsThemeListener(); + } + applyTheme(uiTheme); 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 == newValue) { + // no-op + } else if (newValue == UiTheme.AUTOMATIC) { + registerOsThemeListener(); + } else if (oldValue == UiTheme.AUTOMATIC) { + removeOsThemeListener(); + } + + applyTheme(newValue); + } + + private void removeOsThemeListener() { + if (appearanceProvider.isPresent()) { try { appearanceProvider.get().removeListener(systemInterfaceThemeListener); } catch (UiAppearanceException e) { - LOG.error("Failed to disable automatic theme switching."); - } - } - loadSelectedStyleSheet(newValue); - } - - 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(); + 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 systemInterfaceThemeChanged(Theme theme) { - switch (theme) { - case LIGHT -> applyLightTheme(); - case DARK -> applyDarkTheme(); - } - } - - private void applySystemTheme() { + private void registerOsThemeListener() { if (appearanceProvider.isPresent()) { - systemInterfaceThemeChanged(appearanceProvider.get().getSystemTheme()); + try { + appearanceProvider.get().addListener(systemInterfaceThemeListener); + } catch (UiAppearanceException e) { + LOG.warn("Failed to enable automatic theme switching.", e); + } } else { - LOG.warn("No UiAppearanceProvider present, assuming LIGHT theme..."); - applyLightTheme(); + LOG.warn("Unable to register for os theme changes: No supported UiAppearanceProvider present"); } } - private void applyLightTheme() { - var stylesheet = Optional // - .ofNullable(getClass().getResource("/css/light_theme.bss")) // - .orElse(getClass().getResource("/css/light_theme.css")); + private void applyTheme(UiTheme uiTheme) { + if (!licenseHolder.isValidLicense()) { + loadAndApplyLightTheme(); + } else { + switch (uiTheme) { + case AUTOMATIC -> { + var osTheme = appearanceProvider.map(UiAppearanceProvider::getSystemTheme).orElse(Theme.LIGHT); + systemInterfaceThemeChanged(osTheme); + } + case LIGHT -> loadAndApplyLightTheme(); + case DARK -> loadAndApplyDarkTheme(); + } + } + } + + private void systemInterfaceThemeChanged(Theme osTheme) { + switch (osTheme) { + case LIGHT -> loadAndApplyLightTheme(); + case DARK -> loadAndApplyDarkTheme(); + } + } + + private void loadAndApplyLightTheme() { + loadAndApplyTheme(Theme.LIGHT, "/css/light_theme.css"); + } + + private void loadAndApplyDarkTheme() { + loadAndApplyTheme(Theme.DARK, "/css/dark_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; } } 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..56562fc9c --- /dev/null +++ b/src/main/java/org/cryptomator/ui/fxapp/JfxUiAppearanceProvider.java @@ -0,0 +1,68 @@ +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 org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javafx.application.ColorScheme; +import javafx.application.Platform; +import javafx.beans.value.ChangeListener; +import java.util.concurrent.ConcurrentHashMap; + +@DisplayName("JavaFX Color Scheme switcher") +@OperatingSystem(OperatingSystem.Value.LINUX) +@OperatingSystem(OperatingSystem.Value.WINDOWS) +@Priority(1050) +public class JfxUiAppearanceProvider implements UiAppearanceProvider { + + private static final Logger LOG = LoggerFactory.getLogger(JfxUiAppearanceProvider.class); + + 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()) { + 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)); + } + } +} + + 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); }