From f175139fd3917541e0e084841218623bcab828e1 Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Mon, 14 Aug 2023 16:11:55 +0200 Subject: [PATCH] add wallet summary dialog --- .../sparrowwallet/sparrow/AppController.java | 19 ++ .../sparrow/control/CoinTreeTable.java | 14 ++ .../sparrow/control/EntryCell.java | 6 + .../sparrow/control/FiatCell.java | 94 ++++++++++ .../sparrow/control/SearchWalletDialog.java | 2 + .../sparrow/control/WalletSummaryDialog.java | 171 ++++++++++++++++++ .../wallet/WalletTransactionsEntry.java | 20 +- .../com/sparrowwallet/sparrow/app.fxml | 1 + .../sparrowwallet/sparrow/wallet/wallet.css | 8 + 9 files changed, 331 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/sparrowwallet/sparrow/control/FiatCell.java create mode 100644 src/main/java/com/sparrowwallet/sparrow/control/WalletSummaryDialog.java diff --git a/src/main/java/com/sparrowwallet/sparrow/AppController.java b/src/main/java/com/sparrowwallet/sparrow/AppController.java index 87102f15..fca62e8f 100644 --- a/src/main/java/com/sparrowwallet/sparrow/AppController.java +++ b/src/main/java/com/sparrowwallet/sparrow/AppController.java @@ -166,6 +166,9 @@ public class AppController implements Initializable { @FXML private MenuItem lockAllWallets; + @FXML + private MenuItem showWalletSummary; + @FXML private MenuItem searchWallet; @@ -380,6 +383,7 @@ public class AppController implements Initializable { deleteWallet.disableProperty().bind(exportWallet.disableProperty()); closeTab.setDisable(true); lockWallet.setDisable(true); + showWalletSummary.disableProperty().bind(exportWallet.disableProperty()); searchWallet.disableProperty().bind(exportWallet.disableProperty()); refreshWallet.disableProperty().bind(Bindings.or(exportWallet.disableProperty(), Bindings.or(serverToggle.disableProperty(), AppServices.onlineProperty().not()))); sendToMany.disableProperty().bind(exportWallet.disableProperty()); @@ -1469,6 +1473,21 @@ public class AppController implements Initializable { } } + public void showWalletSummary(ActionEvent event) { + Tab selectedTab = tabs.getSelectionModel().getSelectedItem(); + if(selectedTab != null) { + TabData tabData = (TabData) selectedTab.getUserData(); + if(tabData instanceof WalletTabData) { + TabPane subTabs = (TabPane) selectedTab.getContent(); + List walletForms = subTabs.getTabs().stream().map(subTab -> ((WalletTabData)subTab.getUserData()).getWalletForm()).collect(Collectors.toList()); + if(!walletForms.isEmpty()) { + WalletSummaryDialog walletSummaryDialog = new WalletSummaryDialog(walletForms); + walletSummaryDialog.showAndWait(); + } + } + } + } + public void refreshWallet(ActionEvent event) { WalletForm selectedWalletForm = getSelectedWalletForm(); if(selectedWalletForm != null) { diff --git a/src/main/java/com/sparrowwallet/sparrow/control/CoinTreeTable.java b/src/main/java/com/sparrowwallet/sparrow/control/CoinTreeTable.java index 87f68c0f..911a25be 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/CoinTreeTable.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/CoinTreeTable.java @@ -2,6 +2,7 @@ package com.sparrowwallet.sparrow.control; import com.sparrowwallet.drongo.BitcoinUnit; import com.sparrowwallet.drongo.wallet.Wallet; +import com.sparrowwallet.sparrow.CurrencyRate; import com.sparrowwallet.sparrow.UnitFormat; import com.sparrowwallet.sparrow.AppServices; import com.sparrowwallet.sparrow.EventManager; @@ -28,6 +29,7 @@ import java.util.Optional; public class CoinTreeTable extends TreeTableView { private BitcoinUnit bitcoinUnit; private UnitFormat unitFormat; + private CurrencyRate currencyRate; public BitcoinUnit getBitcoinUnit() { return bitcoinUnit; @@ -64,6 +66,18 @@ public class CoinTreeTable extends TreeTableView { } } + public CurrencyRate getCurrencyRate() { + return currencyRate; + } + + public void setCurrencyRate(CurrencyRate currencyRate) { + this.currencyRate = currencyRate; + + if(!getChildren().isEmpty()) { + refresh(); + } + } + public void updateHistoryStatus(WalletHistoryStatusEvent event) { if(getRoot() != null) { Entry entry = getRoot().getValue(); diff --git a/src/main/java/com/sparrowwallet/sparrow/control/EntryCell.java b/src/main/java/com/sparrowwallet/sparrow/control/EntryCell.java index 9aec1c64..175a3b9e 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/EntryCell.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/EntryCell.java @@ -790,6 +790,8 @@ public class EntryCell extends TreeTableCell implements Confirmati cell.getStyleClass().remove("transaction-row"); cell.getStyleClass().remove("node-row"); cell.getStyleClass().remove("utxo-row"); + cell.getStyleClass().remove("unconfirmed-row"); + cell.getStyleClass().remove("summary-row"); cell.getStyleClass().remove("address-cell"); cell.getStyleClass().remove("hashindex-row"); cell.getStyleClass().remove("confirming"); @@ -824,6 +826,10 @@ public class EntryCell extends TreeTableCell implements Confirmati if(hashIndexEntry.isSpent()) { cell.getStyleClass().add("spent"); } + } else if(entry instanceof WalletSummaryDialog.UnconfirmedEntry) { + cell.getStyleClass().add("unconfirmed-row"); + } else if(entry instanceof WalletSummaryDialog.SummaryEntry) { + cell.getStyleClass().add("summary-row"); } } } diff --git a/src/main/java/com/sparrowwallet/sparrow/control/FiatCell.java b/src/main/java/com/sparrowwallet/sparrow/control/FiatCell.java new file mode 100644 index 00000000..c1b24454 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/control/FiatCell.java @@ -0,0 +1,94 @@ +package com.sparrowwallet.sparrow.control; + +import com.sparrowwallet.drongo.protocol.Transaction; +import com.sparrowwallet.sparrow.CurrencyRate; +import com.sparrowwallet.sparrow.UnitFormat; +import com.sparrowwallet.sparrow.wallet.Entry; +import javafx.scene.control.ContextMenu; +import javafx.scene.control.MenuItem; +import javafx.scene.control.Tooltip; +import javafx.scene.control.TreeTableCell; +import javafx.scene.input.Clipboard; +import javafx.scene.input.ClipboardContent; +import org.controlsfx.tools.Platform; + +import java.math.BigDecimal; +import java.util.Currency; + +public class FiatCell extends TreeTableCell { + private final Tooltip tooltip; + private final FiatContextMenu contextMenu; + + public FiatCell() { + super(); + tooltip = new Tooltip(); + contextMenu = new FiatContextMenu(); + getStyleClass().add("coin-cell"); + if(Platform.getCurrent() == Platform.OSX) { + getStyleClass().add("number-field"); + } + } + + @Override + protected void updateItem(Number amount, boolean empty) { + super.updateItem(amount, empty); + + if(empty || amount == null) { + setText(null); + setGraphic(null); + setTooltip(null); + setContextMenu(null); + } else { + Entry entry = getTreeTableView().getTreeItem(getIndex()).getValue(); + EntryCell.applyRowStyles(this, entry); + + CoinTreeTable coinTreeTable = (CoinTreeTable) getTreeTableView(); + UnitFormat format = coinTreeTable.getUnitFormat(); + CurrencyRate currencyRate = coinTreeTable.getCurrencyRate(); + + if(currencyRate != null && currencyRate.isAvailable()) { + Currency currency = currencyRate.getCurrency(); + double btcRate = currencyRate.getBtcRate(); + + BigDecimal satsBalance = BigDecimal.valueOf(amount.longValue()); + BigDecimal btcBalance = satsBalance.divide(BigDecimal.valueOf(Transaction.SATOSHIS_PER_BITCOIN)); + BigDecimal fiatBalance = btcBalance.multiply(BigDecimal.valueOf(btcRate)); + + String label = format.formatCurrencyValue(fiatBalance.doubleValue()); + tooltip.setText("1 BTC = " + currency.getSymbol() + " " + format.formatCurrencyValue(btcRate)); + + setText(label); + setGraphic(null); + setTooltip(tooltip); + setContextMenu(contextMenu); + } else { + setText(null); + setGraphic(null); + setTooltip(null); + setContextMenu(null); + } + } + } + + private class FiatContextMenu extends ContextMenu { + public FiatContextMenu() { + MenuItem copyValue = new MenuItem("Copy Value"); + copyValue.setOnAction(AE -> { + hide(); + ClipboardContent content = new ClipboardContent(); + content.putString(getText()); + Clipboard.getSystemClipboard().setContent(content); + }); + + MenuItem copyRate = new MenuItem("Copy Rate"); + copyRate.setOnAction(AE -> { + hide(); + ClipboardContent content = new ClipboardContent(); + content.putString(getTooltip().getText()); + Clipboard.getSystemClipboard().setContent(content); + }); + + getItems().addAll(copyValue, copyRate); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/sparrowwallet/sparrow/control/SearchWalletDialog.java b/src/main/java/com/sparrowwallet/sparrow/control/SearchWalletDialog.java index 6e094377..ef2ebd32 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/SearchWalletDialog.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/SearchWalletDialog.java @@ -140,6 +140,8 @@ public class SearchWalletDialog extends Dialog { setResizable(true); + AppServices.moveToActiveWindowScreen(this); + Platform.runLater(search::requestFocus); } diff --git a/src/main/java/com/sparrowwallet/sparrow/control/WalletSummaryDialog.java b/src/main/java/com/sparrowwallet/sparrow/control/WalletSummaryDialog.java new file mode 100644 index 00000000..1d0bd575 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/control/WalletSummaryDialog.java @@ -0,0 +1,171 @@ +package com.sparrowwallet.sparrow.control; + +import com.sparrowwallet.drongo.wallet.Wallet; +import com.sparrowwallet.sparrow.AppServices; +import com.sparrowwallet.sparrow.CurrencyRate; +import com.sparrowwallet.sparrow.io.Config; +import com.sparrowwallet.sparrow.net.ExchangeSource; +import com.sparrowwallet.sparrow.wallet.Entry; +import com.sparrowwallet.sparrow.wallet.Function; +import com.sparrowwallet.sparrow.wallet.WalletForm; +import com.sparrowwallet.sparrow.wallet.WalletTransactionsEntry; +import javafx.beans.property.ReadOnlyObjectWrapper; +import javafx.geometry.Side; +import javafx.scene.chart.NumberAxis; +import javafx.scene.control.*; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.layout.HBox; +import javafx.scene.layout.VBox; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +public class WalletSummaryDialog extends Dialog { + public WalletSummaryDialog(List walletForms) { + if(walletForms.isEmpty()) { + throw new IllegalArgumentException("No wallets selected to summarize"); + } + + Wallet masterWallet = walletForms.get(0).getMasterWallet(); + + final DialogPane dialogPane = getDialogPane(); + dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm()); + dialogPane.getStylesheets().add(AppServices.class.getResource("dialog.css").toExternalForm()); + dialogPane.getStylesheets().add(AppServices.class.getResource("wallet/wallet.css").toExternalForm()); + dialogPane.getStylesheets().add(AppServices.class.getResource("wallet/transactions.css").toExternalForm()); + + AppServices.setStageIcon(dialogPane.getScene().getWindow()); + dialogPane.setHeaderText("Wallet Summary for " + masterWallet.getName()); + + Image image = new Image("image/sparrow-small.png", 50, 50, false, false); + if(!image.isError()) { + ImageView imageView = new ImageView(); + imageView.setSmooth(false); + imageView.setImage(image); + dialogPane.setGraphic(imageView); + } + + HBox hBox = new HBox(40); + + CoinTreeTable table = new CoinTreeTable(); + + TreeTableColumn nameColumn = new TreeTableColumn<>("Wallet"); + nameColumn.setCellValueFactory((TreeTableColumn.CellDataFeatures param) -> { + return new ReadOnlyObjectWrapper<>(param.getValue().getValue().getLabel()); + }); + nameColumn.setCellFactory(p -> new LabelCell()); + table.getColumns().add(nameColumn); + + TreeTableColumn balanceColumn = new TreeTableColumn<>("Balance"); + balanceColumn.setCellValueFactory((TreeTableColumn.CellDataFeatures param) -> { + return new ReadOnlyObjectWrapper<>(param.getValue().getValue().getValue()); + }); + balanceColumn.setCellFactory(p -> new CoinCell()); + table.getColumns().add(balanceColumn); + table.setUnitFormat(masterWallet); + + CurrencyRate currencyRate = AppServices.getFiatCurrencyExchangeRate(); + if(currencyRate != null && currencyRate.isAvailable() && Config.get().getExchangeSource() != ExchangeSource.NONE) { + TreeTableColumn fiatColumn = new TreeTableColumn<>(currencyRate.getCurrency().getSymbol()); + fiatColumn.setCellValueFactory((TreeTableColumn.CellDataFeatures param) -> { + return new ReadOnlyObjectWrapper<>(param.getValue().getValue().getValue()); + }); + fiatColumn.setCellFactory(p -> new FiatCell()); + table.getColumns().add(fiatColumn); + table.setCurrencyRate(currencyRate); + } + + SummaryEntry rootEntry = new SummaryEntry(walletForms); + TreeItem rootItem = new TreeItem<>(rootEntry); + for(Entry childEntry : rootEntry.getChildren()) { + TreeItem childItem = new TreeItem<>(childEntry); + rootItem.getChildren().add(childItem); + childItem.getChildren().add(new TreeItem<>(new UnconfirmedEntry((WalletTransactionsEntry)childEntry))); + } + + table.setShowRoot(true); + table.setRoot(rootItem); + rootItem.setExpanded(true); + + table.setColumnResizePolicy(TreeTableView.CONSTRAINED_RESIZE_POLICY); + table.setPrefWidth(450); + + VBox vBox = new VBox(); + vBox.getChildren().add(table); + + hBox.getChildren().add(vBox); + + NumberAxis xAxis = new NumberAxis(); + xAxis.setSide(Side.BOTTOM); + xAxis.setForceZeroInRange(false); + xAxis.setMinorTickVisible(false); + NumberAxis yAxis = new NumberAxis(); + yAxis.setSide(Side.LEFT); + BalanceChart balanceChart = new BalanceChart(xAxis, yAxis); + balanceChart.initialize(new WalletTransactionsEntry(masterWallet, true)); + balanceChart.setAnimated(false); + balanceChart.setLegendVisible(false); + balanceChart.setVerticalGridLinesVisible(false); + + hBox.getChildren().add(balanceChart); + + getDialogPane().setContent(hBox); + + ButtonType okButtonType = new javafx.scene.control.ButtonType("Done", ButtonBar.ButtonData.OK_DONE); + dialogPane.getButtonTypes().addAll(okButtonType); + + AppServices.moveToActiveWindowScreen(this); + } + + public static class SummaryEntry extends Entry { + private SummaryEntry(List walletForms) { + super(walletForms.get(0).getWallet(), walletForms.get(0).getWallet().getName(), walletForms.stream().map(WalletForm::getWalletTransactionsEntry).collect(Collectors.toList())); + } + + @Override + public Long getValue() { + long value = 0; + for(Entry entry : getChildren()) { + value += entry.getValue(); + } + + return value; + } + + @Override + public String getEntryType() { + return null; + } + + @Override + public Function getWalletFunction() { + return Function.TRANSACTIONS; + } + } + + public static class UnconfirmedEntry extends Entry { + private final WalletTransactionsEntry walletTransactionsEntry; + + private UnconfirmedEntry(WalletTransactionsEntry walletTransactionsEntry) { + super(walletTransactionsEntry.getWallet(), "Unconfirmed", Collections.emptyList()); + this.walletTransactionsEntry = walletTransactionsEntry; + } + + @Override + public Long getValue() { + return walletTransactionsEntry.getMempoolBalance(); + } + + @Override + public String getEntryType() { + return null; + } + + @Override + public Function getWalletFunction() { + return Function.TRANSACTIONS; + } + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/WalletTransactionsEntry.java b/src/main/java/com/sparrowwallet/sparrow/wallet/WalletTransactionsEntry.java index ad06421f..bbbdc014 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/WalletTransactionsEntry.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/WalletTransactionsEntry.java @@ -23,7 +23,11 @@ public class WalletTransactionsEntry extends Entry { private static final Logger log = LoggerFactory.getLogger(WalletTransactionsEntry.class); public WalletTransactionsEntry(Wallet wallet) { - super(wallet, wallet.getName(), getWalletTransactions(wallet).stream().map(WalletTransaction::getTransactionEntry).collect(Collectors.toList())); + this(wallet, false); + } + + public WalletTransactionsEntry(Wallet wallet, boolean includeAllChildWallets) { + super(wallet, wallet.getDisplayName(), getWalletTransactions(wallet, includeAllChildWallets).stream().map(WalletTransaction::getTransactionEntry).collect(Collectors.toList())); calculateBalances(false); //No need to resort } @@ -73,7 +77,7 @@ public class WalletTransactionsEntry extends Entry { .collect(Collectors.toUnmodifiableMap(entry -> new HashIndex(entry.getKey().getHash(), entry.getKey().getIndex()), Map.Entry::getKey, BinaryOperator.maxBy(BlockTransactionHashIndex::compareTo))); - Collection entries = getWalletTransactions(getWallet()); + Collection entries = getWalletTransactions(getWallet(), false); Set current = entries.stream().map(WalletTransaction::getTransactionEntry).collect(Collectors.toCollection(LinkedHashSet::new)); Set previous = new LinkedHashSet<>(getChildren()); @@ -101,7 +105,7 @@ public class WalletTransactionsEntry extends Entry { } } - private static Collection getWalletTransactions(Wallet wallet) { + private static Collection getWalletTransactions(Wallet wallet, boolean includeAllChildWallets) { Map walletTransactionMap = new HashMap<>(wallet.getTransactions().size()); for(KeyPurpose keyPurpose : wallet.getWalletKeyPurposes()) { @@ -109,7 +113,7 @@ public class WalletTransactionsEntry extends Entry { } for(Wallet childWallet : wallet.getChildWallets()) { - if(childWallet.isNested()) { + if(includeAllChildWallets || childWallet.isNested()) { for(KeyPurpose keyPurpose : childWallet.getWalletKeyPurposes()) { getWalletTransactions(childWallet, walletTransactionMap, childWallet.getNode(keyPurpose)); } @@ -218,6 +222,14 @@ public class WalletTransactionsEntry extends Entry { return mempoolBalance; } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof WalletTransactionsEntry)) return false; + + return super.equals(o); + } + private static class WalletTransaction implements Comparable { private final Wallet wallet; private final BlockTransaction blockTransaction; diff --git a/src/main/resources/com/sparrowwallet/sparrow/app.fxml b/src/main/resources/com/sparrowwallet/sparrow/app.fxml index 2dcf3c24..aa5c1181 100644 --- a/src/main/resources/com/sparrowwallet/sparrow/app.fxml +++ b/src/main/resources/com/sparrowwallet/sparrow/app.fxml @@ -124,6 +124,7 @@ + diff --git a/src/main/resources/com/sparrowwallet/sparrow/wallet/wallet.css b/src/main/resources/com/sparrowwallet/sparrow/wallet/wallet.css index 838a0459..059ade27 100644 --- a/src/main/resources/com/sparrowwallet/sparrow/wallet/wallet.css +++ b/src/main/resources/com/sparrowwallet/sparrow/wallet/wallet.css @@ -140,4 +140,12 @@ .address-text-field { -fx-font-size: 13px; -fx-font-family: 'Roboto Mono'; +} + +.unconfirmed-row { + -fx-opacity: 0.7; +} + +.summary-row { + -fx-font-weight: bold; } \ No newline at end of file