diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e3c7fb51e2..756364d627b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv - We added functionality to focus running instance when trying to start a second instance. [#13129](https://github.com/JabRef/jabref/issues/13129) - We added a new setting in the 'Entry Editor' preferences to hide the 'File Annotations' tab when no annotations are available. [#13143](https://github.com/JabRef/jabref/issues/13143) - We added support for multi-file import across different formats. [#13269](https://github.com/JabRef/jabref/issues/13269) +- We added support for dark title bar on Windows. [#11457](https://github.com/JabRef/jabref/issues/11457) ### Changed diff --git a/jabgui/build.gradle.kts b/jabgui/build.gradle.kts index 6c7d221d506..f651f4b848a 100644 --- a/jabgui/build.gradle.kts +++ b/jabgui/build.gradle.kts @@ -24,6 +24,8 @@ dependencies { implementation("org.openjfx:javafx-swing") implementation("org.openjfx:javafx-web") + implementation("com.pixelduke:fxthemes:1.6.0") + implementation("org.slf4j:slf4j-api") implementation("org.tinylog:tinylog-api") implementation("org.tinylog:slf4j-tinylog") diff --git a/jabgui/src/main/java/module-info.java b/jabgui/src/main/java/module-info.java index 0a529f52082..d434689f358 100644 --- a/jabgui/src/main/java/module-info.java +++ b/jabgui/src/main/java/module-info.java @@ -180,6 +180,9 @@ // region: other libraries (alphabetically) // requires cuid; + requires com.dlsc.pdfviewfx; + requires com.pixelduke.fxthemes; + // requires com.sun.jna; // requires dd.plist; requires io.github.adr; // required by okhttp and some AI library @@ -187,6 +190,5 @@ // requires mslinks; requires org.antlr.antlr4.runtime; requires org.libreoffice.uno; - requires com.dlsc.pdfviewfx; // endregion } diff --git a/jabgui/src/main/java/org/jabref/gui/theme/ThemeManager.java b/jabgui/src/main/java/org/jabref/gui/theme/ThemeManager.java index eda4b251f90..d5300c82362 100644 --- a/jabgui/src/main/java/org/jabref/gui/theme/ThemeManager.java +++ b/jabgui/src/main/java/org/jabref/gui/theme/ThemeManager.java @@ -13,9 +13,12 @@ import javafx.application.ColorScheme; import javafx.application.Platform; +import javafx.collections.ListChangeListener; import javafx.scene.Node; import javafx.scene.Scene; import javafx.scene.web.WebEngine; +import javafx.stage.Stage; +import javafx.stage.Window; import org.jabref.gui.WorkspacePreferences; import org.jabref.gui.icon.IconTheme; @@ -26,6 +29,8 @@ import org.jabref.model.util.FileUpdateMonitor; import com.google.common.annotations.VisibleForTesting; +import com.pixelduke.window.ThemeWindowManager; +import com.pixelduke.window.ThemeWindowManagerFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -53,9 +58,11 @@ public class ThemeManager { private final WorkspacePreferences workspacePreferences; private final FileUpdateMonitor fileUpdateMonitor; private final Consumer updateRunner; + private final ThemeWindowManager themeWindowManager; private final StyleSheet baseStyleSheet; private Theme theme; + private boolean isDarkMode; private Scene mainWindowScene; private final Set webEngines = Collections.newSetFromMap(new WeakHashMap<>()); @@ -66,9 +73,13 @@ public ThemeManager(WorkspacePreferences workspacePreferences, this.workspacePreferences = Objects.requireNonNull(workspacePreferences); this.fileUpdateMonitor = Objects.requireNonNull(fileUpdateMonitor); this.updateRunner = Objects.requireNonNull(updateRunner); + this.themeWindowManager = ThemeWindowManagerFactory.create(); this.baseStyleSheet = StyleSheet.create(Theme.BASE_CSS).get(); this.theme = workspacePreferences.getTheme(); + this.isDarkMode = Theme.EMBEDDED_DARK_CSS.equals(this.theme.getName()); + + initializeWindowThemeUpdater(this.isDarkMode); // Watching base CSS only works in development and test scenarios, where the build system exposes the CSS as a // file (e.g. for Gradle run task it will be in build/resources/main/org/jabref/gui/Base.css) @@ -83,6 +94,46 @@ public ThemeManager(WorkspacePreferences workspacePreferences, updateThemeSettings(); } + private void initializeWindowThemeUpdater(boolean darkMode) { + this.isDarkMode = darkMode; + + ListChangeListener windowsListener = change -> { + while (change.next()) { + if (!change.wasAdded()) { + continue; + } + change.getAddedSubList().stream() + .filter(Stage.class::isInstance) + .map(Stage.class::cast) + .forEach(stage -> stage.showingProperty() + .addListener(_ -> applyDarkModeToWindow(stage, isDarkMode))); + } + }; + + Window.getWindows().addListener(windowsListener); + applyDarkModeToAllWindows(darkMode); + + LOGGER.debug("Window theme monitoring initialized"); + } + + private void applyDarkModeToWindow(Stage stage, boolean darkMode) { + if (stage == null || !stage.isShowing()) { + return; + } + + themeWindowManager.setDarkModeForWindowFrame(stage, darkMode); + LOGGER.debug("Applied {} mode to window: {}", darkMode ? "dark" : "light", stage); + } + + private void applyDarkModeToAllWindows(boolean darkMode) { + this.isDarkMode = darkMode; + Window.getWindows().stream() + .filter(Window::isShowing) + .filter(window -> window instanceof Stage) + .map(window -> (Stage) window) + .forEach(stage -> applyDarkModeToWindow(stage, darkMode)); + } + private void updateThemeSettings() { Theme newTheme = Objects.requireNonNull(workspacePreferences.getTheme()); @@ -103,6 +154,12 @@ private void updateThemeSettings() { this.theme = newTheme; LOGGER.info("Theme set to {} with base css {}", newTheme, baseStyleSheet); + boolean isDarkTheme = Theme.EMBEDDED_DARK_CSS.equals(newTheme.getName()); + if (this.isDarkMode != isDarkTheme) { + this.isDarkMode = isDarkTheme; + applyDarkModeToAllWindows(isDarkTheme); + } + this.theme.getAdditionalStylesheet().ifPresent( styleSheet -> addStylesheetToWatchlist(styleSheet, this::additionalCssLiveUpdate));