mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2024-12-24 12:46:45 +00:00
show transaction diagram on every transaction headers screen
This commit is contained in:
parent
febd5c33a2
commit
f4810bb568
5 changed files with 386 additions and 105 deletions
2
drongo
2
drongo
|
@ -1 +1 @@
|
||||||
Subproject commit eb49c9713375ac5f909d8ec3fa4dfddaf7d63ffe
|
Subproject commit 99440eda7f8a2b5a8adec68d32a704634157a3d3
|
|
@ -3,16 +3,16 @@ package com.sparrowwallet.sparrow.control;
|
||||||
import com.sparrowwallet.drongo.KeyPurpose;
|
import com.sparrowwallet.drongo.KeyPurpose;
|
||||||
import com.sparrowwallet.drongo.address.Address;
|
import com.sparrowwallet.drongo.address.Address;
|
||||||
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||||
|
import com.sparrowwallet.drongo.protocol.TransactionOutput;
|
||||||
import com.sparrowwallet.drongo.uri.BitcoinURI;
|
import com.sparrowwallet.drongo.uri.BitcoinURI;
|
||||||
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
|
import com.sparrowwallet.drongo.wallet.*;
|
||||||
import com.sparrowwallet.drongo.wallet.Payment;
|
|
||||||
import com.sparrowwallet.drongo.wallet.WalletNode;
|
|
||||||
import com.sparrowwallet.drongo.wallet.WalletTransaction;
|
|
||||||
import com.sparrowwallet.sparrow.AppServices;
|
import com.sparrowwallet.sparrow.AppServices;
|
||||||
import com.sparrowwallet.sparrow.EventManager;
|
import com.sparrowwallet.sparrow.EventManager;
|
||||||
import com.sparrowwallet.sparrow.event.ExcludeUtxoEvent;
|
import com.sparrowwallet.sparrow.event.ExcludeUtxoEvent;
|
||||||
import com.sparrowwallet.sparrow.event.ReplaceChangeAddressEvent;
|
import com.sparrowwallet.sparrow.event.ReplaceChangeAddressEvent;
|
||||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||||
|
import javafx.beans.property.BooleanProperty;
|
||||||
|
import javafx.beans.property.SimpleBooleanProperty;
|
||||||
import javafx.geometry.Insets;
|
import javafx.geometry.Insets;
|
||||||
import javafx.geometry.Pos;
|
import javafx.geometry.Pos;
|
||||||
import javafx.scene.Group;
|
import javafx.scene.Group;
|
||||||
|
@ -38,6 +38,7 @@ public class TransactionDiagram extends GridPane {
|
||||||
private static final int TOOLTIP_SHOW_DELAY = 50;
|
private static final int TOOLTIP_SHOW_DELAY = 50;
|
||||||
|
|
||||||
private WalletTransaction walletTx;
|
private WalletTransaction walletTx;
|
||||||
|
private final BooleanProperty finalProperty = new SimpleBooleanProperty(false);
|
||||||
|
|
||||||
public void update(WalletTransaction walletTx) {
|
public void update(WalletTransaction walletTx) {
|
||||||
setMinHeight(getDiagramHeight());
|
setMinHeight(getDiagramHeight());
|
||||||
|
@ -105,7 +106,7 @@ public class TransactionDiagram extends GridPane {
|
||||||
private Map<BlockTransactionHashIndex, WalletNode> getDisplayedUtxos() {
|
private Map<BlockTransactionHashIndex, WalletNode> getDisplayedUtxos() {
|
||||||
Map<BlockTransactionHashIndex, WalletNode> selectedUtxos = walletTx.getSelectedUtxos();
|
Map<BlockTransactionHashIndex, WalletNode> selectedUtxos = walletTx.getSelectedUtxos();
|
||||||
|
|
||||||
if(getPayjoinURI() != null) {
|
if(getPayjoinURI() != null && !selectedUtxos.containsValue(null)) {
|
||||||
selectedUtxos = new LinkedHashMap<>(selectedUtxos);
|
selectedUtxos = new LinkedHashMap<>(selectedUtxos);
|
||||||
selectedUtxos.put(new PayjoinBlockTransactionHashIndex(), null);
|
selectedUtxos.put(new PayjoinBlockTransactionHashIndex(), null);
|
||||||
}
|
}
|
||||||
|
@ -228,18 +229,29 @@ public class TransactionDiagram extends GridPane {
|
||||||
label.getStyleClass().add("input-label");
|
label.getStyleClass().add("input-label");
|
||||||
}
|
}
|
||||||
|
|
||||||
label.setGraphic(excludeUtxoButton);
|
if(!isFinal()) {
|
||||||
label.setContentDisplay(ContentDisplay.LEFT);
|
label.setGraphic(excludeUtxoButton);
|
||||||
|
label.setContentDisplay(ContentDisplay.LEFT);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
if(input instanceof PayjoinBlockTransactionHashIndex) {
|
if(input instanceof PayjoinBlockTransactionHashIndex) {
|
||||||
tooltip.setText("Added once transaction is signed and sent to the payjoin server");
|
tooltip.setText("Added once transaction is signed and sent to the payjoin server");
|
||||||
} else {
|
} else if(input instanceof AdditionalBlockTransactionHashIndex additionalReference) {
|
||||||
AdditionalBlockTransactionHashIndex additionalReference = (AdditionalBlockTransactionHashIndex) input;
|
|
||||||
StringJoiner joiner = new StringJoiner("\n");
|
StringJoiner joiner = new StringJoiner("\n");
|
||||||
for(BlockTransactionHashIndex additionalInput : additionalReference.getAdditionalInputs()) {
|
for(BlockTransactionHashIndex additionalInput : additionalReference.getAdditionalInputs()) {
|
||||||
joiner.add(getInputDescription(additionalInput));
|
joiner.add(getInputDescription(additionalInput));
|
||||||
}
|
}
|
||||||
tooltip.setText(joiner.toString());
|
tooltip.setText(joiner.toString());
|
||||||
|
} else {
|
||||||
|
if(walletTx.getInputTransactions() != null && walletTx.getInputTransactions().get(input.getHash()) != null) {
|
||||||
|
BlockTransaction blockTransaction = walletTx.getInputTransactions().get(input.getHash());
|
||||||
|
TransactionOutput txOutput = blockTransaction.getTransaction().getOutputs().get((int)input.getIndex());
|
||||||
|
Address fromAddress = txOutput.getScript().getToAddress();
|
||||||
|
tooltip.setText("Input of " + getSatsValue(txOutput.getValue()) + " sats\n" + input.getHashAsString() + ":" + input.getIndex() + (fromAddress != null ? "\n" + fromAddress : ""));
|
||||||
|
} else {
|
||||||
|
tooltip.setText(input.getHashAsString() + ":" + input.getIndex());
|
||||||
|
}
|
||||||
|
label.getStyleClass().add("input-label");
|
||||||
}
|
}
|
||||||
tooltip.getStyleClass().add("input-label");
|
tooltip.getStyleClass().add("input-label");
|
||||||
}
|
}
|
||||||
|
@ -376,19 +388,22 @@ public class TransactionDiagram extends GridPane {
|
||||||
outputsBox.setAlignment(Pos.CENTER_LEFT);
|
outputsBox.setAlignment(Pos.CENTER_LEFT);
|
||||||
outputsBox.getChildren().add(createSpacer());
|
outputsBox.getChildren().add(createSpacer());
|
||||||
|
|
||||||
|
List<OutputNode> outputNodes = new ArrayList<>();
|
||||||
for(Payment payment : displayedPayments) {
|
for(Payment payment : displayedPayments) {
|
||||||
Glyph outputGlyph = getOutputGlyph(payment);
|
Glyph outputGlyph = getOutputGlyph(payment);
|
||||||
boolean labelledPayment = outputGlyph.getStyleClass().stream().anyMatch(style -> List.of("premix-icon", "badbank-icon", "whirlpoolfee-icon").contains(style)) || payment instanceof AdditionalPayment;
|
boolean labelledPayment = outputGlyph.getStyleClass().stream().anyMatch(style -> List.of("premix-icon", "badbank-icon", "whirlpoolfee-icon").contains(style)) || payment instanceof AdditionalPayment;
|
||||||
String recipientDesc = labelledPayment ? payment.getLabel() : payment.getAddress().toString().substring(0, 8) + "...";
|
payment.setLabel(getOutputLabel(payment));
|
||||||
Label recipientLabel = new Label(recipientDesc, outputGlyph);
|
Label recipientLabel = new Label(payment.getLabel() == null || payment.getType() == Payment.Type.FAKE_MIX ? payment.getAddress().toString().substring(0, 8) + "..." : payment.getLabel(), outputGlyph);
|
||||||
recipientLabel.getStyleClass().add("output-label");
|
recipientLabel.getStyleClass().add("output-label");
|
||||||
recipientLabel.getStyleClass().add(labelledPayment ? "payment-label" : "recipient-label");
|
recipientLabel.getStyleClass().add(labelledPayment ? "payment-label" : "recipient-label");
|
||||||
Tooltip recipientTooltip = new Tooltip((walletTx.isConsolidationSend(payment) ? "Consolidate " : "Pay ") + getSatsValue(payment.getAmount()) + " sats to " + (payment instanceof AdditionalPayment ? "\n" + payment : payment.getLabel() + "\n" + payment.getAddress().toString()));
|
Wallet toWallet = getToWallet(payment);
|
||||||
|
Tooltip recipientTooltip = new Tooltip((toWallet == null ? (walletTx.isConsolidationSend(payment) ? "Consolidate " : "Pay ") : "Receive ")
|
||||||
|
+ getSatsValue(payment.getAmount()) + " sats to "
|
||||||
|
+ (payment instanceof AdditionalPayment ? "\n" + payment : (toWallet == null ? (payment.getLabel() == null ? "external address" : payment.getLabel()) : toWallet.getName()) + "\n" + payment.getAddress().toString()));
|
||||||
recipientTooltip.getStyleClass().add("recipient-label");
|
recipientTooltip.getStyleClass().add("recipient-label");
|
||||||
recipientTooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY));
|
recipientTooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY));
|
||||||
recipientLabel.setTooltip(recipientTooltip);
|
recipientLabel.setTooltip(recipientTooltip);
|
||||||
outputsBox.getChildren().add(recipientLabel);
|
outputNodes.add(new OutputNode(recipientLabel, payment.getAddress()));
|
||||||
outputsBox.getChildren().add(createSpacer());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for(Map.Entry<WalletNode, Long> changeEntry : walletTx.getChangeMap().entrySet()) {
|
for(Map.Entry<WalletNode, Long> changeEntry : walletTx.getChangeMap().entrySet()) {
|
||||||
|
@ -397,29 +412,41 @@ public class TransactionDiagram extends GridPane {
|
||||||
boolean overGapLimit = (changeNode.getIndex() - defaultChangeNode.getIndex()) > walletTx.getWallet().getGapLimit();
|
boolean overGapLimit = (changeNode.getIndex() - defaultChangeNode.getIndex()) > walletTx.getWallet().getGapLimit();
|
||||||
|
|
||||||
HBox actionBox = new HBox();
|
HBox actionBox = new HBox();
|
||||||
String changeDesc = walletTx.getChangeAddress(changeNode).toString().substring(0, 8) + "...";
|
Address changeAddress = walletTx.getChangeAddress(changeNode);
|
||||||
|
String changeDesc = changeAddress.toString().substring(0, 8) + "...";
|
||||||
Label changeLabel = new Label(changeDesc, overGapLimit ? getChangeWarningGlyph() : getChangeGlyph());
|
Label changeLabel = new Label(changeDesc, overGapLimit ? getChangeWarningGlyph() : getChangeGlyph());
|
||||||
changeLabel.getStyleClass().addAll("output-label", "change-label");
|
changeLabel.getStyleClass().addAll("output-label", "change-label");
|
||||||
Tooltip changeTooltip = new Tooltip("Change of " + getSatsValue(changeEntry.getValue()) + " sats to " + changeNode.getDerivationPath().replace("m", "..") + "\n" + walletTx.getChangeAddress(changeNode).toString() + (overGapLimit ? "\nAddress is beyond the gap limit!" : ""));
|
Tooltip changeTooltip = new Tooltip("Change of " + getSatsValue(changeEntry.getValue()) + " sats to " + changeNode.getDerivationPath().replace("m", "..") + "\n" + walletTx.getChangeAddress(changeNode).toString() + (overGapLimit ? "\nAddress is beyond the gap limit!" : ""));
|
||||||
changeTooltip.getStyleClass().add("change-label");
|
changeTooltip.getStyleClass().add("change-label");
|
||||||
changeTooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY));
|
changeTooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY));
|
||||||
changeLabel.setTooltip(changeTooltip);
|
changeLabel.setTooltip(changeTooltip);
|
||||||
|
actionBox.getChildren().add(changeLabel);
|
||||||
|
|
||||||
Button nextChangeAddressButton = new Button("");
|
if(!isFinal()) {
|
||||||
nextChangeAddressButton.setGraphic(getChangeReplaceGlyph());
|
Button nextChangeAddressButton = new Button("");
|
||||||
nextChangeAddressButton.setOnAction(event -> {
|
nextChangeAddressButton.setGraphic(getChangeReplaceGlyph());
|
||||||
EventManager.get().post(new ReplaceChangeAddressEvent(walletTx));
|
nextChangeAddressButton.setOnAction(event -> {
|
||||||
});
|
EventManager.get().post(new ReplaceChangeAddressEvent(walletTx));
|
||||||
Tooltip replaceChangeTooltip = new Tooltip("Use next change address");
|
});
|
||||||
nextChangeAddressButton.setTooltip(replaceChangeTooltip);
|
Tooltip replaceChangeTooltip = new Tooltip("Use next change address");
|
||||||
Label replaceChangeLabel = new Label("", nextChangeAddressButton);
|
nextChangeAddressButton.setTooltip(replaceChangeTooltip);
|
||||||
replaceChangeLabel.getStyleClass().add("replace-change-label");
|
Label replaceChangeLabel = new Label("", nextChangeAddressButton);
|
||||||
replaceChangeLabel.setVisible(false);
|
replaceChangeLabel.getStyleClass().add("replace-change-label");
|
||||||
actionBox.setOnMouseEntered(event -> replaceChangeLabel.setVisible(true));
|
replaceChangeLabel.setVisible(false);
|
||||||
actionBox.setOnMouseExited(event -> replaceChangeLabel.setVisible(false));
|
actionBox.setOnMouseEntered(event -> replaceChangeLabel.setVisible(true));
|
||||||
|
actionBox.setOnMouseExited(event -> replaceChangeLabel.setVisible(false));
|
||||||
|
actionBox.getChildren().add(replaceChangeLabel);
|
||||||
|
}
|
||||||
|
|
||||||
actionBox.getChildren().addAll(changeLabel, replaceChangeLabel);
|
outputNodes.add(new OutputNode(actionBox, changeAddress));
|
||||||
outputsBox.getChildren().add(actionBox);
|
}
|
||||||
|
|
||||||
|
if(isFinal()) {
|
||||||
|
Collections.sort(outputNodes);
|
||||||
|
}
|
||||||
|
|
||||||
|
for(OutputNode outputNode : outputNodes) {
|
||||||
|
outputsBox.getChildren().add(outputNode.outputLabel);
|
||||||
outputsBox.getChildren().add(createSpacer());
|
outputsBox.getChildren().add(createSpacer());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -427,7 +454,7 @@ public class TransactionDiagram extends GridPane {
|
||||||
Label feeLabel = highFee ? new Label("High Fee", getWarningGlyph()) : new Label("Fee", getFeeGlyph());
|
Label feeLabel = highFee ? new Label("High Fee", getWarningGlyph()) : new Label("Fee", getFeeGlyph());
|
||||||
feeLabel.getStyleClass().addAll("output-label", "fee-label");
|
feeLabel.getStyleClass().addAll("output-label", "fee-label");
|
||||||
String percentage = String.format("%.2f", walletTx.getFeePercentage() * 100.0);
|
String percentage = String.format("%.2f", walletTx.getFeePercentage() * 100.0);
|
||||||
Tooltip feeTooltip = new Tooltip("Fee of " + getSatsValue(walletTx.getFee()) + " sats (" + percentage + "%)");
|
Tooltip feeTooltip = new Tooltip(walletTx.getFee() < 0 ? "Unknown fee" : "Fee of " + getSatsValue(walletTx.getFee()) + " sats (" + percentage + "%)");
|
||||||
feeTooltip.getStyleClass().add("fee-tooltip");
|
feeTooltip.getStyleClass().add("fee-tooltip");
|
||||||
feeTooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY));
|
feeTooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY));
|
||||||
feeLabel.setTooltip(feeTooltip);
|
feeLabel.setTooltip(feeTooltip);
|
||||||
|
@ -445,7 +472,10 @@ public class TransactionDiagram extends GridPane {
|
||||||
|
|
||||||
String txDesc = "Transaction";
|
String txDesc = "Transaction";
|
||||||
Label txLabel = new Label(txDesc);
|
Label txLabel = new Label(txDesc);
|
||||||
Tooltip tooltip = new Tooltip(walletTx.getTransaction().getLength() + " bytes\n" + String.format("%.2f", walletTx.getTransaction().getVirtualSize()) + " vBytes");
|
boolean isFinalized = walletTx.getTransaction().hasScriptSigs() || walletTx.getTransaction().hasWitnesses();
|
||||||
|
Tooltip tooltip = new Tooltip(walletTx.getTransaction().getLength() + " bytes\n"
|
||||||
|
+ String.format("%.2f", walletTx.getTransaction().getVirtualSize()) + " vBytes"
|
||||||
|
+ (walletTx.getFee() < 0 ? "" : "\n" + String.format("%.2f", walletTx.getFee() / walletTx.getTransaction().getVirtualSize()) + " sats/vB" + (isFinalized ? "" : " (non-final)")));
|
||||||
tooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY));
|
tooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY));
|
||||||
tooltip.getStyleClass().add("transaction-tooltip");
|
tooltip.getStyleClass().add("transaction-tooltip");
|
||||||
txLabel.setTooltip(tooltip);
|
txLabel.setTooltip(tooltip);
|
||||||
|
@ -469,6 +499,37 @@ public class TransactionDiagram extends GridPane {
|
||||||
return spacer;
|
return spacer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String getOutputLabel(Payment payment) {
|
||||||
|
if(payment.getLabel() != null) {
|
||||||
|
return payment.getLabel();
|
||||||
|
}
|
||||||
|
|
||||||
|
if(payment.getType() == Payment.Type.WHIRLPOOL_FEE) {
|
||||||
|
return "Whirlpool Fee";
|
||||||
|
} else if(walletTx.isPremixSend(payment)) {
|
||||||
|
int premixIndex = getOutputIndex(payment.getAddress()) - 2;
|
||||||
|
return "Premix #" + premixIndex;
|
||||||
|
} else if(walletTx.isBadbankSend(payment)) {
|
||||||
|
return "Badbank Change";
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getOutputIndex(Address address) {
|
||||||
|
return walletTx.getTransaction().getOutputs().stream().filter(txOutput -> address.equals(txOutput.getScript().getToAddress())).mapToInt(TransactionOutput::getIndex).findFirst().orElseThrow();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Wallet getToWallet(Payment payment) {
|
||||||
|
for(Wallet openWallet : AppServices.get().getOpenWallets().keySet()) {
|
||||||
|
if(openWallet != walletTx.getWallet() && openWallet.isWalletAddress(payment.getAddress())) {
|
||||||
|
return openWallet;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
public Glyph getOutputGlyph(Payment payment) {
|
public Glyph getOutputGlyph(Payment payment) {
|
||||||
if(payment.getType().equals(Payment.Type.FAKE_MIX)) {
|
if(payment.getType().equals(Payment.Type.FAKE_MIX)) {
|
||||||
return getFakeMixGlyph();
|
return getFakeMixGlyph();
|
||||||
|
@ -482,6 +543,8 @@ public class TransactionDiagram extends GridPane {
|
||||||
return getWhirlpoolFeeGlyph();
|
return getWhirlpoolFeeGlyph();
|
||||||
} else if(payment instanceof AdditionalPayment) {
|
} else if(payment instanceof AdditionalPayment) {
|
||||||
return ((AdditionalPayment)payment).getOutputGlyph(this);
|
return ((AdditionalPayment)payment).getOutputGlyph(this);
|
||||||
|
} else if(getToWallet(payment) != null) {
|
||||||
|
return getDepositGlyph();
|
||||||
}
|
}
|
||||||
|
|
||||||
return getPaymentGlyph();
|
return getPaymentGlyph();
|
||||||
|
@ -508,6 +571,13 @@ public class TransactionDiagram extends GridPane {
|
||||||
return consolidationGlyph;
|
return consolidationGlyph;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Glyph getDepositGlyph() {
|
||||||
|
Glyph depositGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.ARROW_DOWN);
|
||||||
|
depositGlyph.getStyleClass().add("deposit-icon");
|
||||||
|
depositGlyph.setFontSize(12);
|
||||||
|
return depositGlyph;
|
||||||
|
}
|
||||||
|
|
||||||
public static Glyph getPremixGlyph() {
|
public static Glyph getPremixGlyph() {
|
||||||
Glyph premixGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.RANDOM);
|
Glyph premixGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.RANDOM);
|
||||||
premixGlyph.getStyleClass().add("premix-icon");
|
premixGlyph.getStyleClass().add("premix-icon");
|
||||||
|
@ -589,6 +659,18 @@ public class TransactionDiagram extends GridPane {
|
||||||
return lockGlyph;
|
return lockGlyph;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isFinal() {
|
||||||
|
return finalProperty.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public BooleanProperty finalProperty() {
|
||||||
|
return finalProperty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFinal(boolean isFinal) {
|
||||||
|
this.finalProperty.set(isFinal);
|
||||||
|
}
|
||||||
|
|
||||||
private static class PayjoinBlockTransactionHashIndex extends BlockTransactionHashIndex {
|
private static class PayjoinBlockTransactionHashIndex extends BlockTransactionHashIndex {
|
||||||
public PayjoinBlockTransactionHashIndex() {
|
public PayjoinBlockTransactionHashIndex() {
|
||||||
super(Sha256Hash.ZERO_HASH, 0, new Date(), 0L, 0, 0);
|
super(Sha256Hash.ZERO_HASH, 0, new Date(), 0L, 0, 0);
|
||||||
|
@ -644,4 +726,23 @@ public class TransactionDiagram extends GridPane {
|
||||||
return additionalPayments.stream().map(payment -> payment.getAddress().toString()).collect(Collectors.joining("\n"));
|
return additionalPayments.stream().map(payment -> payment.getAddress().toString()).collect(Collectors.joining("\n"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class OutputNode implements Comparable<OutputNode> {
|
||||||
|
public Node outputLabel;
|
||||||
|
public Address address;
|
||||||
|
|
||||||
|
public OutputNode(Node outputLabel, Address address) {
|
||||||
|
this.outputLabel = outputLabel;
|
||||||
|
this.address = address;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int compareTo(TransactionDiagram.OutputNode o) {
|
||||||
|
try {
|
||||||
|
return getOutputIndex(address) - getOutputIndex(o.address);
|
||||||
|
} catch(Exception e) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -66,6 +66,9 @@ public class HeadersController extends TransactionFormController implements Init
|
||||||
@FXML
|
@FXML
|
||||||
private IdLabel id;
|
private IdLabel id;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private TransactionDiagram transactionDiagram;
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
private Spinner<Integer> version;
|
private Spinner<Integer> version;
|
||||||
|
|
||||||
|
@ -347,6 +350,8 @@ public class HeadersController extends TransactionFormController implements Init
|
||||||
updateFee(feeAmt);
|
updateFee(feeAmt);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
transactionDiagram.update(getWalletTransaction(headersForm.getInputTransactions()));
|
||||||
|
|
||||||
blockchainForm.managedProperty().bind(blockchainForm.visibleProperty());
|
blockchainForm.managedProperty().bind(blockchainForm.visibleProperty());
|
||||||
|
|
||||||
signingWalletForm.managedProperty().bind(signingWalletForm.visibleProperty());
|
signingWalletForm.managedProperty().bind(signingWalletForm.visibleProperty());
|
||||||
|
@ -489,6 +494,94 @@ public class HeadersController extends TransactionFormController implements Init
|
||||||
feeRate.setText(String.format("%.2f", feeRateAmt) + " sats/vB" + (headersForm.isTransactionFinalized() ? "" : " (non-final)"));
|
feeRate.setText(String.format("%.2f", feeRateAmt) + " sats/vB" + (headersForm.isTransactionFinalized() ? "" : " (non-final)"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private WalletTransaction getWalletTransaction(Map<Sha256Hash, BlockTransaction> inputTransactions) {
|
||||||
|
Wallet wallet = getWalletFromTransactionInputs();
|
||||||
|
|
||||||
|
if(wallet != null) {
|
||||||
|
Map<BlockTransactionHashIndex, WalletNode> selectedTxos = new LinkedHashMap<>();
|
||||||
|
Map<BlockTransactionHashIndex, WalletNode> walletTxos = wallet.getWalletTxos();
|
||||||
|
for(TransactionInput txInput : headersForm.getTransaction().getInputs()) {
|
||||||
|
BlockTransactionHashIndex selectedTxo = walletTxos.keySet().stream().filter(txo -> txInput.getOutpoint().getHash().equals(txo.getHash()) && txInput.getOutpoint().getIndex() == txo.getIndex())
|
||||||
|
.findFirst().orElse(new BlockTransactionHashIndex(txInput.getOutpoint().getHash(), 0, null, null, txInput.getOutpoint().getIndex(), 0));
|
||||||
|
selectedTxos.put(selectedTxo, walletTxos.get(selectedTxo));
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Payment> payments = new ArrayList<>();
|
||||||
|
Map<WalletNode, Long> changeMap = new LinkedHashMap<>();
|
||||||
|
Map<Script, WalletNode> changeOutputScripts = wallet.getWalletOutputScripts(KeyPurpose.CHANGE);
|
||||||
|
for(TransactionOutput txOutput : headersForm.getTransaction().getOutputs()) {
|
||||||
|
WalletNode changeNode = changeOutputScripts.get(txOutput.getScript());
|
||||||
|
if(changeNode != null) {
|
||||||
|
if(headersForm.getTransaction().getOutputs().size() == 4 && headersForm.getTransaction().getOutputs().stream().anyMatch(txo -> txo != txOutput && txo.getValue() == txOutput.getValue())) {
|
||||||
|
try {
|
||||||
|
payments.add(new Payment(txOutput.getScript().getToAddresses()[0], ".." + changeNode + " (Fake Mix)", txOutput.getValue(), false, Payment.Type.FAKE_MIX));
|
||||||
|
} catch(Exception e) {
|
||||||
|
//ignore
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
changeMap.put(changeNode, txOutput.getValue());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Payment.Type paymentType = Payment.Type.DEFAULT;
|
||||||
|
Wallet masterWallet = wallet.isMasterWallet() ? wallet : wallet.getMasterWallet();
|
||||||
|
Wallet premixWallet = masterWallet.getChildWallet(StandardAccount.WHIRLPOOL_PREMIX);
|
||||||
|
if(premixWallet != null && headersForm.getTransaction().getOutputs().stream().anyMatch(premixWallet::isWalletTxo) && txOutput.getIndex() == 1) {
|
||||||
|
paymentType = Payment.Type.WHIRLPOOL_FEE;
|
||||||
|
}
|
||||||
|
|
||||||
|
BlockTransactionHashIndex receivedTxo = walletTxos.keySet().stream().filter(txo -> txo.getHash().equals(txOutput.getHash()) && txo.getIndex() == txOutput.getIndex()).findFirst().orElse(null);
|
||||||
|
String label = headersForm.getName() == null || (headersForm.getName().startsWith("[") && headersForm.getName().endsWith("]") && headersForm.getName().length() == 8) ? null : headersForm.getName();
|
||||||
|
try {
|
||||||
|
payments.add(new Payment(txOutput.getScript().getToAddresses()[0], receivedTxo != null ? receivedTxo.getLabel() : label, txOutput.getValue(), false, paymentType));
|
||||||
|
} catch(Exception e) {
|
||||||
|
//ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new WalletTransaction(wallet, headersForm.getTransaction(), Collections.emptyList(), selectedTxos, payments, changeMap, fee.getValue(), inputTransactions);
|
||||||
|
} else {
|
||||||
|
Map<BlockTransactionHashIndex, WalletNode> selectedTxos = headersForm.getTransaction().getInputs().stream()
|
||||||
|
.collect(Collectors.toMap(txInput -> {
|
||||||
|
if(inputTransactions != null) {
|
||||||
|
BlockTransaction blockTransaction = inputTransactions.get(txInput.getOutpoint().getHash());
|
||||||
|
if(blockTransaction != null) {
|
||||||
|
TransactionOutput txOutput = blockTransaction.getTransaction().getOutputs().get((int)txInput.getOutpoint().getIndex());
|
||||||
|
return new BlockTransactionHashIndex(blockTransaction.getHash(), blockTransaction.getHeight(), blockTransaction.getDate(), blockTransaction.getFee(), txInput.getOutpoint().getIndex(), txOutput.getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new BlockTransactionHashIndex(txInput.getOutpoint().getHash(), 0, null, null, txInput.getOutpoint().getIndex(), 0);
|
||||||
|
},
|
||||||
|
txInput -> new WalletNode("m/0"),
|
||||||
|
(u, v) -> { throw new IllegalStateException("Duplicate TXOs"); },
|
||||||
|
LinkedHashMap::new));
|
||||||
|
selectedTxos.entrySet().forEach(entry -> entry.setValue(null));
|
||||||
|
|
||||||
|
List<Payment> payments = new ArrayList<>();
|
||||||
|
for(TransactionOutput txOutput : headersForm.getTransaction().getOutputs()) {
|
||||||
|
try {
|
||||||
|
payments.add(new Payment(txOutput.getScript().getToAddresses()[0], null, txOutput.getValue(), false));
|
||||||
|
} catch(Exception e) {
|
||||||
|
//ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new WalletTransaction(null, headersForm.getTransaction(), Collections.emptyList(), selectedTxos, payments, Collections.emptyMap(), fee.getValue(), inputTransactions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Wallet getWalletFromTransactionInputs() {
|
||||||
|
for(TransactionInput txInput : headersForm.getTransaction().getInputs()) {
|
||||||
|
for(Wallet openWallet : AppServices.get().getOpenWallets().keySet()) {
|
||||||
|
if(openWallet.isWalletTxo(txInput)) {
|
||||||
|
return openWallet;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
private void updateBlockchainForm(BlockTransaction blockTransaction, Integer currentHeight) {
|
private void updateBlockchainForm(BlockTransaction blockTransaction, Integer currentHeight) {
|
||||||
signaturesForm.setVisible(false);
|
signaturesForm.setVisible(false);
|
||||||
blockchainForm.setVisible(true);
|
blockchainForm.setVisible(true);
|
||||||
|
@ -986,6 +1079,7 @@ public class HeadersController extends TransactionFormController implements Init
|
||||||
if(feeAmt != null) {
|
if(feeAmt != null) {
|
||||||
updateFee(feeAmt);
|
updateFee(feeAmt);
|
||||||
}
|
}
|
||||||
|
transactionDiagram.update(getWalletTransaction(event.getInputTransactions()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1120,6 +1214,7 @@ public class HeadersController extends TransactionFormController implements Init
|
||||||
updateType();
|
updateType();
|
||||||
updateSize();
|
updateSize();
|
||||||
updateFee(headersForm.getPsbt().getFee());
|
updateFee(headersForm.getPsbt().getFee());
|
||||||
|
transactionDiagram.update(getWalletTransaction(headersForm.getInputTransactions()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -24,6 +24,67 @@
|
||||||
-fx-padding: 0 0 0 12;
|
-fx-padding: 0 0 0 12;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.headers-tabs > .tab-header-area .tab {
|
||||||
|
-fx-pref-height: 50;
|
||||||
|
-fx-pref-width: 90;
|
||||||
|
-fx-alignment: CENTER;
|
||||||
|
}
|
||||||
|
|
||||||
|
.headers-tabs > .tab-header-area .tab-label {
|
||||||
|
-fx-pref-height: 50;
|
||||||
|
-fx-pref-width: 90;
|
||||||
|
-fx-alignment: CENTER;
|
||||||
|
-fx-translate-x: -6;
|
||||||
|
}
|
||||||
|
|
||||||
|
#transactionDiagram .boundary {
|
||||||
|
-fx-stroke: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
#transactionDiagram .input-label, #transactionDiagram .recipient-label, #transactionDiagram .change-label, #transactionDiagram .fee-tooltip, #transactionDiagram .transaction-tooltip {
|
||||||
|
-fx-font-size: 13px;
|
||||||
|
-fx-font-family: 'Roboto Mono';
|
||||||
|
}
|
||||||
|
|
||||||
|
#transactionDiagram .fee-warning-icon {
|
||||||
|
-fx-text-fill: rgb(202, 18, 67);
|
||||||
|
}
|
||||||
|
|
||||||
|
#transactionDiagram .inputs-type, #transactionDiagram .input-line, #transactionDiagram .output-line {
|
||||||
|
-fx-fill: transparent;
|
||||||
|
-fx-text-fill: #696c77;
|
||||||
|
-fx-stroke: #696c77;
|
||||||
|
-fx-stroke-width: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#transactionDiagram .input-dashed-line {
|
||||||
|
-fx-stroke-dash-array: 5px 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#transactionDiagram .utxo-label .button, #transactionDiagram .replace-change-label .button {
|
||||||
|
-fx-padding: 0;
|
||||||
|
-fx-pref-height: 18;
|
||||||
|
-fx-pref-width: 18;
|
||||||
|
-fx-border-width: 0;
|
||||||
|
-fx-background-color: -fx-background;
|
||||||
|
}
|
||||||
|
|
||||||
|
#transactionDiagram .utxo-label .button .label .text {
|
||||||
|
-fx-fill: -fx-background;
|
||||||
|
}
|
||||||
|
|
||||||
|
#transactionDiagram .utxo-label:hover .button .label .text {
|
||||||
|
-fx-fill: -fx-text-base-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
#transactionDiagram .change-warning-icon {
|
||||||
|
-fx-text-fill: rgb(238, 210, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.details-lower .fieldset {
|
||||||
|
-fx-padding: 0 0 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
.future-warning {
|
.future-warning {
|
||||||
-fx-text-fill: rgb(238, 210, 2);
|
-fx-text-fill: rgb(238, 210, 2);
|
||||||
-fx-padding: 0 0 0 12;
|
-fx-padding: 0 0 0 12;
|
||||||
|
|
|
@ -25,6 +25,9 @@
|
||||||
<?import javafx.scene.control.ProgressBar?>
|
<?import javafx.scene.control.ProgressBar?>
|
||||||
<?import javafx.scene.control.Tooltip?>
|
<?import javafx.scene.control.Tooltip?>
|
||||||
<?import com.sparrowwallet.sparrow.control.DynamicForm?>
|
<?import com.sparrowwallet.sparrow.control.DynamicForm?>
|
||||||
|
<?import javafx.scene.control.TabPane?>
|
||||||
|
<?import javafx.scene.control.Tab?>
|
||||||
|
<?import com.sparrowwallet.sparrow.control.TransactionDiagram?>
|
||||||
|
|
||||||
<GridPane hgap="10.0" vgap="10.0" styleClass="tx-pane" xmlns="http://javafx.com/javafx/10.0.2-internal" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.sparrowwallet.sparrow.transaction.HeadersController" stylesheets="@headers.css, @transaction.css, @../general.css">
|
<GridPane hgap="10.0" vgap="10.0" styleClass="tx-pane" xmlns="http://javafx.com/javafx/10.0.2-internal" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.sparrowwallet.sparrow.transaction.HeadersController" stylesheets="@headers.css, @transaction.css, @../general.css">
|
||||||
<padding>
|
<padding>
|
||||||
|
@ -52,83 +55,104 @@
|
||||||
|
|
||||||
<Separator GridPane.columnIndex="0" GridPane.rowIndex="1" GridPane.columnSpan="2" styleClass="form-separator"/>
|
<Separator GridPane.columnIndex="0" GridPane.rowIndex="1" GridPane.columnSpan="2" styleClass="form-separator"/>
|
||||||
|
|
||||||
<Form GridPane.columnIndex="0" GridPane.rowIndex="2">
|
<TabPane side="RIGHT" GridPane.columnIndex="0" GridPane.rowIndex="2" GridPane.columnSpan="2" styleClass="headers-tabs">
|
||||||
<Fieldset text="Headers" inputGrow="SOMETIMES">
|
<Tab text="Overview" closable="false">
|
||||||
<Field text="Version:">
|
<TransactionDiagram fx:id="transactionDiagram" maxWidth="700" final="true"/>
|
||||||
<Spinner fx:id="version" prefWidth="60" editable="true" />
|
</Tab>
|
||||||
</Field>
|
<Tab text="Detail" closable="false">
|
||||||
<Field text="Type:">
|
<GridPane hgap="10.0" vgap="10.0">
|
||||||
<CopyableLabel fx:id="segwit" />
|
<columnConstraints>
|
||||||
</Field>
|
<ColumnConstraints percentWidth="50" />
|
||||||
</Fieldset>
|
<ColumnConstraints percentWidth="50" />
|
||||||
</Form>
|
</columnConstraints>
|
||||||
|
<rowConstraints>
|
||||||
|
<RowConstraints />
|
||||||
|
</rowConstraints>
|
||||||
|
<Form GridPane.columnIndex="0" GridPane.rowIndex="0">
|
||||||
|
<Fieldset text="Headers" inputGrow="SOMETIMES">
|
||||||
|
<Field text="Version:">
|
||||||
|
<Spinner fx:id="version" prefWidth="60" editable="true" />
|
||||||
|
</Field>
|
||||||
|
<Field text="Type:">
|
||||||
|
<CopyableLabel fx:id="segwit" />
|
||||||
|
</Field>
|
||||||
|
</Fieldset>
|
||||||
|
</Form>
|
||||||
|
|
||||||
<Form GridPane.columnIndex="1" GridPane.rowIndex="2">
|
<Form GridPane.columnIndex="1" GridPane.rowIndex="0">
|
||||||
<Fieldset fx:id="locktimeFieldset" text="Absolute Locktime" inputGrow="SOMETIMES">
|
<Fieldset fx:id="locktimeFieldset" text="Absolute Locktime" inputGrow="SOMETIMES">
|
||||||
<Field text="Type:">
|
<Field text="Type:">
|
||||||
<SegmentedButton>
|
<SegmentedButton>
|
||||||
<toggleGroup>
|
<toggleGroup>
|
||||||
<ToggleGroup fx:id="locktimeToggleGroup" />
|
<ToggleGroup fx:id="locktimeToggleGroup" />
|
||||||
</toggleGroup>
|
</toggleGroup>
|
||||||
<buttons>
|
<buttons>
|
||||||
<ToggleButton fx:id="locktimeNoneType" text="Disabled" userData="none" toggleGroup="$locktimeToggleGroup" />
|
<ToggleButton fx:id="locktimeNoneType" text="Disabled" userData="none" toggleGroup="$locktimeToggleGroup" />
|
||||||
<ToggleButton fx:id="locktimeBlockType" text="Block" userData="block" toggleGroup="$locktimeToggleGroup" />
|
<ToggleButton fx:id="locktimeBlockType" text="Block" userData="block" toggleGroup="$locktimeToggleGroup" />
|
||||||
<ToggleButton fx:id="locktimeDateType" text="Date" userData="date" toggleGroup="$locktimeToggleGroup" />
|
<ToggleButton fx:id="locktimeDateType" text="Date" userData="date" toggleGroup="$locktimeToggleGroup" />
|
||||||
</buttons>
|
</buttons>
|
||||||
</SegmentedButton>
|
</SegmentedButton>
|
||||||
</Field>
|
</Field>
|
||||||
<Field fx:id="locktimeNoneField" text="Block:">
|
<Field fx:id="locktimeNoneField" text="Block:">
|
||||||
<Spinner fx:id="locktimeNone" disable="true" prefWidth="120"/>
|
<Spinner fx:id="locktimeNone" disable="true" prefWidth="120"/>
|
||||||
</Field>
|
</Field>
|
||||||
<Field fx:id="locktimeBlockField" text="Block:">
|
<Field fx:id="locktimeBlockField" text="Block:">
|
||||||
<Spinner fx:id="locktimeBlock" editable="true" prefWidth="120"/>
|
<Spinner fx:id="locktimeBlock" editable="true" prefWidth="120"/>
|
||||||
<Hyperlink fx:id="locktimeCurrentHeight" text="Set current height" onAction="#setLocktimeToCurrentHeight" />
|
<Hyperlink fx:id="locktimeCurrentHeight" text="Set current height" onAction="#setLocktimeToCurrentHeight" />
|
||||||
<Label fx:id="futureBlockWarning">
|
<Label fx:id="futureBlockWarning">
|
||||||
<graphic>
|
<graphic>
|
||||||
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="12" icon="EXCLAMATION_TRIANGLE" styleClass="future-warning" />
|
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="12" icon="EXCLAMATION_TRIANGLE" styleClass="future-warning" />
|
||||||
</graphic>
|
</graphic>
|
||||||
<tooltip>
|
<tooltip>
|
||||||
<Tooltip text="Future block specified - transaction cannot be broadcast until this block height"/>
|
<Tooltip text="Future block specified - transaction cannot be broadcast until this block height"/>
|
||||||
</tooltip>
|
</tooltip>
|
||||||
</Label>
|
</Label>
|
||||||
</Field>
|
</Field>
|
||||||
<Field fx:id="locktimeDateField" text="Date:">
|
<Field fx:id="locktimeDateField" text="Date:">
|
||||||
<DateTimePicker fx:id="locktimeDate" prefWidth="180"/>
|
<DateTimePicker fx:id="locktimeDate" prefWidth="180"/>
|
||||||
<Label fx:id="futureDateWarning">
|
<Label fx:id="futureDateWarning">
|
||||||
<graphic>
|
<graphic>
|
||||||
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="12" icon="EXCLAMATION_TRIANGLE" styleClass="future-warning" />
|
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="12" icon="EXCLAMATION_TRIANGLE" styleClass="future-warning" />
|
||||||
</graphic>
|
</graphic>
|
||||||
<tooltip>
|
<tooltip>
|
||||||
<Tooltip text="Future date specified - transaction cannot be broadcast until this date"/>
|
<Tooltip text="Future date specified - transaction cannot be broadcast until this date"/>
|
||||||
</tooltip>
|
</tooltip>
|
||||||
</Label>
|
</Label>
|
||||||
</Field>
|
</Field>
|
||||||
</Fieldset>
|
</Fieldset>
|
||||||
</Form>
|
</Form>
|
||||||
|
|
||||||
<Separator GridPane.columnIndex="0" GridPane.rowIndex="3" GridPane.columnSpan="2" styleClass="form-separator"/>
|
<HBox GridPane.columnIndex="0" GridPane.rowIndex="1" GridPane.columnSpan="2">
|
||||||
|
<padding>
|
||||||
|
<Insets right="40"/>
|
||||||
|
</padding>
|
||||||
|
<Separator styleClass="form-separator" HBox.hgrow="ALWAYS"/>
|
||||||
|
</HBox>
|
||||||
|
|
||||||
<Form GridPane.columnIndex="0" GridPane.rowIndex="4">
|
<Form GridPane.columnIndex="0" GridPane.rowIndex="2" styleClass="details-lower">
|
||||||
<Fieldset text="Size" inputGrow="SOMETIMES">
|
<Fieldset text="Size" inputGrow="SOMETIMES">
|
||||||
<Field text="Bytes:">
|
<Field text="Bytes:">
|
||||||
<CopyableLabel fx:id="size" />
|
<CopyableLabel fx:id="size" />
|
||||||
</Field>
|
</Field>
|
||||||
<Field text="vBytes:">
|
<Field text="vBytes:">
|
||||||
<CopyableLabel fx:id="virtualSize" />
|
<CopyableLabel fx:id="virtualSize" />
|
||||||
</Field>
|
</Field>
|
||||||
</Fieldset>
|
</Fieldset>
|
||||||
</Form>
|
</Form>
|
||||||
|
|
||||||
<Form GridPane.columnIndex="1" GridPane.rowIndex="4">
|
<Form GridPane.columnIndex="1" GridPane.rowIndex="2" styleClass="details-lower">
|
||||||
<Fieldset text="Fee" inputGrow="SOMETIMES">
|
<Fieldset text="Fee" inputGrow="SOMETIMES">
|
||||||
<Field text="Amount:">
|
<Field text="Amount:">
|
||||||
<CoinLabel fx:id="fee" />
|
<CoinLabel fx:id="fee" />
|
||||||
</Field>
|
</Field>
|
||||||
<Field text="Rate:">
|
<Field text="Rate:">
|
||||||
<CopyableLabel fx:id="feeRate" />
|
<CopyableLabel fx:id="feeRate" />
|
||||||
</Field>
|
</Field>
|
||||||
</Fieldset>
|
</Fieldset>
|
||||||
</Form>
|
</Form>
|
||||||
|
</GridPane>
|
||||||
|
</Tab>
|
||||||
|
</TabPane>
|
||||||
|
|
||||||
<Separator GridPane.columnIndex="0" GridPane.rowIndex="5" GridPane.columnSpan="2" styleClass="form-separator"/>
|
<Separator GridPane.columnIndex="0" GridPane.rowIndex="5" GridPane.columnSpan="2" styleClass="form-separator"/>
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue