addresses table

This commit is contained in:
Craig Raw
2020-05-26 10:19:34 +02:00
parent 3a8581da13
commit cabd62166a
14 changed files with 419 additions and 19 deletions
@@ -252,12 +252,13 @@ public class AppController implements Initializable {
FileType fileType = IOUtils.getFileType(file);
if(FileType.JSON.equals(fileType)) {
Wallet wallet = storage.loadWallet();
restorePublicKeysFromSeed(wallet, null);
Tab tab = addWalletTab(storage, wallet);
tabs.getSelectionModel().select(tab);
} else if(FileType.BINARY.equals(fileType)) {
WalletPasswordDialog dlg = new WalletPasswordDialog(WalletPasswordDialog.PasswordRequirement.LOAD);
Optional<SecureString> optionalPassword = dlg.showAndWait();
if(!optionalPassword.isPresent()) {
if(optionalPassword.isEmpty()) {
return;
}
@@ -0,0 +1,295 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.protocol.Transaction;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.ReceiveActionEvent;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.event.Event;
import javafx.geometry.Pos;
import javafx.scene.control.*;
import javafx.scene.control.cell.TextFieldTreeTableCell;
import javafx.scene.input.Clipboard;
import javafx.scene.input.ClipboardContent;
import javafx.scene.layout.HBox;
import javafx.scene.text.Font;
import javafx.util.converter.DefaultStringConverter;
import org.controlsfx.glyphfont.FontAwesome;
import org.controlsfx.glyphfont.Glyph;
import java.lang.reflect.Field;
import java.util.Locale;
public class AddressTreeTable extends TreeTableView<AddressTreeTable.Data> {
public void initialize(Wallet.Node rootNode) {
getStyleClass().add("address-treetable");
String address = null;
Data rootData = new Data(rootNode);
TreeItem<Data> rootItem = new TreeItem<>(rootData);
for(Wallet.Node childNode : rootNode.getChildren()) {
Data childData = new Data(childNode);
TreeItem<Data> childItem = new TreeItem<>(childData);
rootItem.getChildren().add(childItem);
address = childNode.getAddress().toString();
}
rootItem.setExpanded(true);
setRoot(rootItem);
setShowRoot(false);
TreeTableColumn<Data, Data> addressCol = new TreeTableColumn<>("Address / Outpoints");
addressCol.setCellValueFactory((TreeTableColumn.CellDataFeatures<Data, Data> param) -> {
return new ReadOnlyObjectWrapper<>(param.getValue().getValue());
});
addressCol.setCellFactory(p -> new DataCell());
addressCol.setSortable(false);
getColumns().add(addressCol);
if(address != null) {
addressCol.setMinWidth(TextUtils.computeTextWidth(Font.font("Courier"), address, 0.0));
}
TreeTableColumn<Data, String> labelCol = new TreeTableColumn<>("Label");
labelCol.setCellValueFactory((TreeTableColumn.CellDataFeatures<Data, String> param) -> {
return param.getValue().getValue().getLabelProperty();
});
labelCol.setCellFactory(p -> new LabelCell());
labelCol.setSortable(false);
getColumns().add(labelCol);
TreeTableColumn<Data, Long> amountCol = new TreeTableColumn<>("Amount");
amountCol.setCellValueFactory((TreeTableColumn.CellDataFeatures<Data, Long> param) -> {
return new ReadOnlyObjectWrapper<>(param.getValue().getValue().getAmount());
});
amountCol.setCellFactory(p -> new AmountCell());
amountCol.setSortable(false);
getColumns().add(amountCol);
TreeTableColumn<Data, Void> actionCol = new TreeTableColumn<>("Actions");
actionCol.setCellFactory(p -> new ActionCell());
actionCol.setSortable(false);
getColumns().add(actionCol);
setEditable(true);
setColumnResizePolicy(TreeTableView.CONSTRAINED_RESIZE_POLICY);
}
public static class Data {
private final Wallet.Node walletNode;
private final SimpleStringProperty labelProperty;
private final Long amount;
public Data(Wallet.Node walletNode) {
this.walletNode = walletNode;
labelProperty = new SimpleStringProperty(walletNode.getLabel());
labelProperty.addListener((observable, oldValue, newValue) -> walletNode.setLabel(newValue));
amount = walletNode.getAmount();
}
public Wallet.Node getWalletNode() {
return walletNode;
}
public String getLabel() {
return labelProperty.get();
}
public StringProperty getLabelProperty() {
return labelProperty;
}
public Long getAmount() {
return amount;
}
}
private static class DataCell extends TreeTableCell<Data, Data> {
public DataCell() {
super();
getStyleClass().add("data-cell");
}
@Override
protected void updateItem(Data data, boolean empty) {
super.updateItem(data, empty);
if(empty) {
setText(null);
setGraphic(null);
} else {
if(data.getWalletNode() != null) {
Address address = data.getWalletNode().getAddress();
setText(address.toString());
setContextMenu(new AddressContextMenu(address));
} else {
//TODO: Add transaction outpoint
}
}
}
}
private static class AddressContextMenu extends ContextMenu {
public AddressContextMenu(Address address) {
MenuItem copyAddress = new MenuItem("Copy Address");
copyAddress.setOnAction(AE -> {
hide();
ClipboardContent content = new ClipboardContent();
content.putString(address.toString());
Clipboard.getSystemClipboard().setContent(content);
});
MenuItem copyHex = new MenuItem("Copy Script Output Bytes");
copyHex.setOnAction(AE -> {
hide();
ClipboardContent content = new ClipboardContent();
content.putString(Utils.bytesToHex(address.getOutputScriptData()));
Clipboard.getSystemClipboard().setContent(content);
});
getItems().addAll(copyAddress, copyHex);
}
}
private static class LabelCell extends TextFieldTreeTableCell<Data, String> {
public LabelCell() {
super(new DefaultStringConverter());
getStyleClass().add("label-cell");
}
@Override
public void updateItem(String label, boolean empty) {
super.updateItem(label, empty);
if(empty) {
setText(null);
setGraphic(null);
} else {
setText(label);
setContextMenu(new LabelContextMenu(label));
}
}
@Override
public void commitEdit(String label) {
// This block is necessary to support commit on losing focus, because
// the baked-in mechanism sets our editing state to false before we can
// intercept the loss of focus. The default commitEdit(...) method
// simply bails if we are not editing...
if (!isEditing() && !label.equals(getItem())) {
TreeTableView<Data> treeTable = getTreeTableView();
if(treeTable != null) {
TreeTableColumn<Data, String> column = getTableColumn();
TreeTableColumn.CellEditEvent<Data, String> event = new TreeTableColumn.CellEditEvent<>(
treeTable, new TreeTablePosition<>(treeTable, getIndex(), column),
TreeTableColumn.editCommitEvent(), label
);
Event.fireEvent(column, event);
}
}
super.commitEdit(label);
}
@Override
public void startEdit() {
super.startEdit();
try {
Field f = getClass().getSuperclass().getDeclaredField("textField");
f.setAccessible(true);
TextField textField = (TextField)f.get(this);
textField.focusedProperty().addListener((obs, wasFocused, isNowFocused) -> {
if (!isNowFocused) {
commitEdit(getConverter().fromString(textField.getText()));
setText(getConverter().fromString(textField.getText()));
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
}
private static class LabelContextMenu extends ContextMenu {
public LabelContextMenu(String label) {
MenuItem copyLabel = new MenuItem("Copy Label");
copyLabel.setOnAction(AE -> {
hide();
ClipboardContent content = new ClipboardContent();
content.putString(label);
Clipboard.getSystemClipboard().setContent(content);
});
getItems().add(copyLabel);
}
}
private static class AmountCell extends TreeTableCell<Data, Long> {
public AmountCell() {
super();
getStyleClass().add("amount-cell");
}
@Override
protected void updateItem(Long amount, boolean empty) {
super.updateItem(amount, empty);
if(empty || amount == null) {
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";
Tooltip tooltip = new Tooltip();
if(amount > CoinLabel.MAX_SATS_SHOWN) {
tooltip.setText(satsValue);
setText(btcValue);
} else {
tooltip.setText(btcValue);
setText(satsValue);
}
setTooltip(tooltip);
}
}
}
private static class ActionCell extends TreeTableCell<Data, Void> {
private final HBox actionBox;
public ActionCell() {
super();
getStyleClass().add("action-cell");
actionBox = new HBox();
actionBox.setSpacing(8);
actionBox.setAlignment(Pos.CENTER);
Button receiveButton = new Button("");
Glyph receiveGlyph = new Glyph("FontAwesome", FontAwesome.Glyph.ARROW_DOWN);
receiveGlyph.setFontSize(12);
receiveButton.setGraphic(receiveGlyph);
receiveButton.setOnAction(event -> {
EventManager.get().post(new ReceiveActionEvent(getTreeTableView().getTreeItem(getIndex()).getValue().getWalletNode()));
});
actionBox.getChildren().add(receiveButton);
}
@Override
protected void updateItem(Void item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setGraphic(null);
} else {
setGraphic(actionBox);
}
}
}
}
@@ -16,7 +16,7 @@ import java.util.Locale;
public class CoinLabel extends CopyableLabel {
public static final int MAX_SATS_SHOWN = 1000000;
private DecimalFormat btcFormat = new DecimalFormat("0", DecimalFormatSymbols.getInstance(Locale.ENGLISH));
public static final DecimalFormat BTC_FORMAT = new DecimalFormat("0", DecimalFormatSymbols.getInstance(Locale.ENGLISH));
private final LongProperty value = new SimpleLongProperty();
private Tooltip tooltip;
@@ -28,7 +28,7 @@ public class CoinLabel extends CopyableLabel {
public CoinLabel(String text) {
super(text);
btcFormat.setMaximumFractionDigits(8);
BTC_FORMAT.setMaximumFractionDigits(8);
valueProperty().addListener((observable, oldValue, newValue) -> setValueAsText((Long)newValue));
tooltip = new Tooltip();
contextMenu = new CoinContextMenu();
@@ -51,7 +51,7 @@ public class CoinLabel extends CopyableLabel {
setContextMenu(contextMenu);
String satsValue = String.format(Locale.ENGLISH, "%,d",value) + " sats";
String btcValue = btcFormat.format(value.doubleValue() / Transaction.SATOSHIS_PER_BITCOIN) + " BTC";
String btcValue = BTC_FORMAT.format(value.doubleValue() / Transaction.SATOSHIS_PER_BITCOIN) + " BTC";
if(value > MAX_SATS_SHOWN) {
tooltip.setText(satsValue);
setText(btcValue);
@@ -78,7 +78,7 @@ public class CoinLabel extends CopyableLabel {
copyBtcValue.setOnAction(AE -> {
hide();
ClipboardContent content = new ClipboardContent();
content.putString(btcFormat.format((double)getValue() / Transaction.SATOSHIS_PER_BITCOIN));
content.putString(BTC_FORMAT.format((double)getValue() / Transaction.SATOSHIS_PER_BITCOIN));
Clipboard.getSystemClipboard().setContent(content);
});
@@ -2,10 +2,7 @@ package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.KeyDerivation;
import com.sparrowwallet.drongo.crypto.ChildNumber;
import com.sparrowwallet.drongo.wallet.Bip39MnemonicCode;
import com.sparrowwallet.drongo.wallet.DeterministicSeed;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.KeystoreImportEvent;
import com.sparrowwallet.sparrow.io.*;
@@ -244,7 +241,9 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
return true;
} catch (ImportException e) {
String errorMessage = e.getMessage();
if(e.getCause() != null && e.getCause().getMessage() != null && !e.getCause().getMessage().isEmpty()) {
if(e.getCause() instanceof MnemonicException.MnemonicChecksumException) {
errorMessage = "Invalid word list - checksum incorrect";
} else if(e.getCause() != null && e.getCause().getMessage() != null && !e.getCause().getMessage().isEmpty()) {
errorMessage = e.getCause().getMessage();
}
setError("Import Error", errorMessage);
@@ -0,0 +1,15 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.wallet.Wallet;
public class ReceiveActionEvent {
private Wallet.Node receiveNode;
public ReceiveActionEvent(Wallet.Node receiveNode) {
this.receiveNode = receiveNode;
}
public Wallet.Node getReceiveNode() {
return receiveNode;
}
}
@@ -0,0 +1,32 @@
package com.sparrowwallet.sparrow.wallet;
import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.control.AddressTreeTable;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import java.net.URL;
import java.util.ResourceBundle;
public class AddressesController extends WalletFormController implements Initializable {
@FXML
private AddressTreeTable receiveTable;
@FXML
private AddressTreeTable changeTable;
@Override
public void initialize(URL location, ResourceBundle resources) {
EventManager.get().register(this);
}
@Override
public void initializeView() {
Wallet wallet = walletForm.getWallet();
receiveTable.initialize(wallet.getNodes(KeyPurpose.RECEIVE));
changeTable.initialize(wallet.getNodes(KeyPurpose.CHANGE));
}
}
@@ -47,9 +47,9 @@ public class WalletController extends WalletFormController implements Initializa
for(Node walletFunction : walletPane.getChildren()) {
if(walletFunction.getUserData().equals(function)) {
existing = true;
walletFunction.setViewOrder(1);
} else {
walletFunction.setViewOrder(0);
} else {
walletFunction.setViewOrder(1);
}
}
@@ -57,6 +57,7 @@ public class WalletController extends WalletFormController implements Initializa
if(!existing) {
FXMLLoader functionLoader = new FXMLLoader(AppController.class.getResource("wallet/" + function.toString().toLowerCase() + ".fxml"));
Node walletFunction = functionLoader.load();
walletFunction.setUserData(function);
WalletFormController controller = functionLoader.getController();
controller.setWalletForm(getWalletForm());
walletFunction.setViewOrder(1);