add wallet search dialog for labels, address, values and txids

This commit is contained in:
Craig Raw 2022-01-31 17:15:30 +02:00
parent 3820b9838d
commit 6d2167428f
22 changed files with 410 additions and 35 deletions

View file

@ -34,6 +34,7 @@ import com.sparrowwallet.sparrow.soroban.SorobanServices;
import com.sparrowwallet.sparrow.transaction.TransactionController;
import com.sparrowwallet.sparrow.transaction.TransactionData;
import com.sparrowwallet.sparrow.transaction.TransactionView;
import com.sparrowwallet.sparrow.wallet.Entry;
import com.sparrowwallet.sparrow.wallet.WalletController;
import com.sparrowwallet.sparrow.wallet.WalletForm;
import com.sparrowwallet.sparrow.whirlpool.Whirlpool;
@ -155,6 +156,9 @@ public class AppController implements Initializable {
@FXML
private MenuItem lockWallet;
@FXML
private MenuItem searchWallet;
@FXML
private MenuItem refreshWallet;
@ -334,6 +338,7 @@ public class AppController implements Initializable {
showPSBT.visibleProperty().bind(saveTransaction.visibleProperty().not());
exportWallet.setDisable(true);
lockWallet.setDisable(true);
searchWallet.disableProperty().bind(exportWallet.disableProperty());
refreshWallet.disableProperty().bind(Bindings.or(exportWallet.disableProperty(), Bindings.or(serverToggle.disableProperty(), AppServices.onlineProperty().not())));
sendToMany.disableProperty().bind(exportWallet.disableProperty());
sweepPrivateKey.disableProperty().bind(Bindings.or(serverToggle.disableProperty(), AppServices.onlineProperty().not()));
@ -1353,6 +1358,19 @@ public class AppController implements Initializable {
}
}
public void searchWallet(ActionEvent event) {
WalletForm selectedWalletForm = getSelectedWalletForm();
if(selectedWalletForm != null) {
SearchWalletDialog searchWalletDialog = new SearchWalletDialog(selectedWalletForm);
Optional<Entry> optEntry = searchWalletDialog.showAndWait();
if(optEntry.isPresent()) {
Entry entry = optEntry.get();
EventManager.get().post(new FunctionActionEvent(entry.getWalletFunction(), entry.getWallet()));
Platform.runLater(() -> EventManager.get().post(new SelectEntryEvent(entry)));
}
}
}
public void refreshWallet(ActionEvent event) {
WalletForm selectedWalletForm = getSelectedWalletForm();
if(selectedWalletForm != null) {
@ -2430,12 +2448,7 @@ public class AppController implements Initializable {
}
@Subscribe
public void sendAction(SendActionEvent event) {
selectTab(event.getWallet());
}
@Subscribe
public void recieveAction(ReceiveActionEvent event) {
public void functionAction(FunctionActionEvent event) {
selectTab(event.getWallet());
}

View file

@ -80,6 +80,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
}
HBox actionBox = new HBox();
actionBox.getStyleClass().add("cell-actions");
Button viewTransactionButton = new Button("");
viewTransactionButton.setGraphic(getViewTransactionGlyph());
viewTransactionButton.setOnAction(event -> {
@ -118,6 +119,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
getStyleClass().add("address-cell");
HBox actionBox = new HBox();
actionBox.getStyleClass().add("cell-actions");
Button receiveButton = new Button("");
receiveButton.setGraphic(getReceiveGlyph());
receiveButton.setOnAction(event -> {
@ -152,6 +154,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
setTooltip(tooltip);
HBox actionBox = new HBox();
actionBox.getStyleClass().add("cell-actions");
Button viewTransactionButton = new Button("");
viewTransactionButton.setGraphic(getViewTransactionGlyph());
viewTransactionButton.setOnAction(event -> {

View file

@ -70,9 +70,6 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
dialogPane.setGraphic(imageView);
}
VBox vBox = new VBox();
vBox.setSpacing(20);
Form form = new Form();
Fieldset fieldset = new Fieldset();
fieldset.setText("");

View file

@ -0,0 +1,218 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.wallet.*;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.collections.ListChangeListener;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.VBox;
import org.controlsfx.control.textfield.TextFields;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import tornadofx.control.Field;
import tornadofx.control.Fieldset;
import tornadofx.control.Form;
import java.util.ArrayList;
import java.util.List;
public class SearchWalletDialog extends Dialog<Entry> {
private static final Logger log = LoggerFactory.getLogger(SearchWalletDialog.class);
private final WalletForm walletForm;
private final TextField search;
private final CoinTreeTable results;
public SearchWalletDialog(WalletForm walletForm) {
this.walletForm = walletForm;
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("search.css").toExternalForm());
AppServices.setStageIcon(dialogPane.getScene().getWindow());
dialogPane.setHeaderText("Search Wallet");
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);
}
VBox vBox = new VBox();
vBox.setSpacing(20);
Form form = new Form();
Fieldset fieldset = new Fieldset();
fieldset.setText("");
fieldset.setSpacing(10);
Field searchField = new Field();
searchField.setText("Search:");
search = TextFields.createClearableTextField();
search.setPromptText("Label, address, value or transaction ID");
searchField.getInputs().add(search);
fieldset.getChildren().addAll(searchField);
form.getChildren().add(fieldset);
results = new CoinTreeTable();
results.setShowRoot(false);
results.setPrefWidth(850);
results.setBitcoinUnit(walletForm.getWallet());
results.setColumnResizePolicy(TreeTableView.CONSTRAINED_RESIZE_POLICY);
results.setPlaceholder(new Label("No results"));
TreeTableColumn<Entry, String> typeColumn = new TreeTableColumn<>("Type");
typeColumn.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, String> param) -> {
return new ReadOnlyObjectWrapper<>(param.getValue().getValue().getEntryType());
});
results.getColumns().add(typeColumn);
TreeTableColumn<Entry, Entry> entryCol = new TreeTableColumn<>("Date / Address / Output");
entryCol.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, Entry> param) -> {
return new ReadOnlyObjectWrapper<>(param.getValue().getValue());
});
entryCol.setCellFactory(p -> new SearchEntryCell());
String address = walletForm.getNodeEntry(KeyPurpose.RECEIVE).getAddress().toString();
if(address != null) {
entryCol.setMinWidth(TextUtils.computeTextWidth(AppServices.getMonospaceFont(), address, 0.0));
}
results.getColumns().add(entryCol);
TreeTableColumn<Entry, String> labelCol = new TreeTableColumn<>("Label");
labelCol.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, String> param) -> {
return param.getValue().getValue().labelProperty();
});
labelCol.setCellFactory(p -> new SearchLabelCell());
results.getColumns().add(labelCol);
TreeTableColumn<Entry, Number> amountCol = new TreeTableColumn<>("Value");
amountCol.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, Number> param) -> {
return new ReadOnlyObjectWrapper<>(param.getValue().getValue().getValue());
});
amountCol.setCellFactory(p -> new CoinCell());
results.getColumns().add(amountCol);
vBox.getChildren().addAll(form, results);
dialogPane.setContent(vBox);
ButtonType showButtonType = new javafx.scene.control.ButtonType("Show", ButtonBar.ButtonData.APPLY);
ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
dialogPane.getButtonTypes().addAll(cancelButtonType, showButtonType);
Button showButton = (Button) dialogPane.lookupButton(showButtonType);
showButton.setDefaultButton(true);
showButton.setDisable(true);
setResultConverter(buttonType -> buttonType == showButtonType ? results.getSelectionModel().getSelectedItem().getValue() : null);
results.getSelectionModel().getSelectedIndices().addListener((ListChangeListener<Integer>) c -> {
showButton.setDisable(results.getSelectionModel().getSelectedCells().isEmpty());
});
search.textProperty().addListener((observable, oldValue, newValue) -> {
searchWallet(newValue.toLowerCase());
});
setResizable(true);
}
private void searchWallet(String searchText) {
List<Entry> matchingEntries = new ArrayList<>();
if(!searchText.isEmpty()) {
Long searchValue = null;
try {
searchValue = Math.abs(Long.parseLong(searchText));
} catch(NumberFormatException e) {
//ignore
}
WalletTransactionsEntry walletTransactionsEntry = walletForm.getWalletTransactionsEntry();
for(Entry entry : walletTransactionsEntry.getChildren()) {
if(entry instanceof TransactionEntry transactionEntry) {
if(transactionEntry.getBlockTransaction().getHash().toString().equals(searchText) ||
(transactionEntry.getLabel() != null && transactionEntry.getLabel().toLowerCase().contains(searchText)) ||
(transactionEntry.getValue() != null && searchValue != null && Math.abs(transactionEntry.getValue()) == searchValue)) {
matchingEntries.add(entry);
}
}
}
for(KeyPurpose keyPurpose : KeyPurpose.DEFAULT_PURPOSES) {
NodeEntry purposeEntry = walletForm.getNodeEntry(keyPurpose);
for(Entry entry : purposeEntry.getChildren()) {
if(entry instanceof NodeEntry nodeEntry) {
if(nodeEntry.getAddress().toString().contains(searchText) ||
(nodeEntry.getLabel() != null && nodeEntry.getLabel().toLowerCase().contains(searchText)) ||
(nodeEntry.getValue() != null && searchValue != null && Math.abs(nodeEntry.getValue()) == searchValue)) {
matchingEntries.add(entry);
}
}
}
}
WalletUtxosEntry walletUtxosEntry = walletForm.getWalletUtxosEntry();
for(Entry entry : walletUtxosEntry.getChildren()) {
if(entry instanceof HashIndexEntry hashIndexEntry) {
if(hashIndexEntry.getBlockTransaction().getHash().toString().equals(searchText) ||
(hashIndexEntry.getLabel() != null && hashIndexEntry.getLabel().toLowerCase().contains(searchText)) ||
(hashIndexEntry.getValue() != null && searchValue != null && Math.abs(hashIndexEntry.getValue()) == searchValue)) {
matchingEntries.add(entry);
}
}
}
}
SearchWalletEntry rootEntry = new SearchWalletEntry(walletForm.getWallet(), matchingEntries);
RecursiveTreeItem<Entry> rootItem = new RecursiveTreeItem<>(rootEntry, Entry::getChildren);
results.setRoot(rootItem);
}
private static class SearchWalletEntry extends Entry {
public SearchWalletEntry(Wallet wallet, List<Entry> entries) {
super(wallet, wallet.getName(), entries);
}
@Override
public Long getValue() {
return 0L;
}
@Override
public String getEntryType() {
return "Search Wallet Results";
}
@Override
public Function getWalletFunction() {
return null;
}
}
private static class SearchEntryCell extends EntryCell {
@Override
protected void updateItem(Entry entry, boolean empty) {
super.updateItem(entry, empty);
setContextMenu(null);
}
}
private static class SearchLabelCell extends LabelCell {
@Override
public void updateItem(String label, boolean empty) {
super.updateItem(label, empty);
setContextMenu(null);
}
}
}

View file

@ -0,0 +1,26 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.wallet.Function;
public class FunctionActionEvent {
private final Function function;
private final Wallet wallet;
public FunctionActionEvent(Function function, Wallet wallet) {
this.function = function;
this.wallet = wallet;
}
public Function getFunction() {
return function;
}
public Wallet getWallet() {
return wallet;
}
public boolean selectFunction() {
return true;
}
}

View file

@ -1,20 +1,15 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.wallet.Function;
import com.sparrowwallet.sparrow.wallet.NodeEntry;
public class ReceiveActionEvent {
private final Wallet wallet;
public class ReceiveActionEvent extends FunctionActionEvent {
public ReceiveActionEvent(NodeEntry receiveEntry) {
this.wallet = receiveEntry.getWallet();
super(Function.RECEIVE, receiveEntry.getWallet());
}
public ReceiveActionEvent(Wallet wallet) {
this.wallet = wallet;
}
public Wallet getWallet() {
return wallet;
super(Function.RECEIVE, wallet);
}
}

View file

@ -0,0 +1,20 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.wallet.Entry;
public class SelectEntryEvent {
private final Entry entry;
public SelectEntryEvent(Entry entry) {
this.entry = entry;
}
public Entry getEntry() {
return entry;
}
public Wallet getWallet() {
return entry.getWallet();
}
}

View file

@ -2,23 +2,24 @@ package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.wallet.Function;
import java.util.List;
public class SendActionEvent {
private final Wallet wallet;
public class SendActionEvent extends FunctionActionEvent {
private final List<BlockTransactionHashIndex> utxos;
public SendActionEvent(Wallet wallet, List<BlockTransactionHashIndex> utxos) {
this.wallet = wallet;
super(Function.SEND, wallet);
this.utxos = utxos;
}
public Wallet getWallet() {
return wallet;
}
public List<BlockTransactionHashIndex> getUtxos() {
return utxos;
}
@Override
public boolean selectFunction() {
return !getUtxos().isEmpty();
}
}

View file

@ -100,6 +100,16 @@ public class AddressesController extends WalletFormController implements Initial
}
}
@Subscribe
public void selectEntry(SelectEntryEvent event) {
if(event.getWallet().equals(getWalletForm().getWallet()) && event.getEntry().getWalletFunction() == Function.ADDRESSES) {
List<AddressTreeTable> addressTreeTables = List.of(receiveTable, changeTable);
for(AddressTreeTable addressTreeTable : addressTreeTables) {
selectEntry(addressTreeTable, event.getEntry());
}
}
}
public void exportReceiveAddresses(ActionEvent event) {
exportAddresses(KeyPurpose.RECEIVE);
}

View file

@ -42,6 +42,10 @@ public abstract class Entry {
public abstract Long getValue();
public abstract String getEntryType();
public abstract Function getWalletFunction();
public void updateLabel(Entry entry) {
if(this.equals(entry)) {
labelProperty.set(entry.getLabel());

View file

@ -69,6 +69,16 @@ public class HashIndexEntry extends Entry implements Comparable<HashIndexEntry>
return hashIndex.getValue();
}
@Override
public String getEntryType() {
return "Hash Index";
}
@Override
public Function getWalletFunction() {
return Function.ADDRESSES;
}
public enum Type {
INPUT, OUTPUT
}

View file

@ -63,6 +63,16 @@ public class NodeEntry extends Entry implements Comparable<NodeEntry> {
return node.getUnspentValue();
}
@Override
public String getEntryType() {
return "Address";
}
@Override
public Function getWalletFunction() {
return Function.ADDRESSES;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;

View file

@ -74,6 +74,16 @@ public class TransactionEntry extends Entry implements Comparable<TransactionEnt
return value;
}
@Override
public String getEntryType() {
return "Transaction";
}
@Override
public Function getWalletFunction() {
return Function.TRANSACTIONS;
}
public boolean isConfirming() {
return getConfirmations() < BlockTransactionHash.BLOCKS_TO_CONFIRM;
}

View file

@ -277,4 +277,11 @@ public class TransactionsController extends WalletFormController implements Init
public void loadingLogChanged(LoadingLogChangedEvent event) {
transactionsMasterDetail.setShowDetailNode(event.isVisible());
}
@Subscribe
public void selectEntry(SelectEntryEvent event) {
if(event.getWallet().equals(getWalletForm().getWallet()) && event.getEntry().getWalletFunction() == Function.TRANSACTIONS) {
selectEntry(transactionsTable, event.getEntry());
}
}
}

View file

@ -37,6 +37,16 @@ public class UtxoEntry extends HashIndexEntry {
return false;
}
@Override
public String getEntryType() {
return "UTXO";
}
@Override
public Function getWalletFunction() {
return Function.UTXOS;
}
public boolean isMixing() {
return mixStatusProperty != null && ((mixStatusProperty.get().getMixProgress() != null && mixStatusProperty.get().getMixProgress().getMixStep() != MixStep.FAIL) || mixStatusProperty.get().getNextMixUtxo() != null);
}

View file

@ -604,4 +604,12 @@ public class UtxosController extends WalletFormController implements Initializab
}
}
}
@Subscribe
public void selectEntry(SelectEntryEvent event) {
if(event.getWallet().equals(getWalletForm().getWallet()) && event.getEntry().getWalletFunction() == Function.UTXOS) {
utxosTable.getSelectionModel().clearSelection();
selectEntry(utxosTable, event.getEntry());
}
}
}

View file

@ -221,16 +221,9 @@ public class WalletController extends WalletFormController implements Initializa
}
@Subscribe
public void receiveAction(ReceiveActionEvent event) {
if(event.getWallet().equals(walletForm.getWallet())) {
selectFunction(Function.RECEIVE);
}
}
@Subscribe
public void sendAction(SendActionEvent event) {
if(!event.getUtxos().isEmpty() && event.getWallet().equals(walletForm.getWallet())) {
selectFunction(Function.SEND);
public void functionAction(FunctionActionEvent event) {
if(event.selectFunction() && event.getWallet().equals(walletForm.getWallet())) {
selectFunction(event.getFunction());
}
}

View file

@ -10,6 +10,9 @@ import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.WalletTabData;
import com.sparrowwallet.sparrow.control.FiatLabel;
import com.sparrowwallet.sparrow.event.WalletTabsClosedEvent;
import javafx.application.Platform;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeTableView;
public abstract class WalletFormController extends BaseController {
public WalletForm walletForm;
@ -64,4 +67,17 @@ public abstract class WalletFormController extends BaseController {
fiatLabel.setBtcRate(0.0);
}
}
protected void selectEntry(TreeTableView<Entry> treeTableView, Entry entry) {
for(TreeItem<Entry> treeEntry : treeTableView.getRoot().getChildren()) {
if(treeEntry.getValue().equals(entry)) {
treeTableView.getSelectionModel().select(treeEntry);
Platform.runLater(() -> {
treeTableView.requestFocus();
treeTableView.scrollTo(treeTableView.getSelectionModel().getSelectedIndex());
});
break;
}
}
}
}

View file

@ -30,6 +30,16 @@ public class WalletTransactionsEntry extends Entry {
return getBalance();
}
@Override
public String getEntryType() {
return "Wallet Transactions";
}
@Override
public Function getWalletFunction() {
return Function.TRANSACTIONS;
}
private void calculateBalances(boolean resort) {
long balance = 0L;
long mempoolBalance = 0L;

View file

@ -20,6 +20,16 @@ public class WalletUtxosEntry extends Entry {
return 0L;
}
@Override
public String getEntryType() {
return "Wallet UTXOs";
}
@Override
public Function getWalletFunction() {
return Function.UTXOS;
}
protected void calculateDuplicates() {
Map<String, UtxoEntry> addressMap = new HashMap<>();

View file

@ -104,6 +104,7 @@
<MenuItem fx:id="lockWallet" mnemonicParsing="false" text="Lock Wallet" accelerator="Shortcut+L" onAction="#lockWallet"/>
<MenuItem mnemonicParsing="false" text="Lock All Wallets" accelerator="Shortcut+Shift+L" onAction="#lockWallets"/>
<SeparatorMenuItem />
<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"/>
</items>
</Menu>

View file

@ -0,0 +1,3 @@
.cell-actions {
visibility: hidden;
}