add wallet summary dialog

This commit is contained in:
Craig Raw 2023-08-14 16:11:55 +02:00
parent bebd7eebe5
commit f175139fd3
9 changed files with 331 additions and 4 deletions

View file

@ -166,6 +166,9 @@ public class AppController implements Initializable {
@FXML @FXML
private MenuItem lockAllWallets; private MenuItem lockAllWallets;
@FXML
private MenuItem showWalletSummary;
@FXML @FXML
private MenuItem searchWallet; private MenuItem searchWallet;
@ -380,6 +383,7 @@ public class AppController implements Initializable {
deleteWallet.disableProperty().bind(exportWallet.disableProperty()); deleteWallet.disableProperty().bind(exportWallet.disableProperty());
closeTab.setDisable(true); closeTab.setDisable(true);
lockWallet.setDisable(true); lockWallet.setDisable(true);
showWalletSummary.disableProperty().bind(exportWallet.disableProperty());
searchWallet.disableProperty().bind(exportWallet.disableProperty()); searchWallet.disableProperty().bind(exportWallet.disableProperty());
refreshWallet.disableProperty().bind(Bindings.or(exportWallet.disableProperty(), Bindings.or(serverToggle.disableProperty(), AppServices.onlineProperty().not()))); refreshWallet.disableProperty().bind(Bindings.or(exportWallet.disableProperty(), Bindings.or(serverToggle.disableProperty(), AppServices.onlineProperty().not())));
sendToMany.disableProperty().bind(exportWallet.disableProperty()); 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<WalletForm> 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) { public void refreshWallet(ActionEvent event) {
WalletForm selectedWalletForm = getSelectedWalletForm(); WalletForm selectedWalletForm = getSelectedWalletForm();
if(selectedWalletForm != null) { if(selectedWalletForm != null) {

View file

@ -2,6 +2,7 @@ package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.BitcoinUnit; import com.sparrowwallet.drongo.BitcoinUnit;
import com.sparrowwallet.drongo.wallet.Wallet; import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.CurrencyRate;
import com.sparrowwallet.sparrow.UnitFormat; import com.sparrowwallet.sparrow.UnitFormat;
import com.sparrowwallet.sparrow.AppServices; import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.EventManager;
@ -28,6 +29,7 @@ import java.util.Optional;
public class CoinTreeTable extends TreeTableView<Entry> { public class CoinTreeTable extends TreeTableView<Entry> {
private BitcoinUnit bitcoinUnit; private BitcoinUnit bitcoinUnit;
private UnitFormat unitFormat; private UnitFormat unitFormat;
private CurrencyRate currencyRate;
public BitcoinUnit getBitcoinUnit() { public BitcoinUnit getBitcoinUnit() {
return bitcoinUnit; return bitcoinUnit;
@ -64,6 +66,18 @@ public class CoinTreeTable extends TreeTableView<Entry> {
} }
} }
public CurrencyRate getCurrencyRate() {
return currencyRate;
}
public void setCurrencyRate(CurrencyRate currencyRate) {
this.currencyRate = currencyRate;
if(!getChildren().isEmpty()) {
refresh();
}
}
public void updateHistoryStatus(WalletHistoryStatusEvent event) { public void updateHistoryStatus(WalletHistoryStatusEvent event) {
if(getRoot() != null) { if(getRoot() != null) {
Entry entry = getRoot().getValue(); Entry entry = getRoot().getValue();

View file

@ -790,6 +790,8 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
cell.getStyleClass().remove("transaction-row"); cell.getStyleClass().remove("transaction-row");
cell.getStyleClass().remove("node-row"); cell.getStyleClass().remove("node-row");
cell.getStyleClass().remove("utxo-row"); cell.getStyleClass().remove("utxo-row");
cell.getStyleClass().remove("unconfirmed-row");
cell.getStyleClass().remove("summary-row");
cell.getStyleClass().remove("address-cell"); cell.getStyleClass().remove("address-cell");
cell.getStyleClass().remove("hashindex-row"); cell.getStyleClass().remove("hashindex-row");
cell.getStyleClass().remove("confirming"); cell.getStyleClass().remove("confirming");
@ -824,6 +826,10 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
if(hashIndexEntry.isSpent()) { if(hashIndexEntry.isSpent()) {
cell.getStyleClass().add("spent"); 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");
} }
} }
} }

View file

@ -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<Entry, Number> {
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);
}
}
}

View file

@ -140,6 +140,8 @@ public class SearchWalletDialog extends Dialog<Entry> {
setResizable(true); setResizable(true);
AppServices.moveToActiveWindowScreen(this);
Platform.runLater(search::requestFocus); Platform.runLater(search::requestFocus);
} }

View file

@ -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<Void> {
public WalletSummaryDialog(List<WalletForm> 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<Entry, String> nameColumn = new TreeTableColumn<>("Wallet");
nameColumn.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, String> param) -> {
return new ReadOnlyObjectWrapper<>(param.getValue().getValue().getLabel());
});
nameColumn.setCellFactory(p -> new LabelCell());
table.getColumns().add(nameColumn);
TreeTableColumn<Entry, Number> balanceColumn = new TreeTableColumn<>("Balance");
balanceColumn.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, Number> 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<Entry, Number> fiatColumn = new TreeTableColumn<>(currencyRate.getCurrency().getSymbol());
fiatColumn.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, Number> 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<Entry> rootItem = new TreeItem<>(rootEntry);
for(Entry childEntry : rootEntry.getChildren()) {
TreeItem<Entry> 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<WalletForm> 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;
}
}
}

View file

@ -23,7 +23,11 @@ public class WalletTransactionsEntry extends Entry {
private static final Logger log = LoggerFactory.getLogger(WalletTransactionsEntry.class); private static final Logger log = LoggerFactory.getLogger(WalletTransactionsEntry.class);
public WalletTransactionsEntry(Wallet wallet) { 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 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, .collect(Collectors.toUnmodifiableMap(entry -> new HashIndex(entry.getKey().getHash(), entry.getKey().getIndex()), Map.Entry::getKey,
BinaryOperator.maxBy(BlockTransactionHashIndex::compareTo))); BinaryOperator.maxBy(BlockTransactionHashIndex::compareTo)));
Collection<WalletTransactionsEntry.WalletTransaction> entries = getWalletTransactions(getWallet()); Collection<WalletTransactionsEntry.WalletTransaction> entries = getWalletTransactions(getWallet(), false);
Set<Entry> current = entries.stream().map(WalletTransaction::getTransactionEntry).collect(Collectors.toCollection(LinkedHashSet::new)); Set<Entry> current = entries.stream().map(WalletTransaction::getTransactionEntry).collect(Collectors.toCollection(LinkedHashSet::new));
Set<Entry> previous = new LinkedHashSet<>(getChildren()); Set<Entry> previous = new LinkedHashSet<>(getChildren());
@ -101,7 +105,7 @@ public class WalletTransactionsEntry extends Entry {
} }
} }
private static Collection<WalletTransaction> getWalletTransactions(Wallet wallet) { private static Collection<WalletTransaction> getWalletTransactions(Wallet wallet, boolean includeAllChildWallets) {
Map<BlockTransaction, WalletTransaction> walletTransactionMap = new HashMap<>(wallet.getTransactions().size()); Map<BlockTransaction, WalletTransaction> walletTransactionMap = new HashMap<>(wallet.getTransactions().size());
for(KeyPurpose keyPurpose : wallet.getWalletKeyPurposes()) { for(KeyPurpose keyPurpose : wallet.getWalletKeyPurposes()) {
@ -109,7 +113,7 @@ public class WalletTransactionsEntry extends Entry {
} }
for(Wallet childWallet : wallet.getChildWallets()) { for(Wallet childWallet : wallet.getChildWallets()) {
if(childWallet.isNested()) { if(includeAllChildWallets || childWallet.isNested()) {
for(KeyPurpose keyPurpose : childWallet.getWalletKeyPurposes()) { for(KeyPurpose keyPurpose : childWallet.getWalletKeyPurposes()) {
getWalletTransactions(childWallet, walletTransactionMap, childWallet.getNode(keyPurpose)); getWalletTransactions(childWallet, walletTransactionMap, childWallet.getNode(keyPurpose));
} }
@ -218,6 +222,14 @@ public class WalletTransactionsEntry extends Entry {
return mempoolBalance; 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<WalletTransaction> { private static class WalletTransaction implements Comparable<WalletTransaction> {
private final Wallet wallet; private final Wallet wallet;
private final BlockTransaction blockTransaction; private final BlockTransaction blockTransaction;

View file

@ -124,6 +124,7 @@
<MenuItem fx:id="lockWallet" mnemonicParsing="false" text="Lock Wallet" accelerator="Shortcut+L" onAction="#lockWallet"/> <MenuItem fx:id="lockWallet" mnemonicParsing="false" text="Lock Wallet" accelerator="Shortcut+L" onAction="#lockWallet"/>
<MenuItem fx:id="lockAllWallets" mnemonicParsing="false" text="Lock All Wallets" accelerator="Shortcut+Shift+L" onAction="#lockWallets"/> <MenuItem fx:id="lockAllWallets" mnemonicParsing="false" text="Lock All Wallets" accelerator="Shortcut+Shift+L" onAction="#lockWallets"/>
<SeparatorMenuItem /> <SeparatorMenuItem />
<MenuItem fx:id="showWalletSummary" mnemonicParsing="false" text="Show Wallet Summary" onAction="#showWalletSummary"/>
<MenuItem fx:id="searchWallet" mnemonicParsing="false" text="Search Wallet" accelerator="Shortcut+Shift+S" onAction="#searchWallet"/> <MenuItem fx:id="searchWallet" mnemonicParsing="false" text="Search Wallet" accelerator="Shortcut+Shift+S" onAction="#searchWallet"/>
<MenuItem fx:id="refreshWallet" mnemonicParsing="false" text="Refresh Wallet" accelerator="Shortcut+R" onAction="#refreshWallet"/> <MenuItem fx:id="refreshWallet" mnemonicParsing="false" text="Refresh Wallet" accelerator="Shortcut+R" onAction="#refreshWallet"/>
</items> </items>

View file

@ -141,3 +141,11 @@
-fx-font-size: 13px; -fx-font-size: 13px;
-fx-font-family: 'Roboto Mono'; -fx-font-family: 'Roboto Mono';
} }
.unconfirmed-row {
-fx-opacity: 0.7;
}
.summary-row {
-fx-font-weight: bold;
}