balance chart, transactions view refactor, tx viewer coin labels updating

This commit is contained in:
Craig Raw 2020-07-10 12:48:36 +02:00
parent d8c19ac0f8
commit 539013919b
15 changed files with 337 additions and 28 deletions

View file

@ -0,0 +1,84 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.BitcoinUnit;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.wallet.TransactionEntry;
import com.sparrowwallet.sparrow.wallet.WalletTransactionsEntry;
import javafx.beans.NamedArg;
import javafx.scene.Node;
import javafx.scene.chart.*;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
public class BalanceChart extends LineChart<Number, Number> {
private XYChart.Series<Number, Number> balanceSeries;
private TransactionEntry selectedEntry;
public BalanceChart(@NamedArg("xAxis") Axis<Number> xAxis, @NamedArg("yAxis") Axis<Number> yAxis) {
super(xAxis, yAxis);
}
public void initialize(WalletTransactionsEntry walletTransactionsEntry) {
balanceSeries = new XYChart.Series<>();
getData().add(balanceSeries);
update(walletTransactionsEntry);
BitcoinUnit unit = Config.get().getBitcoinUnit();
setBitcoinUnit(walletTransactionsEntry.getWallet(), unit);
}
public void update(WalletTransactionsEntry walletTransactionsEntry) {
balanceSeries.getData().clear();
List<Data<Number, Number>> balanceDataList = walletTransactionsEntry.getChildren().stream()
.map(entry -> (TransactionEntry)entry)
.map(txEntry -> new XYChart.Data<>((Number)txEntry.getBlockTransaction().getDate().getTime(), (Number)txEntry.getBalance(), txEntry))
.collect(Collectors.toList());
if(!balanceDataList.isEmpty()) {
long min = balanceDataList.stream().map(data -> data.getXValue().longValue()).min(Long::compare).get();
long max = balanceDataList.stream().map(data -> data.getXValue().longValue()).max(Long::compare).get();
DateAxisFormatter dateAxisFormatter = new DateAxisFormatter(max - min);
NumberAxis xAxis = (NumberAxis)getXAxis();
xAxis.setTickLabelFormatter(dateAxisFormatter);
}
balanceSeries.getData().addAll(balanceDataList);
if(selectedEntry != null) {
select(selectedEntry);
}
}
public void select(TransactionEntry transactionEntry) {
Set<Node> selectedSymbols = lookupAll(".chart-line-symbol.selected");
for(Node selectedSymbol : selectedSymbols) {
selectedSymbol.getStyleClass().remove("selected");
}
for(int i = 0; i < balanceSeries.getData().size(); i++) {
XYChart.Data<Number, Number> data = balanceSeries.getData().get(i);
Node symbol = lookup(".chart-line-symbol.data" + i);
if(symbol != null) {
if(data.getXValue().equals(transactionEntry.getBlockTransaction().getDate().getTime())) {
symbol.getStyleClass().add("selected");
selectedEntry = transactionEntry;
}
}
}
}
public void setBitcoinUnit(Wallet wallet, BitcoinUnit unit) {
if(unit == null || unit.equals(BitcoinUnit.AUTO)) {
unit = wallet.getAutoUnit();
}
NumberAxis yaxis = (NumberAxis)getYAxis();
yaxis.setTickLabelFormatter(new CoinAxisFormatter(yaxis, unit));
}
}

View file

@ -29,7 +29,7 @@ public class CoinLabel extends CopyableLabel {
public CoinLabel(String text) {
super(text);
BTC_FORMAT.setMaximumFractionDigits(8);
valueProperty().addListener((observable, oldValue, newValue) -> setValueAsText((Long)newValue));
valueProperty().addListener((observable, oldValue, newValue) -> setValueAsText((Long)newValue, Config.get().getBitcoinUnit()));
tooltip = new Tooltip();
contextMenu = new CoinContextMenu();
}
@ -46,14 +46,22 @@ public class CoinLabel extends CopyableLabel {
this.value.set(value);
}
private void setValueAsText(Long value) {
public void refresh() {
refresh(Config.get().getBitcoinUnit());
}
public void refresh(BitcoinUnit bitcoinUnit) {
setValueAsText(getValue(), bitcoinUnit);
}
private void setValueAsText(Long value, BitcoinUnit bitcoinUnit) {
setTooltip(tooltip);
setContextMenu(contextMenu);
String satsValue = String.format(Locale.ENGLISH, "%,d",value) + " sats";
String btcValue = BTC_FORMAT.format(value.doubleValue() / Transaction.SATOSHIS_PER_BITCOIN) + " BTC";
BitcoinUnit unit = Config.get().getBitcoinUnit();
BitcoinUnit unit = bitcoinUnit;
if(unit == null || unit.equals(BitcoinUnit.AUTO)) {
unit = (value >= BitcoinUnit.getAutoThreshold() ? BitcoinUnit.BTC : BitcoinUnit.SATOSHIS);
}

View file

@ -0,0 +1,44 @@
package com.sparrowwallet.sparrow.control;
import javafx.util.StringConverter;
import java.text.DateFormat;
import java.text.DecimalFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
public class DateAxisFormatter extends StringConverter<Number> {
private static final DateFormat HOUR_FORMAT = new SimpleDateFormat("HH:mm");
private static final DateFormat DAY_FORMAT = new SimpleDateFormat("d MMM");
private static final DateFormat MONTH_FORMAT = new SimpleDateFormat("MMM yy");
private final DateFormat dateFormat;
private int oddCounter;
public DateAxisFormatter(long duration) {
if(duration < (24 * 60 * 60 * 1000L)) {
dateFormat = HOUR_FORMAT;
} else if(duration < (365 * 24 * 60 * 60 * 1000L)) {
dateFormat = DAY_FORMAT;
} else {
dateFormat = MONTH_FORMAT;
}
}
@Override
public String toString(Number object) {
oddCounter++;
return oddCounter % 3 == 0 ? dateFormat.format(new Date(object.longValue())) : "";
}
@Override
public Number fromString(String string) {
try {
Date date = dateFormat.parse(string);
return date.getTime();
} catch (ParseException e) {
throw new RuntimeException(e);
}
}
}

View file

@ -73,9 +73,7 @@ public class TransactionsTreeTable extends CoinTreeTable {
}
public void updateHistory(List<WalletNode> updatedNodes) {
//Recalculate from scratch and update accordingly - any changes may affect the balance of other transactions
WalletTransactionsEntry rootEntry = (WalletTransactionsEntry)getRoot().getValue();
rootEntry.updateTransactions();
//Transaction entries should have already been updated using WalletTransactionsEntry.updateHistory, so only a resort required
sort();
}

View file

@ -93,9 +93,7 @@ public class UtxosTreeTable extends CoinTreeTable {
}
public void updateHistory(List<WalletNode> updatedNodes) {
//Recalculate from scratch and update accordingly
WalletUtxosEntry rootEntry = (WalletUtxosEntry)getRoot().getValue();
rootEntry.updateUtxos();
//Utxo entries should have already been updated, so only a resort required
sort();
}

View file

@ -10,6 +10,7 @@ import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.control.CoinLabel;
import com.sparrowwallet.sparrow.control.IdLabel;
import com.sparrowwallet.sparrow.control.CopyableLabel;
import com.sparrowwallet.sparrow.event.BitcoinUnitChangedEvent;
import com.sparrowwallet.sparrow.event.BlockTransactionFetchedEvent;
import com.sparrowwallet.sparrow.event.TransactionChangedEvent;
import com.sparrowwallet.sparrow.event.TransactionLocktimeChangedEvent;
@ -356,4 +357,9 @@ public class HeadersController extends TransactionFormController implements Init
}
}
}
@Subscribe
public void bitcoinUnitChanged(BitcoinUnitChangedEvent event) {
fee.refresh(event.getBitcoinUnit());
}
}

View file

@ -9,10 +9,7 @@ import com.sparrowwallet.drongo.psbt.PSBTInput;
import com.sparrowwallet.drongo.wallet.BlockTransaction;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.control.*;
import com.sparrowwallet.sparrow.event.BlockTransactionFetchedEvent;
import com.sparrowwallet.sparrow.event.TransactionChangedEvent;
import com.sparrowwallet.sparrow.event.TransactionLocktimeChangedEvent;
import com.sparrowwallet.sparrow.event.ViewTransactionEvent;
import com.sparrowwallet.sparrow.event.*;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.geometry.Pos;
@ -498,4 +495,9 @@ public class InputController extends TransactionFormController implements Initia
locktimeAbsolute.setText(Long.toString(event.getTransaction().getLocktime()));
}
}
@Subscribe
public void bitcoinUnitChanged(BitcoinUnitChangedEvent event) {
spends.refresh(event.getBitcoinUnit());
}
}

View file

@ -7,6 +7,7 @@ import com.sparrowwallet.drongo.wallet.BlockTransaction;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.control.CoinLabel;
import com.sparrowwallet.sparrow.control.CopyableLabel;
import com.sparrowwallet.sparrow.event.BitcoinUnitChangedEvent;
import com.sparrowwallet.sparrow.event.BlockTransactionFetchedEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
@ -160,4 +161,9 @@ public class InputsController extends TransactionFormController implements Initi
updateBlockTransactionInputs(event.getInputTransactions());
}
}
@Subscribe
public void bitcoinUnitChanged(BitcoinUnitChangedEvent event) {
total.refresh(event.getBitcoinUnit());
}
}

View file

@ -10,6 +10,7 @@ import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.control.AddressLabel;
import com.sparrowwallet.sparrow.control.CoinLabel;
import com.sparrowwallet.sparrow.control.CopyableLabel;
import com.sparrowwallet.sparrow.event.BitcoinUnitChangedEvent;
import com.sparrowwallet.sparrow.event.BlockTransactionOutputsFetchedEvent;
import com.sparrowwallet.sparrow.event.ViewTransactionEvent;
import com.sparrowwallet.sparrow.io.ElectrumServer;
@ -142,4 +143,9 @@ public class OutputController extends TransactionFormController implements Initi
updateSpent(event.getOutputTransactions());
}
}
@Subscribe
public void bitcoinUnitChanged(BitcoinUnitChangedEvent event) {
value.refresh(event.getBitcoinUnit());
}
}

View file

@ -1,9 +1,12 @@
package com.sparrowwallet.sparrow.transaction;
import com.google.common.eventbus.Subscribe;
import com.sparrowwallet.drongo.protocol.Transaction;
import com.sparrowwallet.drongo.protocol.TransactionOutput;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.control.CoinLabel;
import com.sparrowwallet.sparrow.control.CopyableLabel;
import com.sparrowwallet.sparrow.event.BitcoinUnitChangedEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.chart.PieChart;
@ -25,7 +28,7 @@ public class OutputsController extends TransactionFormController implements Init
@Override
public void initialize(URL location, ResourceBundle resources) {
EventManager.get().register(this);
}
public void setModel(OutputsForm form) {
@ -47,4 +50,9 @@ public class OutputsController extends TransactionFormController implements Init
addPieData(outputsPie, tx.getOutputs());
}
}
@Subscribe
public void bitcoinUnitChanged(BitcoinUnitChangedEvent event) {
total.refresh(event.getBitcoinUnit());
}
}

View file

@ -2,10 +2,15 @@ package com.sparrowwallet.sparrow.wallet;
import com.google.common.eventbus.Subscribe;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.control.BalanceChart;
import com.sparrowwallet.sparrow.control.CoinLabel;
import com.sparrowwallet.sparrow.control.CopyableLabel;
import com.sparrowwallet.sparrow.control.TransactionsTreeTable;
import com.sparrowwallet.sparrow.event.*;
import javafx.collections.ListChangeListener;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.TreeItem;
import javafx.scene.input.MouseEvent;
import java.net.URL;
@ -13,9 +18,21 @@ import java.util.ResourceBundle;
public class TransactionsController extends WalletFormController implements Initializable {
@FXML
private CoinLabel balance;
@FXML
private CopyableLabel fiatBalance;
@FXML
private CoinLabel mempoolBalance;
@FXML
private TransactionsTreeTable transactionsTable;
@FXML
private BalanceChart balanceChart;
@Override
public void initialize(URL location, ResourceBundle resources) {
EventManager.get().register(this);
@ -23,20 +40,46 @@ public class TransactionsController extends WalletFormController implements Init
@Override
public void initializeView() {
transactionsTable.initialize(getWalletForm().getWalletTransactionsEntry());
WalletTransactionsEntry walletTransactionsEntry = getWalletForm().getWalletTransactionsEntry();
transactionsTable.initialize(walletTransactionsEntry);
balance.setValue(walletTransactionsEntry.getBalance());
mempoolBalance.setValue(walletTransactionsEntry.getMempoolBalance());
balanceChart.initialize(walletTransactionsEntry);
transactionsTable.getSelectionModel().getSelectedIndices().addListener((ListChangeListener<Integer>) c -> {
TreeItem<Entry> selectedItem = transactionsTable.getSelectionModel().getSelectedItem();
if(selectedItem != null && selectedItem.getValue() instanceof TransactionEntry) {
balanceChart.select((TransactionEntry)selectedItem.getValue());
}
});
}
@Subscribe
public void walletNodesChanged(WalletNodesChangedEvent event) {
if(event.getWallet().equals(walletForm.getWallet())) {
transactionsTable.updateAll(getWalletForm().getWalletTransactionsEntry());
WalletTransactionsEntry walletTransactionsEntry = getWalletForm().getWalletTransactionsEntry();
transactionsTable.updateAll(walletTransactionsEntry);
balance.setValue(walletTransactionsEntry.getBalance());
mempoolBalance.setValue(walletTransactionsEntry.getMempoolBalance());
balanceChart.update(walletTransactionsEntry);
}
}
@Subscribe
public void walletHistoryChanged(WalletHistoryChangedEvent event) {
if(event.getWallet().equals(walletForm.getWallet())) {
WalletTransactionsEntry walletTransactionsEntry = getWalletForm().getWalletTransactionsEntry();
//Will automatically update transactionsTable transactions and recalculate balances
walletTransactionsEntry.updateTransactions();
transactionsTable.updateHistory(event.getHistoryChangedNodes());
balance.setValue(walletTransactionsEntry.getBalance());
mempoolBalance.setValue(walletTransactionsEntry.getMempoolBalance());
balanceChart.update(walletTransactionsEntry);
}
}
@ -44,12 +87,16 @@ public class TransactionsController extends WalletFormController implements Init
public void walletEntryLabelChanged(WalletEntryLabelChangedEvent event) {
if(event.getWallet().equals(walletForm.getWallet())) {
transactionsTable.updateLabel(event.getEntry());
balanceChart.update(getWalletForm().getWalletTransactionsEntry());
}
}
@Subscribe
public void bitcoinUnitChanged(BitcoinUnitChangedEvent event) {
transactionsTable.setBitcoinUnit(getWalletForm().getWallet(), event.getBitcoinUnit());
balanceChart.setBitcoinUnit(getWalletForm().getWallet(), event.getBitcoinUnit());
balance.refresh(event.getBitcoinUnit());
mempoolBalance.refresh(event.getBitcoinUnit());
}
//TODO: Remove

View file

@ -44,16 +44,22 @@ public class UtxosController extends WalletFormController implements Initializab
@Subscribe
public void walletNodesChanged(WalletNodesChangedEvent event) {
if(event.getWallet().equals(walletForm.getWallet())) {
utxosTable.updateAll(getWalletForm().getWalletUtxosEntry());
utxosChart.update(getWalletForm().getWalletUtxosEntry());
WalletUtxosEntry walletUtxosEntry = getWalletForm().getWalletUtxosEntry();
utxosTable.updateAll(walletUtxosEntry);
utxosChart.update(walletUtxosEntry);
}
}
@Subscribe
public void walletHistoryChanged(WalletHistoryChangedEvent event) {
if(event.getWallet().equals(walletForm.getWallet())) {
WalletUtxosEntry walletUtxosEntry = getWalletForm().getWalletUtxosEntry();
//Will automatically update utxosTable
walletUtxosEntry.updateUtxos();
utxosTable.updateHistory(event.getHistoryChangedNodes());
utxosChart.update(getWalletForm().getWalletUtxosEntry());
utxosChart.update(walletUtxosEntry);
}
}

View file

@ -31,17 +31,24 @@ public class WalletTransactionsEntry extends Entry {
protected void calculateBalances() {
long balance = 0L;
long mempoolBalance = 0L;
//Note transaction entries must be in ascending order. This sorting is ultimately done according to BlockTransactions' comparator
getChildren().sort(Comparator.comparing(TransactionEntry.class::cast));
for(Entry entry : getChildren()) {
TransactionEntry transactionEntry = (TransactionEntry)entry;
balance += entry.getValue();
if(transactionEntry.getConfirmations() != 0) {
balance += entry.getValue();
} else {
mempoolBalance += entry.getValue();
}
transactionEntry.setBalance(balance);
}
setBalance(balance);
setMempoolBalance(mempoolBalance);
}
public void updateTransactions() {
@ -126,6 +133,39 @@ public class WalletTransactionsEntry extends Entry {
return balance;
}
/**
* Defines the wallet balance in the mempool
*/
private LongProperty mempoolBalance;
public final void setMempoolBalance(long value) {
if(mempoolBalance != null || value != 0) {
mempoolBalanceProperty().set(value);
}
}
public final long getMempoolBalance() {
return mempoolBalance == null ? 0L : mempoolBalance.get();
}
public final LongProperty mempoolBalanceProperty() {
if(mempoolBalance == null) {
mempoolBalance = new LongPropertyBase(0L) {
@Override
public Object getBean() {
return WalletTransactionsEntry.this;
}
@Override
public String getName() {
return "mempoolBalance";
}
};
}
return mempoolBalance;
}
private static class WalletTransaction {
private final Wallet wallet;
private final BlockTransaction blockTransaction;

View file

@ -3,3 +3,23 @@
-fx-font-size: 1.2em;
-fx-padding: 10 0 10 0;
}
#balanceChart {
-fx-padding: 10 0 0 0;
-fx-max-width: 350px;
-fx-pref-width: 350px;
-fx-max-height: 150px;
}
.default-color0.chart-series-line {
-fx-stroke: rgba(105, 108, 119, 0.3);
-fx-stroke-width: 1px;
}
.chart-line-symbol {
-fx-background-color: rgba(105, 108, 119, 0.3);
}
.chart-line-symbol.selected {
-fx-background-color: rgba(30, 136, 207, 0.8);
}

View file

@ -7,15 +7,51 @@
<?import javafx.scene.layout.*?>
<?import com.sparrowwallet.sparrow.control.TransactionsTreeTable?>
<?import javafx.geometry.Insets?>
<?import com.sparrowwallet.sparrow.control.BalanceChart?>
<?import javafx.scene.chart.NumberAxis?>
<?import tornadofx.control.Form?>
<?import tornadofx.control.Fieldset?>
<?import tornadofx.control.Field?>
<?import com.sparrowwallet.sparrow.control.CoinLabel?>
<?import com.sparrowwallet.sparrow.control.CopyableLabel?>
<BorderPane stylesheets="@transactions.css, @wallet.css, @../general.css" styleClass="wallet-pane" xmlns="http://javafx.com/javafx/10.0.2-internal" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.sparrowwallet.sparrow.wallet.TransactionsController">
<padding>
<Insets left="25.0" right="25.0" top="15.0" bottom="25.0" />
</padding>
<top>
<Label styleClass="transactions-treetable-label" text="Transactions" /> <!-- onMouseClicked="#advanceBlock" -->
</top>
<center>
<TransactionsTreeTable fx:id="transactionsTable" />
<VBox spacing="10">
<padding>
<Insets left="25.0" right="25.0" top="25.0" bottom="25.0" />
</padding>
<GridPane styleClass="send-form" hgap="10.0" vgap="10.0">
<columnConstraints>
<ColumnConstraints percentWidth="50" />
<ColumnConstraints percentWidth="50" />
</columnConstraints>
<rowConstraints>
<RowConstraints />
</rowConstraints>
<Form GridPane.columnIndex="0" GridPane.rowIndex="0">
<Fieldset inputGrow="SOMETIMES" text="Transactions">
<Field text="Balance:">
<CoinLabel fx:id="balance"/>
</Field>
<Field text="Fiat balance:">
<CopyableLabel fx:id="fiatBalance" />
</Field>
<Field text="Mempool:">
<CoinLabel fx:id="mempoolBalance" />
</Field>
</Fieldset>
</Form>
<BalanceChart fx:id="balanceChart" animated="false" legendVisible="false" verticalGridLinesVisible="false" GridPane.columnIndex="1" GridPane.rowIndex="0">
<xAxis>
<NumberAxis side="BOTTOM" forceZeroInRange="false" minorTickVisible="false" />
</xAxis>
<yAxis>
<NumberAxis side="LEFT" />
</yAxis>
</BalanceChart>
</GridPane>
<TransactionsTreeTable fx:id="transactionsTable" VBox.vgrow="ALWAYS" />
</VBox>
</center>
</BorderPane>