add a system theme option that follows the os light or dark setting, and make it the default for new installs

This commit is contained in:
Craig Raw
2026-09-16 14:36:22 +02:00
parent de169b180a
commit a573f22aef
15 changed files with 90 additions and 32 deletions
@@ -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;
}
@@ -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<Toggle> 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<Scene> 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()) {
@@ -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<Integer, BlockSummary> blockSummaries = new ConcurrentHashMap<>();
private static Map<Integer, Double> 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");
@@ -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();
@@ -1,5 +1,5 @@
package com.sparrowwallet.sparrow;
public enum Theme {
LIGHT, DARK
LIGHT, DARK, SYSTEM
}
@@ -993,6 +993,7 @@ public class DevicePane extends TitledDescriptionPane {
List<Wallet> 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<Integer> optRange = rangeInputDialog.showAndWait();
@@ -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");
@@ -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<String, Number> {
}
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());
@@ -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 {
@@ -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);
@@ -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");
@@ -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");
@@ -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");
@@ -14,7 +14,7 @@
<Label fx:id="title" text="Sparrow" styleClass="title-label" />
</HBox>
<Region HBox.hgrow="ALWAYS"/>
<DialogImage type="SPARROW" AnchorPane.rightAnchor="0"/>
<DialogImage fx:id="dialogImage" type="SPARROW" AnchorPane.rightAnchor="0"/>
</HBox>
<VBox spacing="10" styleClass="content-area">
<Label text="Sparrow is a Bitcoin wallet with the goal of providing greater transparency and usability on the path to full financial self-sovereignty. It attempts to provide all of the detail about your wallet setup, transactions and UTXOs so that you can transact with a full understanding of your money." wrapText="true" />
@@ -107,6 +107,11 @@
</Menu>
<Menu mnemonicParsing="false" text="Theme">
<items>
<RadioMenuItem mnemonicParsing="false" text="System" toggleGroup="$theme" onAction="#setTheme">
<userData>
<Theme fx:constant="SYSTEM" />
</userData>
</RadioMenuItem>
<RadioMenuItem mnemonicParsing="false" text="Light" toggleGroup="$theme" onAction="#setTheme">
<userData>
<Theme fx:constant="LIGHT" />