From a573f22aefffbe54dad1eac53b79c35459258d4c Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Wed, 16 Sep 2026 14:36:22 +0200 Subject: [PATCH] add a system theme option that follows the os light or dark setting, and make it the default for new installs --- .../sparrow/AboutController.java | 17 +++++++++ .../sparrowwallet/sparrow/AppController.java | 30 +++++++++++----- .../sparrowwallet/sparrow/AppServices.java | 36 +++++++++++++++++-- .../sparrowwallet/sparrow/SparrowDesktop.java | 1 + .../java/com/sparrowwallet/sparrow/Theme.java | 2 +- .../sparrow/control/DevicePane.java | 1 + .../sparrow/control/DialogImage.java | 4 +-- .../control/MempoolSizeFeeRatesChart.java | 4 +-- .../sparrow/control/QREncoding.java | 4 +-- .../sparrow/control/TransactionDiagram.java | 4 +-- .../sparrow/control/WalletIcon.java | 5 +-- .../sparrow/control/WalletModelImage.java | 5 +-- .../sparrow/wallet/PaymentController.java | 2 +- .../com/sparrowwallet/sparrow/about.fxml | 2 +- .../com/sparrowwallet/sparrow/app.fxml | 5 +++ 15 files changed, 90 insertions(+), 32 deletions(-) diff --git a/src/main/java/com/sparrowwallet/sparrow/AboutController.java b/src/main/java/com/sparrowwallet/sparrow/AboutController.java index 95cd881d..6a4d78a4 100644 --- a/src/main/java/com/sparrowwallet/sparrow/AboutController.java +++ b/src/main/java/com/sparrowwallet/sparrow/AboutController.java @@ -1,5 +1,6 @@ package com.sparrowwallet.sparrow; +import com.sparrowwallet.sparrow.control.DialogImage; import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.scene.control.Label; @@ -11,10 +12,26 @@ public class AboutController { @FXML private Label title; + @FXML + private DialogImage dialogImage; + public void initializeView() { title.setText(SparrowWallet.APP_NAME + " " + SparrowWallet.APP_VERSION + SparrowWallet.APP_VERSION_SUFFIX); } + public void refreshTheme() { + String darkCss = AppServices.class.getResource("darktheme.css").toExternalForm(); + if(AppServices.isDarkTheme()) { + if(!stage.getScene().getStylesheets().contains(darkCss)) { + stage.getScene().getStylesheets().add(darkCss); + } + } else { + stage.getScene().getStylesheets().remove(darkCss); + } + + dialogImage.refresh(); + } + public void setStage(Stage stage) { this.stage = stage; } diff --git a/src/main/java/com/sparrowwallet/sparrow/AppController.java b/src/main/java/com/sparrowwallet/sparrow/AppController.java index 3a7927aa..87f75cfb 100644 --- a/src/main/java/com/sparrowwallet/sparrow/AppController.java +++ b/src/main/java/com/sparrowwallet/sparrow/AppController.java @@ -388,8 +388,8 @@ public class AppController implements Initializable { Theme configTheme = Config.get().getTheme(); if(configTheme == null) { - configTheme = Theme.LIGHT; - Config.get().setTheme(Theme.LIGHT); + configTheme = Theme.SYSTEM; + Config.get().setTheme(Theme.SYSTEM); } final Theme selectedTheme = configTheme; Optional selectedThemeToggle = theme.getToggles().stream().filter(toggle -> selectedTheme.equals(toggle.getUserData())).findFirst(); @@ -578,6 +578,8 @@ public class AppController implements Initializable { controller.initializeView(); setStageIcon(stage); stage.setOnShowing(event -> { + //The macOS application menu reuses a single About stage, so the theme may have changed since it was created + controller.refreshTheme(); AppServices.moveToActiveWindowScreen(stage, 600, 460); }); @@ -1374,6 +1376,7 @@ public class AppController implements Initializable { File walletFile = Storage.getExistingWallet(wallet.getName()); if(walletFile != null) { Alert alert = new Alert(Alert.AlertType.CONFIRMATION); + alert.initOwner(rootStack.getScene().getWindow()); AppServices.setStageIcon(alert.getDialogPane().getScene().getWindow()); alert.setTitle("Existing wallet found"); alert.setHeaderText("Replace existing wallet?"); @@ -2643,7 +2646,7 @@ public class AppController implements Initializable { Config.get().setTheme(selectedTheme); } - EventManager.get().post(new ThemeChangedEvent(selectedTheme)); + EventManager.get().post(new ThemeChangedEvent(AppServices.getActiveTheme())); } private void serverToggleStartAnimation() { @@ -2747,13 +2750,24 @@ public class AppController implements Initializable { @Subscribe public void themeChanged(ThemeChangedEvent event) { + //Owned dialogs follow the main window stylesheets, but these non-modal dialogs have no owner + List scenes = new ArrayList<>(List.of(tabs.getScene())); + if(sendToManyDialog != null) { + scenes.add(sendToManyDialog.getDialogPane().getScene()); + } + if(searchWalletDialog != null) { + scenes.add(searchWalletDialog.getDialogPane().getScene()); + } + String darkCss = getClass().getResource("darktheme.css").toExternalForm(); - if(event.getTheme() == Theme.DARK) { - if(!tabs.getScene().getStylesheets().contains(darkCss)) { - tabs.getScene().getStylesheets().add(darkCss); + for(Scene scene : scenes) { + if(event.getTheme() == Theme.DARK) { + if(!scene.getStylesheets().contains(darkCss)) { + scene.getStylesheets().add(darkCss); + } + } else { + scene.getStylesheets().remove(darkCss); } - } else { - tabs.getScene().getStylesheets().remove(darkCss); } for(Tab tab : tabs.getTabs()) { diff --git a/src/main/java/com/sparrowwallet/sparrow/AppServices.java b/src/main/java/com/sparrowwallet/sparrow/AppServices.java index 3fab2342..157b29be 100644 --- a/src/main/java/com/sparrowwallet/sparrow/AppServices.java +++ b/src/main/java/com/sparrowwallet/sparrow/AppServices.java @@ -30,6 +30,7 @@ import com.sparrowwallet.sparrow.net.*; import io.reactivex.rxjavafx.schedulers.JavaFxScheduler; import io.reactivex.subjects.PublishSubject; import javafx.application.Application; +import javafx.application.ColorScheme; import javafx.application.Platform; import javafx.beans.property.BooleanProperty; import javafx.beans.property.SimpleBooleanProperty; @@ -132,6 +133,8 @@ public class AppServices { private static volatile ChainTip announcedTip; + private static volatile boolean systemDarkTheme; + private static final Map blockSummaries = new ConcurrentHashMap<>(); private static Map targetBlockFeeRates; @@ -956,13 +959,42 @@ public class AppServices { return getInteractionServices().showAlert(title, content, alertType, graphic, buttons); } + public static void monitorSystemTheme() { + try { + Platform.Preferences preferences = Platform.getPreferences(); + systemDarkTheme = preferences.getColorScheme() == ColorScheme.DARK; + preferences.colorSchemeProperty().addListener((observable, oldValue, colorScheme) -> { + systemDarkTheme = colorScheme == ColorScheme.DARK; + if(Config.get().getTheme() == null || Config.get().getTheme() == Theme.SYSTEM) { + EventManager.get().post(new ThemeChangedEvent(getActiveTheme())); + } + }); + } catch(Exception e) { + log.warn("Could not read the system color scheme", e); + } + } + + public static Theme getActiveTheme() { + Theme theme = Config.get().getTheme(); + if(theme == null || theme == Theme.SYSTEM) { + return systemDarkTheme ? Theme.DARK : Theme.LIGHT; + } + + return theme; + } + + public static boolean isDarkTheme() { + return getActiveTheme() == Theme.DARK; + } + public static void setStageIcon(Window window) { Stage stage = (Stage)window; stage.getIcons().add(getWindowIcon()); if(stage.getScene() != null) { - if(Config.get().getTheme() == Theme.DARK) { - stage.getScene().getStylesheets().add(AppServices.class.getResource("darktheme.css").toExternalForm()); + String darkCss = AppServices.class.getResource("darktheme.css").toExternalForm(); + if(isDarkTheme() && !stage.getScene().getStylesheets().contains(darkCss)) { + stage.getScene().getStylesheets().add(darkCss); } if(Config.get().isChunkAddresses()) { stage.getScene().getRoot().getStyleClass().add("chunk-addresses"); diff --git a/src/main/java/com/sparrowwallet/sparrow/SparrowDesktop.java b/src/main/java/com/sparrowwallet/sparrow/SparrowDesktop.java index 81ea740c..4fe355e0 100644 --- a/src/main/java/com/sparrowwallet/sparrow/SparrowDesktop.java +++ b/src/main/java/com/sparrowwallet/sparrow/SparrowDesktop.java @@ -46,6 +46,7 @@ public class SparrowDesktop extends Application { URL.setURLStreamHandlerFactory(protocol -> WalletIcon.PROTOCOL.equals(protocol) ? new WalletIcon.WalletIconStreamHandler() : null); AppServices.initialize(this); + AppServices.monitorSystemTheme(); boolean createNewWallet = false; Mode mode = Config.get().getMode(); diff --git a/src/main/java/com/sparrowwallet/sparrow/Theme.java b/src/main/java/com/sparrowwallet/sparrow/Theme.java index ee84ef92..8d9d17ce 100644 --- a/src/main/java/com/sparrowwallet/sparrow/Theme.java +++ b/src/main/java/com/sparrowwallet/sparrow/Theme.java @@ -1,5 +1,5 @@ package com.sparrowwallet.sparrow; public enum Theme { - LIGHT, DARK + LIGHT, DARK, SYSTEM } diff --git a/src/main/java/com/sparrowwallet/sparrow/control/DevicePane.java b/src/main/java/com/sparrowwallet/sparrow/control/DevicePane.java index c9e8a9cb..0db1bee9 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/DevicePane.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/DevicePane.java @@ -993,6 +993,7 @@ public class DevicePane extends TitledDescriptionPane { List wallets = new ArrayList<>(); RangeInputDialog rangeInputDialog = new RangeInputDialog(StandardAccount.ACCOUNT_0.getAccountNumber(), StandardAccount.ACCOUNT_30.getAccountNumber(), StandardAccount.ACCOUNT_10.getAccountNumber()); + rangeInputDialog.initOwner(this.getScene().getWindow()); rangeInputDialog.setTitle("Choose number of accounts"); rangeInputDialog.setHeaderText("Enter the number of additional accounts to scan for existing funds.\n\nThis may take a few minutes depending on how many accounts are selected."); Optional optRange = rangeInputDialog.showAndWait(); diff --git a/src/main/java/com/sparrowwallet/sparrow/control/DialogImage.java b/src/main/java/com/sparrowwallet/sparrow/control/DialogImage.java index 9a5d43c3..5a684ee8 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/DialogImage.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/DialogImage.java @@ -1,8 +1,6 @@ package com.sparrowwallet.sparrow.control; import com.sparrowwallet.sparrow.AppServices; -import com.sparrowwallet.sparrow.Theme; -import com.sparrowwallet.sparrow.io.Config; import javafx.beans.NamedArg; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty; @@ -43,7 +41,7 @@ public class DialogImage extends StackPane { protected void refresh(Type type) { SVGImage svgImage; - if(Config.get().getTheme() == Theme.DARK) { + if(AppServices.isDarkTheme()) { svgImage = loadSVGImage("/image/dialog/" + type.name().toLowerCase(Locale.ROOT) + "-invert.svg"); } else { svgImage = loadSVGImage("/image/dialog/" + type.name().toLowerCase(Locale.ROOT) + ".svg"); diff --git a/src/main/java/com/sparrowwallet/sparrow/control/MempoolSizeFeeRatesChart.java b/src/main/java/com/sparrowwallet/sparrow/control/MempoolSizeFeeRatesChart.java index cd335216..7a9c28ef 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/MempoolSizeFeeRatesChart.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/MempoolSizeFeeRatesChart.java @@ -2,9 +2,7 @@ package com.sparrowwallet.sparrow.control; import com.sparrowwallet.drongo.OsType; import com.sparrowwallet.sparrow.AppServices; -import com.sparrowwallet.sparrow.Theme; import com.sparrowwallet.sparrow.glyphfont.FontAwesome5; -import com.sparrowwallet.sparrow.io.Config; import com.sparrowwallet.sparrow.net.MempoolRateSize; import javafx.application.Platform; import javafx.beans.NamedArg; @@ -63,7 +61,7 @@ public class MempoolSizeFeeRatesChart extends StackedAreaChart { } scenePane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm()); - if(Config.get().getTheme() == Theme.DARK) { + if(AppServices.isDarkTheme()) { scenePane.getStylesheets().add(AppServices.class.getResource("darktheme.css").toExternalForm()); } scenePane.getStylesheets().add(AppServices.class.getResource("wallet/wallet.css").toExternalForm()); diff --git a/src/main/java/com/sparrowwallet/sparrow/control/QREncoding.java b/src/main/java/com/sparrowwallet/sparrow/control/QREncoding.java index b309ec85..8a256449 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/QREncoding.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/QREncoding.java @@ -1,9 +1,7 @@ package com.sparrowwallet.sparrow.control; import com.sparrowwallet.sparrow.AppServices; -import com.sparrowwallet.sparrow.Theme; import com.sparrowwallet.sparrow.glyphfont.FontAwesome5; -import com.sparrowwallet.sparrow.io.Config; import javafx.geometry.Insets; import javafx.scene.Node; import org.controlsfx.glyphfont.Glyph; @@ -31,7 +29,7 @@ public enum QREncoding { public Node getSVGImage() { try { - URL url = AppServices.class.getResource("/image/qrencoding/" + getName().toLowerCase(Locale.ROOT) + "-icon" + (Config.get().getTheme() == Theme.DARK ? "-invert" : "") + ".svg"); + URL url = AppServices.class.getResource("/image/qrencoding/" + getName().toLowerCase(Locale.ROOT) + "-icon" + (AppServices.isDarkTheme() ? "-invert" : "") + ".svg"); if(url != null) { return SVGLoader.load(url); } else { diff --git a/src/main/java/com/sparrowwallet/sparrow/control/TransactionDiagram.java b/src/main/java/com/sparrowwallet/sparrow/control/TransactionDiagram.java index 69026e1d..dab45740 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/TransactionDiagram.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/TransactionDiagram.java @@ -98,7 +98,7 @@ public class TransactionDiagram extends GridPane { } scenePane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm()); - if(Config.get().getTheme() == Theme.DARK) { + if(AppServices.isDarkTheme()) { scenePane.getStylesheets().add(AppServices.class.getResource("darktheme.css").toExternalForm()); } scenePane.getStylesheets().add(AppServices.class.getResource("wallet/wallet.css").toExternalForm()); @@ -998,7 +998,7 @@ public class TransactionDiagram extends GridPane { transactionDiagram.setFinal(true); transactionDiagram.setExpanded(isExpanded()); transactionDiagram.setBackground(new Background(new BackgroundFill(Color.TRANSPARENT, null, null))); - transactionDiagram.setStyle("-fx-text-background-color: " + (Config.get().getTheme() == Theme.DARK ? "#ffffff" : "#000000")); + transactionDiagram.setStyle("-fx-text-background-color: " + (AppServices.isDarkTheme() ? "#ffffff" : "#000000")); updateDerivedDiagram(transactionDiagram); Scene scene = new Scene(transactionDiagram); scene.setFill(Color.TRANSPARENT); diff --git a/src/main/java/com/sparrowwallet/sparrow/control/WalletIcon.java b/src/main/java/com/sparrowwallet/sparrow/control/WalletIcon.java index 710cb98b..824dddb9 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/WalletIcon.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/WalletIcon.java @@ -2,9 +2,7 @@ package com.sparrowwallet.sparrow.control; import com.sparrowwallet.drongo.wallet.*; import com.sparrowwallet.sparrow.AppServices; -import com.sparrowwallet.sparrow.Theme; import com.sparrowwallet.sparrow.glyphfont.FontAwesome5; -import com.sparrowwallet.sparrow.io.Config; import com.sparrowwallet.sparrow.io.ImageUtils; import com.sparrowwallet.sparrow.io.Storage; import javafx.application.Platform; @@ -12,7 +10,6 @@ import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.geometry.Pos; import javafx.scene.image.Image; -import javafx.scene.image.ImageView; import javafx.scene.layout.StackPane; import javafx.scene.paint.ImagePattern; import javafx.scene.shape.Circle; @@ -73,7 +70,7 @@ public class WalletIcon extends StackPane { WalletModel walletModel = keystore.getWalletModel(); SVGImage svgImage; - if(Config.get().getTheme() == Theme.DARK) { + if(AppServices.isDarkTheme()) { svgImage = loadSVGImage("/image/walletmodel/" + walletModel.getType() + "-icon-invert.svg"); } else { svgImage = loadSVGImage("/image/walletmodel/" + walletModel.getType() + "-icon.svg"); diff --git a/src/main/java/com/sparrowwallet/sparrow/control/WalletModelImage.java b/src/main/java/com/sparrowwallet/sparrow/control/WalletModelImage.java index 10263a41..d7d475ff 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/WalletModelImage.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/WalletModelImage.java @@ -2,12 +2,9 @@ package com.sparrowwallet.sparrow.control; import com.sparrowwallet.drongo.wallet.WalletModel; import com.sparrowwallet.sparrow.AppServices; -import com.sparrowwallet.sparrow.Theme; -import com.sparrowwallet.sparrow.io.Config; import javafx.beans.NamedArg; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty; -import javafx.scene.image.Image; import javafx.scene.layout.StackPane; import org.girod.javafx.svgimage.SVGImage; import org.girod.javafx.svgimage.SVGLoader; @@ -51,7 +48,7 @@ public class WalletModelImage extends StackPane { protected void refresh(WalletModel walletModel) { SVGImage svgImage; - if(Config.get().getTheme() == Theme.DARK) { + if(AppServices.isDarkTheme()) { svgImage = loadSVGImage("/image/walletmodel/" + walletModel.getType() + "-invert.svg"); } else { svgImage = loadSVGImage("/image/walletmodel/" + walletModel.getType() + ".svg"); diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/PaymentController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/PaymentController.java index 456432a9..dfdd4348 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/PaymentController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/PaymentController.java @@ -903,7 +903,7 @@ public class PaymentController extends WalletFormController implements Initializ public static Node getBitcoinCharacter() { try { URL url; - if(Config.get().getTheme() == Theme.DARK) { + if(AppServices.isDarkTheme()) { url = AppServices.class.getResource("/image/bitcoin-character-invert.svg"); } else { url = AppServices.class.getResource("/image/bitcoin-character.svg"); diff --git a/src/main/resources/com/sparrowwallet/sparrow/about.fxml b/src/main/resources/com/sparrowwallet/sparrow/about.fxml index 52794707..fab0605e 100644 --- a/src/main/resources/com/sparrowwallet/sparrow/about.fxml +++ b/src/main/resources/com/sparrowwallet/sparrow/about.fxml @@ -14,7 +14,7 @@