diff --git a/src/main/java/com/sparrowwallet/sparrow/control/AddressTreeTable.java b/src/main/java/com/sparrowwallet/sparrow/control/AddressTreeTable.java index 6a5ec20b..313151f9 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/AddressTreeTable.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/AddressTreeTable.java @@ -6,7 +6,10 @@ import com.sparrowwallet.drongo.protocol.Transaction; import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.event.ReceiveActionEvent; import com.sparrowwallet.sparrow.event.ReceiveToEvent; +import com.sparrowwallet.sparrow.event.TransactionViewEvent; +import com.sparrowwallet.sparrow.glyphfont.FontAwesome5; import com.sparrowwallet.sparrow.wallet.Entry; +import com.sparrowwallet.sparrow.wallet.HashIndexEntry; import com.sparrowwallet.sparrow.wallet.NodeEntry; import javafx.application.Platform; import javafx.beans.property.ReadOnlyObjectWrapper; @@ -56,9 +59,9 @@ public class AddressTreeTable extends TreeTableView { labelCol.setSortable(false); getColumns().add(labelCol); - TreeTableColumn amountCol = new TreeTableColumn<>("Amount"); + TreeTableColumn amountCol = new TreeTableColumn<>("Value"); amountCol.setCellValueFactory((TreeTableColumn.CellDataFeatures param) -> { - return new ReadOnlyObjectWrapper<>(param.getValue().getValue().getAmount()); + return new ReadOnlyObjectWrapper<>(param.getValue().getValue().getValue()); }); amountCol.setCellFactory(p -> new AmountCell()); amountCol.setSortable(false); @@ -76,16 +79,36 @@ public class AddressTreeTable extends TreeTableView { setColumnResizePolicy(TreeTableView.CONSTRAINED_RESIZE_POLICY); } + private static void applyRowStyles(TreeTableCell cell, Entry entry) { + cell.getStyleClass().remove("node-row"); + cell.getStyleClass().remove("hashindex-row"); + cell.getStyleClass().remove("spent"); + + if(entry != null) { + if(entry instanceof NodeEntry) { + cell.getStyleClass().add("node-row"); + } else if(entry instanceof HashIndexEntry) { + cell.getStyleClass().add("hashindex-row"); + HashIndexEntry hashIndexEntry = (HashIndexEntry)entry; + if(hashIndexEntry.isSpent()) { + cell.getStyleClass().add("spent"); + } + } + } + } + private static class DataCell extends TreeTableCell { public DataCell() { super(); - getStyleClass().add("address-cell"); } @Override protected void updateItem(Entry entry, boolean empty) { super.updateItem(entry, empty); + applyRowStyles(this, entry); + getStyleClass().remove("address-cell"); + if(empty) { setText(null); setGraphic(null); @@ -95,8 +118,14 @@ public class AddressTreeTable extends TreeTableView { Address address = nodeEntry.getAddress(); setText(address.toString()); setContextMenu(new AddressContextMenu(address)); - } else { - //TODO: Add transaction outpoint + getStyleClass().add("address-cell"); + } else if(entry instanceof HashIndexEntry) { + HashIndexEntry hashIndexEntry = (HashIndexEntry)entry; + setText(hashIndexEntry.getDescription()); + setContextMenu(new HashIndexEntryContextMenu(hashIndexEntry)); + Tooltip tooltip = new Tooltip(); + tooltip.setText(hashIndexEntry.getHashIndex().toString()); + setTooltip(tooltip); } } } @@ -124,6 +153,21 @@ public class AddressTreeTable extends TreeTableView { } } + private static class HashIndexEntryContextMenu extends ContextMenu { + public HashIndexEntryContextMenu(HashIndexEntry hashIndexEntry) { + String label = "Copy " + (hashIndexEntry.getType().equals(HashIndexEntry.Type.OUTPUT) ? "Transaction Output" : "Transaction Input"); + MenuItem copyHashIndex = new MenuItem(label); + copyHashIndex.setOnAction(AE -> { + hide(); + ClipboardContent content = new ClipboardContent(); + content.putString(hashIndexEntry.getHashIndex().toString()); + Clipboard.getSystemClipboard().setContent(content); + }); + + getItems().add(copyHashIndex); + } + } + private static class LabelCell extends TextFieldTreeTableCell { public LabelCell() { super(new DefaultStringConverter()); @@ -138,6 +182,8 @@ public class AddressTreeTable extends TreeTableView { setText(null); setGraphic(null); } else { + applyRowStyles(this, getTreeTableView().getTreeItem(getIndex()).getValue()); + setText(label); setContextMenu(new LabelContextMenu(label)); } @@ -212,16 +258,15 @@ public class AddressTreeTable extends TreeTableView { setText(null); setGraphic(null); } else { - String satsValue = String.format(Locale.ENGLISH, "%,d", amount) + " sats"; - String btcValue = CoinLabel.BTC_FORMAT.format(amount.doubleValue() / Transaction.SATOSHIS_PER_BITCOIN) + " BTC"; + applyRowStyles(this, getTreeTableView().getTreeItem(getIndex()).getValue()); + + String satsValue = String.format(Locale.ENGLISH, "%,d", amount); + String btcValue = CoinLabel.getBTCFormat().format(amount.doubleValue() / Transaction.SATOSHIS_PER_BITCOIN) + " BTC"; + Tooltip tooltip = new Tooltip(); - if(amount > CoinLabel.MAX_SATS_SHOWN) { - tooltip.setText(satsValue); - setText(btcValue); - } else { - tooltip.setText(btcValue); - setText(satsValue); - } + tooltip.setText(btcValue); + + setText(satsValue); setTooltip(tooltip); } } @@ -230,6 +275,7 @@ public class AddressTreeTable extends TreeTableView { private static class ActionCell extends TreeTableCell { private final HBox actionBox; private final Button receiveButton; + private final Button viewTransactionButton; public ActionCell() { super(); @@ -248,6 +294,15 @@ public class AddressTreeTable extends TreeTableView { EventManager.get().post(new ReceiveActionEvent(nodeEntry)); Platform.runLater(() -> EventManager.get().post(new ReceiveToEvent(nodeEntry))); }); + + viewTransactionButton = new Button(""); + Glyph searchGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.SEARCH); + searchGlyph.setFontSize(12); + viewTransactionButton.setGraphic(searchGlyph); + viewTransactionButton.setOnAction(event -> { + HashIndexEntry hashIndexEntry = (HashIndexEntry)getTreeTableView().getTreeItem(getIndex()).getValue(); + EventManager.get().post(new TransactionViewEvent(hashIndexEntry.getBlockTransaction(), hashIndexEntry)); + }); } @Override @@ -256,9 +311,13 @@ public class AddressTreeTable extends TreeTableView { if (empty) { setGraphic(null); } else { + applyRowStyles(this, getTreeTableView().getTreeItem(getIndex()).getValue()); + actionBox.getChildren().remove(0, actionBox.getChildren().size()); if(entry instanceof NodeEntry) { actionBox.getChildren().add(receiveButton); + } else if(entry instanceof HashIndexEntry) { + actionBox.getChildren().add(viewTransactionButton); } setGraphic(actionBox); diff --git a/src/main/java/com/sparrowwallet/sparrow/control/CoinLabel.java b/src/main/java/com/sparrowwallet/sparrow/control/CoinLabel.java index 0571a836..1c5f7ebd 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/CoinLabel.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/CoinLabel.java @@ -85,4 +85,9 @@ public class CoinLabel extends CopyableLabel { getItems().addAll(copySatsValue, copyBtcValue); } } + + public static DecimalFormat getBTCFormat() { + BTC_FORMAT.setMaximumFractionDigits(8); + return BTC_FORMAT; + } } diff --git a/src/main/java/com/sparrowwallet/sparrow/control/DateLabel.java b/src/main/java/com/sparrowwallet/sparrow/control/DateLabel.java new file mode 100644 index 00000000..d0619b70 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/control/DateLabel.java @@ -0,0 +1,31 @@ +package com.sparrowwallet.sparrow.control; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Date; + +public class DateLabel extends CopyableLabel { + private static final DateFormat LAST_24_HR = new SimpleDateFormat("HH:mm"); + private static final DateFormat LAST_YEAR = new SimpleDateFormat("d MMM"); + private static final DateFormat ALL_TIME = new SimpleDateFormat("yyyy/MM/dd"); + + private Date date; + + public DateLabel(Date date) { + super(getShortDateFormat(date)); + this.date = date; + } + + public static String getShortDateFormat(Date date) { + Date now = new Date(); + long elapsed = (now.getTime() - date.getTime()) / 1000; + + if(elapsed < 24 * 60 * 60) { + return LAST_24_HR.format(date); + } else if(elapsed < 365 * 24 * 60 * 60) { + return LAST_YEAR.format(date); + } else { + return ALL_TIME.format(date); + } + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/control/RecursiveTreeItem.java b/src/main/java/com/sparrowwallet/sparrow/control/RecursiveTreeItem.java index 4b8ecc9c..ebeb52ca 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/RecursiveTreeItem.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/RecursiveTreeItem.java @@ -37,7 +37,7 @@ public class RecursiveTreeItem extends TreeItem { } }); - this.setExpanded(true); + this.setExpanded(false); } private void addChildrenListener(T value){ diff --git a/src/main/java/com/sparrowwallet/sparrow/event/TransactionViewEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/TransactionViewEvent.java new file mode 100644 index 00000000..92d1e3ed --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/event/TransactionViewEvent.java @@ -0,0 +1,26 @@ +package com.sparrowwallet.sparrow.event; + +import com.sparrowwallet.drongo.wallet.BlockTransaction; +import com.sparrowwallet.sparrow.wallet.HashIndexEntry; + +public class TransactionViewEvent { + public final BlockTransaction blockTransaction; + public final HashIndexEntry hashIndexEntry; + + public TransactionViewEvent(BlockTransaction blockTransaction) { + this(blockTransaction, null); + } + + public TransactionViewEvent(BlockTransaction blockTransaction, HashIndexEntry hashIndexEntry) { + this.blockTransaction = blockTransaction; + this.hashIndexEntry = hashIndexEntry; + } + + public BlockTransaction getBlockTransaction() { + return blockTransaction; + } + + public HashIndexEntry getHashIndexEntry() { + return hashIndexEntry; + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/glyphfont/FontAwesome5.java b/src/main/java/com/sparrowwallet/sparrow/glyphfont/FontAwesome5.java index a132eb4e..5e980c2a 100644 --- a/src/main/java/com/sparrowwallet/sparrow/glyphfont/FontAwesome5.java +++ b/src/main/java/com/sparrowwallet/sparrow/glyphfont/FontAwesome5.java @@ -26,6 +26,7 @@ public class FontAwesome5 extends GlyphFont { LOCK_OPEN('\uf3c1'), QUESTION_CIRCLE('\uf059'), SD_CARD('\uf7c2'), + SEARCH('\uf002'), WALLET('\uf555'); private final char ch; diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/Entry.java b/src/main/java/com/sparrowwallet/sparrow/wallet/Entry.java index bdb53280..888b5d33 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/Entry.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/Entry.java @@ -32,5 +32,5 @@ public abstract class Entry { return children; } - public abstract Long getAmount(); + public abstract Long getValue(); } diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/HashIndexEntry.java b/src/main/java/com/sparrowwallet/sparrow/wallet/HashIndexEntry.java new file mode 100644 index 00000000..ff00f18f --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/HashIndexEntry.java @@ -0,0 +1,64 @@ +package com.sparrowwallet.sparrow.wallet; + +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.control.DateLabel; +import com.sparrowwallet.sparrow.event.WalletChangedEvent; + +import java.util.Collections; +import java.util.List; + +public class HashIndexEntry extends Entry { + private final Wallet wallet; + private final BlockTransactionHashIndex hashIndex; + private final Type type; + + public HashIndexEntry(Wallet wallet, BlockTransactionHashIndex hashIndex, Type type) { + super(hashIndex.getLabel(), hashIndex.getSpentBy() != null ? List.of(new HashIndexEntry(wallet, hashIndex.getSpentBy(), Type.INPUT)) : Collections.emptyList()); + this.wallet = wallet; + this.hashIndex = hashIndex; + this.type = type; + + labelProperty().addListener((observable, oldValue, newValue) -> { + hashIndex.setLabel(newValue); + EventManager.get().post(new WalletChangedEvent(wallet)); + }); + } + + public Wallet getWallet() { + return wallet; + } + + public BlockTransactionHashIndex getHashIndex() { + return hashIndex; + } + + public Type getType() { + return type; + } + + public BlockTransaction getBlockTransaction() { + return wallet.getTransactions().get(hashIndex.getHash()); + } + + public String getDescription() { + return (type.equals(Type.INPUT) ? "Spent by " : "Received from ") + + getHashIndex().getHash().toString().substring(0, 8) + "...:" + getHashIndex().getIndex() + + " on " + DateLabel.getShortDateFormat(getHashIndex().getDate()); + } + + public boolean isSpent() { + return getType().equals(HashIndexEntry.Type.INPUT) || getHashIndex().getSpentBy() != null; + } + + @Override + public Long getValue() { + return hashIndex.getValue(); + } + + public enum Type { + INPUT, OUTPUT + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/NodeEntry.java b/src/main/java/com/sparrowwallet/sparrow/wallet/NodeEntry.java index 73dcca13..6c88c5d4 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/NodeEntry.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/NodeEntry.java @@ -14,7 +14,11 @@ public class NodeEntry extends Entry { private final WalletNode node; public NodeEntry(Wallet wallet, WalletNode node) { - super(node.getLabel(), node.getChildren().stream().map(childNode -> new NodeEntry(wallet, childNode)).collect(Collectors.toList())); + super(node.getLabel(), + !node.getChildren().isEmpty() ? + node.getChildren().stream().map(childNode -> new NodeEntry(wallet, childNode)).collect(Collectors.toList()) : + node.getTransactionOutputs().stream().map(txo -> new HashIndexEntry(wallet, txo, HashIndexEntry.Type.OUTPUT)).collect(Collectors.toList())); + this.wallet = wallet; this.node = node; @@ -45,9 +49,11 @@ public class NodeEntry extends Entry { } @Override - public Long getAmount() { - //TODO: Iterate through TransactionEntries to calculate amount + public Long getValue() { + if(node.getTransactionOutputs().isEmpty()) { + return null; + } - return null; + return node.getUnspentValue(); } } diff --git a/src/main/resources/com/sparrowwallet/sparrow/wallet/addresses.css b/src/main/resources/com/sparrowwallet/sparrow/wallet/addresses.css index 51509146..febb528a 100644 --- a/src/main/resources/com/sparrowwallet/sparrow/wallet/addresses.css +++ b/src/main/resources/com/sparrowwallet/sparrow/wallet/addresses.css @@ -8,10 +8,30 @@ -fx-font-family: Courier; } +.hashindex-row { + -fx-background-color: #fafafa; +} + +.hashindex-row .text { + -fx-fill: #696c77; +} + +.hashindex-row.spent .text { + -fx-fill: #a0a1a7; +} + .label-cell .text-field { -fx-padding: 0; } +.amount-cell { + -fx-alignment: center-right; +} + +.amount-cell.spent .text { + -fx-strikethrough: true; +} + .action-cell .button { -fx-padding: 0; -fx-pref-height: 18;