diff --git a/src/main/java/com/sparrowwallet/sparrow/control/AmountCell.java b/src/main/java/com/sparrowwallet/sparrow/control/AmountCell.java index 907af83f..67b374ae 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/AmountCell.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/AmountCell.java @@ -3,6 +3,7 @@ package com.sparrowwallet.sparrow.control; import com.sparrowwallet.drongo.protocol.Transaction; import com.sparrowwallet.sparrow.wallet.Entry; import com.sparrowwallet.sparrow.wallet.HashIndexEntry; +import com.sparrowwallet.sparrow.wallet.TransactionEntry; import javafx.scene.control.ContentDisplay; import javafx.scene.control.Tooltip; import javafx.scene.control.TreeTableCell; @@ -14,7 +15,6 @@ class AmountCell extends TreeTableCell { public AmountCell() { super(); getStyleClass().add("amount-cell"); - setContentDisplay(ContentDisplay.RIGHT); } @Override @@ -25,16 +25,37 @@ class AmountCell extends TreeTableCell { setText(null); setGraphic(null); } else { - EntryCell.applyRowStyles(this, getTreeTableView().getTreeItem(getIndex()).getValue()); + Entry entry = getTreeTableView().getTreeItem(getIndex()).getValue(); + EntryCell.applyRowStyles(this, entry); String satsValue = String.format(Locale.ENGLISH, "%,d", amount); - String btcValue = CoinLabel.getBTCFormat().format(amount.doubleValue() / Transaction.SATOSHIS_PER_BITCOIN) + " BTC"; + final String btcValue = CoinLabel.getBTCFormat().format(amount.doubleValue() / Transaction.SATOSHIS_PER_BITCOIN) + " BTC"; - Entry entry = getTreeTableView().getTreeItem(getIndex()).getValue(); - if(entry instanceof HashIndexEntry) { + if(entry instanceof TransactionEntry) { + TransactionEntry transactionEntry = (TransactionEntry)entry; + Tooltip tooltip = new Tooltip(); + tooltip.setText(btcValue + " (" + transactionEntry.getConfirmationsDescription() + ")"); + setTooltip(tooltip); + + transactionEntry.confirmationsProperty().addListener((observable, oldValue, newValue) -> { + Tooltip newTooltip = new Tooltip(); + newTooltip.setText(btcValue + " (" + transactionEntry.getConfirmationsDescription() + ")"); + setTooltip(newTooltip); + }); + + if(transactionEntry.isConfirming()) { + ConfirmationProgressIndicator arc = new ConfirmationProgressIndicator(transactionEntry.getConfirmations()); + arc.confirmationsProperty().bind(transactionEntry.confirmationsProperty()); + setGraphic(arc); + setContentDisplay(ContentDisplay.LEFT); + } else { + setGraphic(null); + } + } else if(entry instanceof HashIndexEntry) { Region node = new Region(); node.setPrefWidth(10); setGraphic(node); + setContentDisplay(ContentDisplay.RIGHT); if(((HashIndexEntry) entry).getType() == HashIndexEntry.Type.INPUT) { satsValue = "-" + satsValue; @@ -43,11 +64,14 @@ class AmountCell extends TreeTableCell { setGraphic(null); } - Tooltip tooltip = new Tooltip(); - tooltip.setText(btcValue); + if(getTooltip() == null) { + Tooltip tooltip = new Tooltip(); + tooltip.setText(btcValue); + setTooltip(tooltip); + } setText(satsValue); - setTooltip(tooltip); + } } } diff --git a/src/main/java/com/sparrowwallet/sparrow/control/ConfirmationProgressIndicator.java b/src/main/java/com/sparrowwallet/sparrow/control/ConfirmationProgressIndicator.java new file mode 100644 index 00000000..2de1b232 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/control/ConfirmationProgressIndicator.java @@ -0,0 +1,137 @@ +package com.sparrowwallet.sparrow.control; + +import com.sparrowwallet.sparrow.wallet.TransactionEntry; +import javafx.animation.*; +import javafx.beans.property.*; +import javafx.geometry.Pos; +import javafx.scene.Group; +import javafx.scene.layout.StackPane; +import javafx.scene.paint.Color; +import javafx.scene.shape.Arc; +import javafx.scene.shape.ArcType; +import javafx.scene.shape.Circle; +import javafx.scene.shape.Line; +import javafx.util.Duration; + +public class ConfirmationProgressIndicator extends StackPane { + private final Group confirmationGroup; + private final Arc arc; + private final Line downTickLine; + private final Line upTickLine; + + public ConfirmationProgressIndicator(int confirmations) { + Circle circle = new Circle(7, 7, 7); + circle.setFill(Color.TRANSPARENT); + circle.getStyleClass().add("confirmation-progress-circle"); + + arc = new Arc(7, 7, 7, 7, 90, getDegrees(confirmations)); + arc.setType(ArcType.ROUND); + arc.getStyleClass().add("confirmation-progress-arc"); + + downTickLine = new Line(4, 8, 4, 8); + downTickLine.setStrokeWidth(1.0); + downTickLine.setOpacity(0); + downTickLine.getStyleClass().add("confirmation-progress-tick"); + upTickLine = new Line(6, 10, 6, 10); + upTickLine.setStrokeWidth(1.0); + upTickLine.setOpacity(0); + upTickLine.getStyleClass().add("confirmation-progress-tick"); + + confirmationGroup = new Group(circle, arc, downTickLine, upTickLine); + getStyleClass().add("confirmation-progress"); + + setAlignment(Pos.CENTER); + getChildren().addAll(confirmationGroup); + + confirmationsProperty().set(confirmations); + confirmationsProperty().addListener((observable, oldValue, newValue) -> { + if(!oldValue.equals(newValue)) { + SequentialTransition sequence = new SequentialTransition(); + + Timeline arcLengthTimeline = new Timeline(); + KeyValue arcLengthValue = new KeyValue(arc.lengthProperty(), getDegrees(newValue.intValue())); + KeyFrame arcLengthFrame = new KeyFrame(Duration.millis(1000), arcLengthValue); + arcLengthTimeline.getKeyFrames().add(arcLengthFrame); + sequence.getChildren().add(arcLengthTimeline); + + if(newValue.intValue() == TransactionEntry.BLOCKS_TO_CONFIRM) { + Timeline arcRadiusTimeline = new Timeline(); + KeyValue arcRadiusXValue = new KeyValue(arc.radiusXProperty(), 0.0); + KeyValue arcRadiusYValue = new KeyValue(arc.radiusYProperty(), 0.0); + KeyFrame arcRadiusFrame = new KeyFrame(Duration.millis(500), arcRadiusXValue, arcRadiusYValue); + arcRadiusTimeline.getKeyFrames().add(arcRadiusFrame); + sequence.getChildren().add(arcRadiusTimeline); + + FadeTransition downTickFadeIn = new FadeTransition(Duration.millis(50), downTickLine); + downTickFadeIn.setFromValue(0); + downTickFadeIn.setToValue(1); + sequence.getChildren().add(downTickFadeIn); + + Timeline downTickLineTimeline = new Timeline(); + KeyValue downTickLineX = new KeyValue(downTickLine.endXProperty(), 6); + KeyValue downTickLineY = new KeyValue(downTickLine.endYProperty(), 10); + KeyFrame downTickLineFrame = new KeyFrame(Duration.millis(125), downTickLineX, downTickLineY); + downTickLineTimeline.getKeyFrames().add(downTickLineFrame); + sequence.getChildren().add(downTickLineTimeline); + + FadeTransition upTickFadeIn = new FadeTransition(Duration.millis(50), upTickLine); + upTickFadeIn.setFromValue(0); + upTickFadeIn.setToValue(1); + sequence.getChildren().add(upTickFadeIn); + + Timeline upTickLineTimeline = new Timeline(); + KeyValue upTickLineX = new KeyValue(upTickLine.endXProperty(), 10); + KeyValue upTickLineY = new KeyValue(upTickLine.endYProperty(), 4); + KeyFrame upTickLineFrame = new KeyFrame(Duration.millis(250), upTickLineX, upTickLineY); + upTickLineTimeline.getKeyFrames().add(upTickLineFrame); + sequence.getChildren().add(upTickLineTimeline); + + FadeTransition groupFadeOut = new FadeTransition(Duration.minutes(1), confirmationGroup); + groupFadeOut.setFromValue(1); + groupFadeOut.setToValue(0); + sequence.getChildren().add(groupFadeOut); + } + + sequence.play(); + } + }); + } + + private static double getDegrees(int confirmations) { + int requiredConfirmations = TransactionEntry.BLOCKS_TO_CONFIRM; + return ((double)Math.min(confirmations, requiredConfirmations)/ requiredConfirmations) * -360d; + } + + /** + * Defines the number of confirmations + */ + private IntegerProperty confirmations; + + public final void setConfirmations(int value) { + if(confirmations != null || value != 0) { + confirmationsProperty().set(value); + } + } + + public final int getConfirmations() { + return confirmations == null ? 0 : confirmations.get(); + } + + public final IntegerProperty confirmationsProperty() { + if(confirmations == null) { + confirmations = new IntegerPropertyBase(0) { + + @Override + public Object getBean() { + return ConfirmationProgressIndicator.this; + } + + @Override + public String getName() { + return "confirmations"; + } + }; + } + return confirmations; + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/control/EntryCell.java b/src/main/java/com/sparrowwallet/sparrow/control/EntryCell.java index 48402988..89b41d20 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/EntryCell.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/EntryCell.java @@ -2,17 +2,16 @@ package com.sparrowwallet.sparrow.control; import com.sparrowwallet.drongo.Utils; import com.sparrowwallet.drongo.address.Address; -import com.sparrowwallet.drongo.protocol.Sha256Hash; -import com.sparrowwallet.drongo.protocol.Transaction; import com.sparrowwallet.drongo.wallet.BlockTransaction; -import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex; import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.event.ReceiveActionEvent; import com.sparrowwallet.sparrow.event.ReceiveToEvent; import com.sparrowwallet.sparrow.event.ViewTransactionEvent; import com.sparrowwallet.sparrow.glyphfont.FontAwesome5; -import com.sparrowwallet.sparrow.transaction.TransactionView; -import com.sparrowwallet.sparrow.wallet.*; +import com.sparrowwallet.sparrow.wallet.Entry; +import com.sparrowwallet.sparrow.wallet.HashIndexEntry; +import com.sparrowwallet.sparrow.wallet.NodeEntry; +import com.sparrowwallet.sparrow.wallet.TransactionEntry; import javafx.application.Platform; import javafx.geometry.Pos; import javafx.scene.control.*; @@ -185,6 +184,15 @@ class EntryCell extends TreeTableCell { if(entry != null) { if(entry instanceof TransactionEntry) { cell.getStyleClass().add("transaction-row"); + TransactionEntry transactionEntry = (TransactionEntry)entry; + if(transactionEntry.isConfirming()) { + cell.getStyleClass().add("confirming"); + transactionEntry.confirmationsProperty().addListener((observable, oldValue, newValue) -> { + if(!transactionEntry.isConfirming()) { + cell.getStyleClass().remove("confirming"); + } + }); + } } else if(entry instanceof NodeEntry) { cell.getStyleClass().add("node-row"); } else if(entry instanceof HashIndexEntry) { diff --git a/src/main/java/com/sparrowwallet/sparrow/event/WalletEntryLabelChangedEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/WalletEntryLabelChangedEvent.java new file mode 100644 index 00000000..462a51bf --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/event/WalletEntryLabelChangedEvent.java @@ -0,0 +1,17 @@ +package com.sparrowwallet.sparrow.event; + +import com.sparrowwallet.drongo.wallet.Wallet; +import com.sparrowwallet.sparrow.wallet.Entry; + +public class WalletEntryLabelChangedEvent extends WalletChangedEvent { + private final Entry entry; + + public WalletEntryLabelChangedEvent(Wallet wallet, Entry entry) { + super(wallet); + this.entry = entry; + } + + public Entry getEntry() { + return entry; + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/event/WalletHistoryChangedEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/WalletHistoryChangedEvent.java index c7824142..1fdef2e4 100644 --- a/src/main/java/com/sparrowwallet/sparrow/event/WalletHistoryChangedEvent.java +++ b/src/main/java/com/sparrowwallet/sparrow/event/WalletHistoryChangedEvent.java @@ -11,11 +11,11 @@ import java.util.stream.Collectors; * This is posted by WalletForm once the history of the wallet has been refreshed, and new transactions detected * Extends WalletChangedEvent so also saves the wallet. */ -public class WalletHistoryChangedEvent extends WalletChangedEvent { +public class WalletHistoryChangedEvent extends WalletBlockHeightChangedEvent { private final List historyChangedNodes; - public WalletHistoryChangedEvent(Wallet wallet, List historyChangedNodes) { - super(wallet); + public WalletHistoryChangedEvent(Wallet wallet, Integer blockHeight, List historyChangedNodes) { + super(wallet, blockHeight); this.historyChangedNodes = historyChangedNodes; } diff --git a/src/main/java/com/sparrowwallet/sparrow/event/WalletNodesChangedEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/WalletNodesChangedEvent.java index 74e2d479..d3a3c1b9 100644 --- a/src/main/java/com/sparrowwallet/sparrow/event/WalletNodesChangedEvent.java +++ b/src/main/java/com/sparrowwallet/sparrow/event/WalletNodesChangedEvent.java @@ -4,16 +4,10 @@ import com.sparrowwallet.drongo.wallet.Wallet; /** * This event is posted by WalletForm once it has received a WalletSettingsChangedEvent and cleared it's entry caches - * It does not extend WalletChangedEvent for the same reason WalletSettingsChangedEvent does not - it does not want to trigger a wallet save. + * The controllers in the wallet package listen to this event to update their views should a wallet's settings change */ -public class WalletNodesChangedEvent { - private final Wallet wallet; - +public class WalletNodesChangedEvent extends WalletChangedEvent { public WalletNodesChangedEvent(Wallet wallet) { - this.wallet = wallet; - } - - public Wallet getWallet() { - return wallet; + super(wallet); } } diff --git a/src/main/java/com/sparrowwallet/sparrow/event/WalletSettingsChangedEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/WalletSettingsChangedEvent.java index aae46e64..b385d600 100644 --- a/src/main/java/com/sparrowwallet/sparrow/event/WalletSettingsChangedEvent.java +++ b/src/main/java/com/sparrowwallet/sparrow/event/WalletSettingsChangedEvent.java @@ -7,22 +7,18 @@ import java.io.File; /** * This event is posted when a wallet's settings are changed (keystores, policy, script type). * This event marks a fundamental change that is used to update application level UI, clear node entry caches and similar. It should only be subscribed to by application-level classes. - * It does not extend WalletChangedEvent since that is used to save the wallet, and the wallet is saved directly in SettingsController before this event is posted. - * Note that all wallet detail controllers that share a WalletForm, that class posts WalletNodesChangedEvent once it has cleared it's entry caches. + * Note that WalletForm does not listen to this event to save the wallet, since the wallet is foreground saved directly in SettingsController before this event is posted. + * This is because any failure in saving the wallet must be immediately reported to the user. + * Note that all wallet detail controllers that share a WalletForm, and that class posts WalletNodesChangedEvent once it has cleared it's entry caches. */ -public class WalletSettingsChangedEvent { - private final Wallet wallet; +public class WalletSettingsChangedEvent extends WalletChangedEvent { private final File walletFile; public WalletSettingsChangedEvent(Wallet wallet, File walletFile) { - this.wallet = wallet; + super(wallet); this.walletFile = walletFile; } - public Wallet getWallet() { - return wallet; - } - public File getWalletFile() { return walletFile; } diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/HashIndexEntry.java b/src/main/java/com/sparrowwallet/sparrow/wallet/HashIndexEntry.java index 147ca59e..7e71ec06 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/HashIndexEntry.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/HashIndexEntry.java @@ -6,7 +6,7 @@ import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex; import com.sparrowwallet.drongo.wallet.Wallet; import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.control.DateLabel; -import com.sparrowwallet.sparrow.event.WalletChangedEvent; +import com.sparrowwallet.sparrow.event.WalletEntryLabelChangedEvent; import java.util.Collections; import java.util.List; @@ -27,7 +27,7 @@ public class HashIndexEntry extends Entry implements Comparable labelProperty().addListener((observable, oldValue, newValue) -> { hashIndex.setLabel(newValue); - EventManager.get().post(new WalletChangedEvent(wallet)); + EventManager.get().post(new WalletEntryLabelChangedEvent(wallet, this)); }); } diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/NodeEntry.java b/src/main/java/com/sparrowwallet/sparrow/wallet/NodeEntry.java index 3f8553e1..c62641ef 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/NodeEntry.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/NodeEntry.java @@ -5,7 +5,7 @@ import com.sparrowwallet.drongo.protocol.Script; import com.sparrowwallet.drongo.wallet.Wallet; import com.sparrowwallet.drongo.wallet.WalletNode; import com.sparrowwallet.sparrow.EventManager; -import com.sparrowwallet.sparrow.event.WalletChangedEvent; +import com.sparrowwallet.sparrow.event.WalletEntryLabelChangedEvent; import java.util.stream.Collectors; @@ -24,7 +24,7 @@ public class NodeEntry extends Entry { labelProperty().addListener((observable, oldValue, newValue) -> { node.setLabel(newValue); - EventManager.get().post(new WalletChangedEvent(wallet)); + EventManager.get().post(new WalletEntryLabelChangedEvent(wallet, this)); }); } diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/TransactionEntry.java b/src/main/java/com/sparrowwallet/sparrow/wallet/TransactionEntry.java index 368d1fac..fbd5fb81 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/TransactionEntry.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/TransactionEntry.java @@ -1,20 +1,22 @@ package com.sparrowwallet.sparrow.wallet; +import com.google.common.eventbus.Subscribe; import com.sparrowwallet.drongo.KeyPurpose; -import com.sparrowwallet.drongo.protocol.Sha256Hash; -import com.sparrowwallet.drongo.protocol.TransactionInput; import com.sparrowwallet.drongo.wallet.BlockTransaction; import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex; import com.sparrowwallet.drongo.wallet.Wallet; import com.sparrowwallet.sparrow.EventManager; -import com.sparrowwallet.sparrow.event.WalletChangedEvent; -import org.jetbrains.annotations.NotNull; +import com.sparrowwallet.sparrow.event.WalletBlockHeightChangedEvent; +import com.sparrowwallet.sparrow.event.WalletEntryLabelChangedEvent; +import javafx.beans.property.IntegerProperty; +import javafx.beans.property.IntegerPropertyBase; import java.util.*; import java.util.stream.Collectors; public class TransactionEntry extends Entry implements Comparable { - private static final int BLOCKS_TO_CONFIRM = 6; + public static final int BLOCKS_TO_CONFIRM = 6; + public static final int BLOCKS_TO_FULLY_CONFIRM = 100; private final Wallet wallet; private final BlockTransaction blockTransaction; @@ -27,8 +29,13 @@ public class TransactionEntry extends Entry implements Comparable { blockTransaction.setLabel(newValue); - EventManager.get().post(new WalletChangedEvent(wallet)); + EventManager.get().post(new WalletEntryLabelChangedEvent(wallet, this)); }); + + setConfirmations(calculateConfirmations()); + if(isFullyConfirming()) { + EventManager.get().register(this); + } } public Wallet getWallet() { @@ -66,7 +73,11 @@ public class TransactionEntry extends Entry implements Comparable createChildEntries(Wallet wallet, Map incoming, Map outgoing) { List incomingOutputEntries = incoming.entrySet().stream().map(input -> new TransactionHashIndexEntry(wallet, input.getKey(), HashIndexEntry.Type.OUTPUT, input.getValue())).collect(Collectors.toList()); List outgoingInputEntries = outgoing.entrySet().stream().map(output -> new TransactionHashIndexEntry(wallet, output.getKey(), HashIndexEntry.Type.INPUT, output.getValue())).collect(Collectors.toList()); @@ -116,7 +138,51 @@ public class TransactionEntry extends Entry implements Comparable EventManager.get().post(new WalletHistoryChangedEvent(wallet, historyChangedNodes))); + Platform.runLater(() -> EventManager.get().post(new WalletHistoryChangedEvent(wallet, blockHeight, historyChangedNodes))); } else if(blockHeight != null && !blockHeight.equals(previousWallet.getStoredBlockHeight())) { Platform.runLater(() -> EventManager.get().post(new WalletBlockHeightChangedEvent(wallet, blockHeight))); } @@ -135,7 +135,16 @@ public class WalletForm { } @Subscribe - public void walletChanged(WalletChangedEvent event) { + public void walletLabelChanged(WalletEntryLabelChangedEvent event) { + backgroundSaveWallet(event); + } + + @Subscribe + public void walletBlockHeightChanged(WalletBlockHeightChangedEvent event) { + backgroundSaveWallet(event); + } + + private void backgroundSaveWallet(WalletChangedEvent event) { if(event.getWallet().equals(wallet)) { try { save(); diff --git a/src/main/resources/com/sparrowwallet/sparrow/wallet/transactions.fxml b/src/main/resources/com/sparrowwallet/sparrow/wallet/transactions.fxml index 1fbf0e14..58766c04 100644 --- a/src/main/resources/com/sparrowwallet/sparrow/wallet/transactions.fxml +++ b/src/main/resources/com/sparrowwallet/sparrow/wallet/transactions.fxml @@ -13,7 +13,7 @@ -
diff --git a/src/main/resources/com/sparrowwallet/sparrow/wallet/wallet.css b/src/main/resources/com/sparrowwallet/sparrow/wallet/wallet.css index 2341879b..70ca9fbe 100644 --- a/src/main/resources/com/sparrowwallet/sparrow/wallet/wallet.css +++ b/src/main/resources/com/sparrowwallet/sparrow/wallet/wallet.css @@ -47,7 +47,11 @@ -fx-text-fill: #a0a1a7; } -.tree-table-row-cell:selected .hashindex-row { +.transaction-row.confirming { + -fx-text-fill: #696c77; +} + +.tree-table-row-cell:selected .hashindex-row, .tree-table-row-cell:selected .transaction-row { -fx-text-fill: white; } @@ -63,6 +67,27 @@ -fx-strikethrough: true; } +.amount-cell .confirmation-progress { + -fx-pref-width: 14; + -fx-padding: 0 8 0 0; +} + +.confirmation-progress-circle, .confirmation-progress-tick { + -fx-stroke: -fx-text-base-color; +} + +.tree-table-row-cell:selected .confirmation-progress-circle, .tree-table-row-cell:selected .confirmation-progress-tick { + -fx-stroke: white; +} + +.confirmation-progress-arc { + -fx-fill: -fx-text-base-color; +} + +.tree-table-row-cell:selected .confirmation-progress-arc { + -fx-fill: white; +} + .entry-cell .button { -fx-padding: 0; -fx-pref-height: 18; @@ -78,3 +103,7 @@ .entry-cell:hover .button .label .text { -fx-fill: -fx-text-base-color; } + +.tree-table-row-cell:selected .entry-cell:hover .button .label .text { + -fx-fill: white; +}