mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2026-08-10 16:43:14 +00:00
format display of addresses in 4 character chunks
This commit is contained in:
@@ -22,6 +22,7 @@ public class AddressLabel extends IdLabel {
|
||||
|
||||
public AddressLabel(String text) {
|
||||
super(text);
|
||||
setSkin(new AddressTextFieldSkin(this));
|
||||
addressProperty().addListener((observable, oldValue, newValue) -> {
|
||||
setAddressAsText(newValue);
|
||||
contextMenu.copyHex.setText("Copy " + newValue.getOutputScriptDataType());
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.Network;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.ContentDisplay;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.skin.LabelSkin;
|
||||
import javafx.scene.text.Font;
|
||||
import javafx.scene.text.Text;
|
||||
import javafx.scene.text.TextFlow;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class AddressLabelSkin extends LabelSkin {
|
||||
public static final int CHUNK_SIZE = 4;
|
||||
public static final Pattern CHUNK_PATTERN = Pattern.compile("(?<=\\G.{" + CHUNK_SIZE + "})");
|
||||
|
||||
private final TextFlow displayFlow;
|
||||
private final ChangeListener<String> textListener;
|
||||
private final ChangeListener<Font> fontListener;
|
||||
|
||||
public AddressLabelSkin(Label control) {
|
||||
super(control);
|
||||
|
||||
displayFlow = new TextFlow();
|
||||
displayFlow.setMouseTransparent(true);
|
||||
|
||||
getChildren().addFirst(displayFlow);
|
||||
|
||||
textListener = (_, _, newText) -> updateDisplay(newText);
|
||||
fontListener = (_, _, _) -> updateDisplay(control.getText());
|
||||
control.textProperty().addListener(textListener);
|
||||
control.fontProperty().addListener(fontListener);
|
||||
updateDisplay(control.getText());
|
||||
|
||||
control.setStyle("-fx-text-fill: transparent;");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
getSkinnable().textProperty().removeListener(textListener);
|
||||
getSkinnable().fontProperty().removeListener(fontListener);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
private void updateDisplay(String text) {
|
||||
displayFlow.getChildren().clear();
|
||||
if(text == null || text.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<AddressSpan> addresses = findAddresses(text);
|
||||
|
||||
int pos = 0;
|
||||
for(AddressSpan span : addresses) {
|
||||
if(span.start > pos) {
|
||||
Text normalText = createText(text.substring(pos, span.start), false);
|
||||
displayFlow.getChildren().add(normalText);
|
||||
}
|
||||
|
||||
addChunkedAddress(text.substring(span.start, span.end));
|
||||
pos = span.end;
|
||||
}
|
||||
|
||||
if(pos < text.length()) {
|
||||
Text normalText = createText(text.substring(pos), false);
|
||||
displayFlow.getChildren().add(normalText);
|
||||
}
|
||||
}
|
||||
|
||||
private void addChunkedAddress(String address) {
|
||||
String[] chunks = CHUNK_PATTERN.split(address);
|
||||
for(int i = 0; i < chunks.length; i++) {
|
||||
Text chunk = createText(chunks[i], i % 2 != 0);
|
||||
displayFlow.getChildren().add(chunk);
|
||||
}
|
||||
}
|
||||
|
||||
private Text createText(String content, boolean alternate) {
|
||||
Text text = new Text(content);
|
||||
text.setFont(getSkinnable().getFont());
|
||||
text.getStyleClass().add("address-chunk");
|
||||
if(alternate) {
|
||||
text.getStyleClass().add("alternate");
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
private List<AddressSpan> findAddresses(String text) {
|
||||
List<AddressSpan> spans = new ArrayList<>();
|
||||
|
||||
Pattern wordBoundary = Pattern.compile("\\S+");
|
||||
Matcher matcher = wordBoundary.matcher(text);
|
||||
|
||||
while(matcher.find()) {
|
||||
String candidate = matcher.group();
|
||||
if(isValidAddress(candidate)) {
|
||||
spans.add(new AddressSpan(matcher.start(), matcher.end()));
|
||||
}
|
||||
}
|
||||
|
||||
return spans;
|
||||
}
|
||||
|
||||
private boolean isValidAddress(String candidate) {
|
||||
Network network = Network.get();
|
||||
return network.hasP2PKHAddressPrefix(candidate) || network.hasP2SHAddressPrefix(candidate) ||
|
||||
candidate.startsWith(network.getBech32AddressHRP()) || candidate.startsWith(network.getSilentPaymentsAddressHrp());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateChildren() {
|
||||
super.updateChildren();
|
||||
if(displayFlow != null && !getChildren().contains(displayFlow)) {
|
||||
getChildren().addFirst(displayFlow);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void layoutChildren(double x, double y, double w, double h) {
|
||||
super.layoutChildren(x, y, w, h);
|
||||
|
||||
// Position TextFlow to align with the label's text area
|
||||
Label label = getSkinnable();
|
||||
Insets padding = label.getPadding();
|
||||
|
||||
Node graphic = label.getGraphic();
|
||||
double graphicOffset = 0;
|
||||
if(graphic != null && label.getContentDisplay() == ContentDisplay.LEFT) {
|
||||
graphicOffset = graphic.getLayoutBounds().getWidth() + label.getGraphicTextGap();
|
||||
}
|
||||
|
||||
displayFlow.resizeRelocate(
|
||||
x + padding.getLeft() + graphicOffset,
|
||||
y + padding.getTop(),
|
||||
w - padding.getLeft() - padding.getRight() - graphicOffset,
|
||||
h - padding.getTop() - padding.getBottom()
|
||||
);
|
||||
}
|
||||
|
||||
private record AddressSpan(int start, int end) {}
|
||||
}
|
||||
@@ -0,0 +1,273 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.Network;
|
||||
import com.sparrowwallet.drongo.protocol.Base58;
|
||||
import com.sparrowwallet.drongo.protocol.Bech32;
|
||||
import impl.org.controlsfx.skin.CustomTextFieldSkin;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.shape.Path;
|
||||
import javafx.scene.shape.Rectangle;
|
||||
import javafx.scene.text.Font;
|
||||
import javafx.scene.text.Text;
|
||||
import javafx.scene.text.TextFlow;
|
||||
import org.controlsfx.control.textfield.CustomTextField;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class AddressTextFieldSkin extends CustomTextFieldSkin {
|
||||
private static final boolean[] BASE58_OK = buildOkTable(new String(Base58.ALPHABET));
|
||||
private static final boolean[] BECH32_DATA_OK = buildOkTable(Bech32.CHARSET);
|
||||
|
||||
private final TextFlow displayFlow;
|
||||
private final Rectangle clip;
|
||||
private final ChangeListener<String> textListener;
|
||||
private final ChangeListener<Font> fontListener;
|
||||
|
||||
public AddressTextFieldSkin(TextField control) {
|
||||
super(control);
|
||||
|
||||
displayFlow = new TextFlow();
|
||||
displayFlow.setMouseTransparent(true);
|
||||
|
||||
clip = new Rectangle();
|
||||
displayFlow.setClip(clip);
|
||||
|
||||
getChildren().addFirst(displayFlow);
|
||||
|
||||
textListener = (_, _, newText) -> updateDisplay(newText);
|
||||
fontListener = (_, _, _) -> updateDisplay(control.getText());
|
||||
control.textProperty().addListener(textListener);
|
||||
control.fontProperty().addListener(fontListener);
|
||||
updateDisplay(control.getText());
|
||||
|
||||
control.setStyle("-fx-text-fill: transparent;");
|
||||
|
||||
// Unbind caret color since it's normally bound to textFill
|
||||
unbindCaretColor(getChildren());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
getSkinnable().textProperty().removeListener(textListener);
|
||||
getSkinnable().fontProperty().removeListener(fontListener);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
private void unbindCaretColor(javafx.collections.ObservableList<Node> children) {
|
||||
for(Node node : children) {
|
||||
if(node instanceof Path path && path.getStroke() != null) {
|
||||
path.fillProperty().unbind();
|
||||
path.strokeProperty().unbind();
|
||||
path.getStyleClass().add("address-field-caret");
|
||||
} else if(node instanceof javafx.scene.Parent parent) {
|
||||
unbindCaretColor(parent.getChildrenUnmodifiable());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ObjectProperty<Node> leftProperty() {
|
||||
if(getSkinnable() instanceof CustomTextField customTextField) {
|
||||
return customTextField.leftProperty();
|
||||
}
|
||||
|
||||
return new SimpleObjectProperty<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ObjectProperty<Node> rightProperty() {
|
||||
if(getSkinnable() instanceof CustomTextField customTextField) {
|
||||
return customTextField.rightProperty();
|
||||
}
|
||||
|
||||
return new SimpleObjectProperty<>();
|
||||
}
|
||||
|
||||
private void updateDisplay(String text) {
|
||||
displayFlow.getChildren().clear();
|
||||
if(text == null || text.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<AddressSpan> addresses = findAddresses(text);
|
||||
|
||||
int pos = 0;
|
||||
for(AddressSpan span : addresses) {
|
||||
if(span.start > pos) {
|
||||
Text normalText = createText(text.substring(pos, span.start), false);
|
||||
displayFlow.getChildren().add(normalText);
|
||||
}
|
||||
|
||||
addChunkedAddress(text.substring(span.start, span.end));
|
||||
pos = span.end;
|
||||
}
|
||||
|
||||
if(pos < text.length()) {
|
||||
Text normalText = createText(text.substring(pos), false);
|
||||
displayFlow.getChildren().add(normalText);
|
||||
}
|
||||
}
|
||||
|
||||
private void addChunkedAddress(String address) {
|
||||
String[] chunks = AddressLabelSkin.CHUNK_PATTERN.split(address);
|
||||
for(int i = 0; i < chunks.length; i++) {
|
||||
Text chunk = createText(chunks[i], i % 2 != 0);
|
||||
displayFlow.getChildren().add(chunk);
|
||||
}
|
||||
}
|
||||
|
||||
private Text createText(String content, boolean alternate) {
|
||||
Text text = new Text(content);
|
||||
text.setFont(getSkinnable().getFont());
|
||||
text.getStyleClass().add("address-chunk");
|
||||
if(alternate) {
|
||||
text.getStyleClass().add("alternate");
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
private List<AddressSpan> findAddresses(String text) {
|
||||
List<AddressSpan> spans = new ArrayList<>();
|
||||
|
||||
Pattern wordBoundary = Pattern.compile("\\S+");
|
||||
Matcher matcher = wordBoundary.matcher(text);
|
||||
|
||||
while(matcher.find()) {
|
||||
String candidate = matcher.group();
|
||||
if(isValidAddress(candidate)) {
|
||||
spans.add(new AddressSpan(matcher.start(), matcher.end()));
|
||||
}
|
||||
}
|
||||
|
||||
return spans;
|
||||
}
|
||||
|
||||
private boolean isValidAddress(String candidate) {
|
||||
if(candidate == null || candidate.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Network network = Network.get();
|
||||
|
||||
// Base58 (legacy) partial: must start with a legacy prefix and contain only base58 chars.
|
||||
if(network.hasP2PKHAddressPrefix(candidate) || network.hasP2SHAddressPrefix(candidate)) {
|
||||
return containsOnlyAscii(candidate, BASE58_OK);
|
||||
}
|
||||
|
||||
String lower = candidate.toLowerCase(Locale.ROOT);
|
||||
|
||||
// Bech32 (segwit v0/v1) partial: starts with HRP, then optional '1', then bech32 data charset.
|
||||
if(lower.startsWith(network.getBech32AddressHRP())) {
|
||||
return isBech32LikePartial(lower);
|
||||
}
|
||||
|
||||
// Silent payments partial (bech32-like): starts with its HRP, then optional '1', then bech32 data charset.
|
||||
if(lower.startsWith(network.getSilentPaymentsAddressHrp())) {
|
||||
return isBech32LikePartial(lower);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean isBech32LikePartial(String lower) {
|
||||
int sep = lower.indexOf(Bech32.BECH32_SEPARATOR);
|
||||
|
||||
if(sep < 0) {
|
||||
return containsOnlyHrpChars(lower);
|
||||
}
|
||||
|
||||
String hrp = lower.substring(0, sep);
|
||||
String dataPart = lower.substring(sep + 1);
|
||||
|
||||
if(hrp.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return containsOnlyHrpChars(hrp) && containsOnlyAscii(dataPart, BECH32_DATA_OK);
|
||||
}
|
||||
|
||||
private static boolean containsOnlyHrpChars(String s) {
|
||||
for(int i = 0; i < s.length(); i++) {
|
||||
char c = s.charAt(i);
|
||||
boolean ok = (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9');
|
||||
if(!ok) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static boolean[] buildOkTable(String allowed) {
|
||||
boolean[] ok = new boolean[128];
|
||||
for(int i = 0; i < allowed.length(); i++) {
|
||||
char c = allowed.charAt(i);
|
||||
if(c < ok.length) {
|
||||
ok[c] = true;
|
||||
} else {
|
||||
throw new IllegalArgumentException("Non-ASCII allowed char: " + c);
|
||||
}
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
private static boolean containsOnlyAscii(String s, boolean[] ok) {
|
||||
for(int i = 0; i < s.length(); i++) {
|
||||
char c = s.charAt(i);
|
||||
if(c >= ok.length || !ok[c]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void layoutChildren(double x, double y, double w, double h) {
|
||||
super.layoutChildren(x, y, w, h);
|
||||
|
||||
Insets padding = getSkinnable().getPadding();
|
||||
|
||||
double leftWidth = 0;
|
||||
double rightWidth = 0;
|
||||
if(getSkinnable() instanceof CustomTextField customTextField) {
|
||||
Node left = customTextField.getLeft();
|
||||
Node right = customTextField.getRight();
|
||||
if(left != null) {
|
||||
leftWidth = left.getLayoutBounds().getWidth();
|
||||
if(left instanceof Region leftRegion) {
|
||||
leftWidth += leftRegion.getPadding().getLeft() + leftRegion.getPadding().getRight() + 1;
|
||||
}
|
||||
}
|
||||
if(right != null) {
|
||||
rightWidth = right.getLayoutBounds().getWidth();
|
||||
if(right instanceof Region rightRegion) {
|
||||
rightWidth += rightRegion.getPadding().getLeft() + rightRegion.getPadding().getRight();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
double availableWidth = w - padding.getLeft() - padding.getRight() - leftWidth - rightWidth;
|
||||
clip.setWidth(availableWidth);
|
||||
clip.setHeight(h);
|
||||
|
||||
double topOffset = getSkinnable().getBaselineOffset() - displayFlow.getBaselineOffset();
|
||||
|
||||
displayFlow.resizeRelocate(
|
||||
padding.getLeft() + leftWidth,
|
||||
topOffset,
|
||||
displayFlow.prefWidth(-1),
|
||||
h - padding.getTop() - padding.getBottom()
|
||||
);
|
||||
}
|
||||
|
||||
private record AddressSpan(int start, int end) {}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.drongo.address.InvalidAddressException;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.text.Text;
|
||||
import javafx.scene.text.TextFlow;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class AddressTooltipSkin implements Skin<Tooltip> {
|
||||
private final Tooltip tooltip;
|
||||
private final TextFlow textFlow;
|
||||
private final ChangeListener<String> textListener;
|
||||
|
||||
public AddressTooltipSkin(Tooltip tooltip) {
|
||||
this.tooltip = tooltip;
|
||||
|
||||
textFlow = new TextFlow();
|
||||
textFlow.getStyleClass().addAll(tooltip.getStyleClass());
|
||||
|
||||
textListener = (_, _, newText) -> updateDisplay(newText);
|
||||
tooltip.textProperty().addListener(textListener);
|
||||
updateDisplay(tooltip.getText());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Tooltip getSkinnable() {
|
||||
return tooltip;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Node getNode() {
|
||||
return textFlow;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
tooltip.textProperty().removeListener(textListener);
|
||||
}
|
||||
|
||||
private void updateDisplay(String text) {
|
||||
textFlow.getChildren().clear();
|
||||
if(text == null || text.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<AddressSpan> addresses = findAddresses(text);
|
||||
|
||||
int pos = 0;
|
||||
for(AddressSpan span : addresses) {
|
||||
if(span.start > pos) {
|
||||
textFlow.getChildren().add(createText(text.substring(pos, span.start), false));
|
||||
}
|
||||
addChunkedAddress(text.substring(span.start, span.end));
|
||||
pos = span.end;
|
||||
}
|
||||
|
||||
if(pos < text.length()) {
|
||||
textFlow.getChildren().add(createText(text.substring(pos), false));
|
||||
}
|
||||
}
|
||||
|
||||
private void addChunkedAddress(String address) {
|
||||
String[] chunks = AddressLabelSkin.CHUNK_PATTERN.split(address);
|
||||
for(int i = 0; i < chunks.length; i++) {
|
||||
textFlow.getChildren().add(createText(chunks[i], i % 2 != 0));
|
||||
}
|
||||
}
|
||||
|
||||
private Text createText(String content, boolean alternate) {
|
||||
Text text = new Text(content);
|
||||
text.getStyleClass().add("address-chunk");
|
||||
if(alternate) {
|
||||
text.getStyleClass().add("alternate");
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
private List<AddressSpan> findAddresses(String text) {
|
||||
List<AddressSpan> spans = new ArrayList<>();
|
||||
|
||||
Pattern wordBoundary = Pattern.compile("\\S+");
|
||||
Matcher matcher = wordBoundary.matcher(text);
|
||||
|
||||
while(matcher.find()) {
|
||||
String candidate = matcher.group();
|
||||
if(isValidAddress(candidate)) {
|
||||
spans.add(new AddressSpan(matcher.start(), matcher.end()));
|
||||
}
|
||||
}
|
||||
|
||||
return spans;
|
||||
}
|
||||
|
||||
private boolean isValidAddress(String candidate) {
|
||||
try {
|
||||
Address.fromString(candidate);
|
||||
return true;
|
||||
} catch(InvalidAddressException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private record AddressSpan(int start, int end) {}
|
||||
}
|
||||
@@ -30,7 +30,11 @@ public class AddressTreeTable extends CoinTreeTable {
|
||||
addressCol.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, Entry> param) -> {
|
||||
return new ReadOnlyObjectWrapper<>(param.getValue().getValue());
|
||||
});
|
||||
addressCol.setCellFactory(p -> new EntryCell());
|
||||
addressCol.setCellFactory(p -> {
|
||||
EntryCell entryCell = new EntryCell();
|
||||
entryCell.setSkin(new AddressTreeTableCellSkin<>(entryCell));
|
||||
return entryCell;
|
||||
});
|
||||
addressCol.setSortable(false);
|
||||
getColumns().add(addressCol);
|
||||
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.control.TreeTableCell;
|
||||
import javafx.scene.control.skin.TreeTableCellSkin;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.text.Text;
|
||||
import javafx.scene.text.TextFlow;
|
||||
|
||||
public class AddressTreeTableCellSkin<S, T> extends TreeTableCellSkin<S, T> {
|
||||
private final TextFlow displayFlow;
|
||||
private final ChangeListener<String> textListener;
|
||||
private final Text ellipsisText;
|
||||
private String currentDisplayedText;
|
||||
|
||||
public AddressTreeTableCellSkin(TreeTableCell<S, T> cell) {
|
||||
super(cell);
|
||||
|
||||
displayFlow = new TextFlow();
|
||||
displayFlow.setMouseTransparent(true);
|
||||
displayFlow.setMinWidth(Region.USE_PREF_SIZE);
|
||||
getChildren().add(displayFlow);
|
||||
|
||||
ellipsisText = new Text("...");
|
||||
ellipsisText.fontProperty().bind(cell.fontProperty());
|
||||
ellipsisText.getStyleClass().add("address-chunk");
|
||||
|
||||
textListener = (_, _, newText) -> updateDisplay(newText);
|
||||
cell.textProperty().addListener(textListener);
|
||||
updateDisplay(cell.getText());
|
||||
|
||||
cell.setStyle("-fx-text-fill: transparent;");
|
||||
}
|
||||
|
||||
private void updateDisplay(String text) {
|
||||
currentDisplayedText = text;
|
||||
buildDisplay(text, false);
|
||||
}
|
||||
|
||||
private void buildDisplay(String text, boolean truncated) {
|
||||
displayFlow.getChildren().clear();
|
||||
|
||||
if(text == null || text.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(getSkinnable().getStyleClass().contains("address-cell")) {
|
||||
String[] chunks = AddressLabelSkin.CHUNK_PATTERN.split(text);
|
||||
for(int i = 0; i < chunks.length; i++) {
|
||||
displayFlow.getChildren().add(createText(chunks[i], i % 2 != 0));
|
||||
}
|
||||
} else {
|
||||
Text normalText = createText(text, false);
|
||||
displayFlow.getChildren().add(normalText);
|
||||
}
|
||||
|
||||
if(truncated) {
|
||||
displayFlow.getChildren().add(ellipsisText);
|
||||
}
|
||||
}
|
||||
|
||||
private Text createText(String content, boolean alternate) {
|
||||
Text text = new Text(content);
|
||||
text.fontProperty().bind(getSkinnable().fontProperty());
|
||||
text.getStyleClass().add("address-chunk");
|
||||
if(alternate) {
|
||||
text.getStyleClass().add("alternate");
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void layoutChildren(double x, double y, double w, double h) {
|
||||
super.layoutChildren(x, y, w, h);
|
||||
|
||||
TreeTableCell<S, T> cell = getSkinnable();
|
||||
Insets padding = cell.getPadding();
|
||||
|
||||
double leftOffset = 0;
|
||||
double topOffset = y + padding.getTop();
|
||||
|
||||
Text labeledText = (Text)getChildren().stream().filter(n -> n instanceof Text).findFirst().orElse(null);
|
||||
if(labeledText != null) {
|
||||
leftOffset = labeledText.getLayoutX();
|
||||
topOffset = labeledText.getLayoutY() - labeledText.getBaselineOffset();
|
||||
|
||||
String fullText = cell.getText();
|
||||
String displayedText = labeledText.getText();
|
||||
|
||||
if(fullText != null && displayedText != null && !fullText.equals(displayedText)) {
|
||||
String ellipsis = cell.getEllipsisString();
|
||||
if(displayedText.endsWith(ellipsis)) {
|
||||
String truncatedText = displayedText.substring(0, displayedText.length() - ellipsis.length());
|
||||
if(!truncatedText.equals(currentDisplayedText)) {
|
||||
currentDisplayedText = truncatedText;
|
||||
buildDisplay(truncatedText, true);
|
||||
}
|
||||
}
|
||||
} else if(fullText != null && !fullText.equals(currentDisplayedText)) {
|
||||
currentDisplayedText = fullText;
|
||||
buildDisplay(fullText, false);
|
||||
}
|
||||
}
|
||||
|
||||
displayFlow.resizeRelocate(
|
||||
leftOffset,
|
||||
topOffset,
|
||||
w - padding.getLeft() - padding.getRight(),
|
||||
h - padding.getTop() - padding.getBottom()
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateChildren() {
|
||||
super.updateChildren();
|
||||
if(displayFlow != null && !getChildren().contains(displayFlow)) {
|
||||
getChildren().add(displayFlow);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
getSkinnable().textProperty().removeListener(textListener);
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -124,13 +124,13 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||
setGraphic(actionBox);
|
||||
} else if(entry instanceof NodeEntry nodeEntry) {
|
||||
Address address = nodeEntry.getAddress();
|
||||
getStyleClass().add("address-cell");
|
||||
setText(address.toString());
|
||||
setContextMenu(new AddressContextMenu(address, nodeEntry.getOutputDescriptor(), nodeEntry, true, getTreeTableView()));
|
||||
Tooltip tooltip = new Tooltip();
|
||||
tooltip.setShowDelay(Duration.millis(250));
|
||||
tooltip.setText(nodeEntry.getNode().toString());
|
||||
setTooltip(tooltip);
|
||||
getStyleClass().add("address-cell");
|
||||
|
||||
HBox actionBox = new HBox();
|
||||
actionBox.getStyleClass().add("cell-actions");
|
||||
|
||||
@@ -134,6 +134,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
address.getStyleClass().add("id");
|
||||
address.setEditable(walletNode == null);
|
||||
address.setTooltip(new Tooltip("Only singlesig addresses can sign"));
|
||||
address.setSkin(new AddressTextFieldSkin(address));
|
||||
addressField.getInputs().add(address);
|
||||
|
||||
if(walletNode != null) {
|
||||
|
||||
@@ -140,6 +140,7 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
|
||||
toAddressField.setText("Sweep to:");
|
||||
toAddress = new ComboBoxTextField();
|
||||
toAddress.getStyleClass().add("fixed-width");
|
||||
toAddress.setSkin(new AddressTextFieldSkin(toAddress));
|
||||
toWallet = new ComboBox<>();
|
||||
toWallet.setItems(FXCollections.observableList(AppServices.get().getOpenWallets().keySet().stream()
|
||||
.filter(w -> !w.isWhirlpoolChildWallet() && !w.isBip47()).collect(Collectors.toList())));
|
||||
|
||||
@@ -74,6 +74,7 @@ public class SearchWalletDialog extends Dialog<Entry> {
|
||||
searchField.setText("Search:");
|
||||
search = TextFields.createClearableTextField();
|
||||
search.setPromptText("Label, address, value or transaction ID");
|
||||
search.setSkin(new AddressTextFieldSkin(search));
|
||||
searchField.getInputs().add(search);
|
||||
|
||||
fieldset.getChildren().addAll(searchField);
|
||||
@@ -113,7 +114,11 @@ public class SearchWalletDialog extends Dialog<Entry> {
|
||||
entryCol.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, Entry> param) -> {
|
||||
return new ReadOnlyObjectWrapper<>(param.getValue().getValue());
|
||||
});
|
||||
entryCol.setCellFactory(p -> new SearchEntryCell());
|
||||
entryCol.setCellFactory(p -> {
|
||||
SearchEntryCell searchEntryCell = new SearchEntryCell();
|
||||
searchEntryCell.setSkin(new AddressTreeTableCellSkin<>(searchEntryCell));
|
||||
return searchEntryCell;
|
||||
});
|
||||
String address = walletForms.iterator().next().getNodeEntry(KeyPurpose.RECEIVE).getAddress().toString();
|
||||
entryCol.setMinWidth(TextUtils.computeTextWidth(AppServices.getMonospaceFont(), address, 0.0));
|
||||
results.getColumns().add(entryCol);
|
||||
|
||||
@@ -532,6 +532,7 @@ public class TransactionDiagram extends GridPane {
|
||||
tooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY));
|
||||
tooltip.setShowDuration(Duration.INDEFINITE);
|
||||
tooltip.setWrapText(true);
|
||||
tooltip.setSkin(new AddressTooltipSkin(tooltip));
|
||||
Window activeWindow = AppServices.getActiveWindow();
|
||||
if(activeWindow != null) {
|
||||
tooltip.setMaxWidth(activeWindow.getWidth());
|
||||
@@ -727,9 +728,13 @@ public class TransactionDiagram extends GridPane {
|
||||
for(Payment payment : displayedPayments) {
|
||||
Glyph outputGlyph = GlyphUtils.getOutputGlyph(walletTx, payment);
|
||||
boolean labelledPayment = outputGlyph.getStyleClass().stream().anyMatch(style -> List.of("premix-icon", "badbank-icon", "whirlpoolfee-icon", "anchor-icon").contains(style)) || payment instanceof AdditionalPayment || payment.getLabel() != null;
|
||||
Label recipientLabel = new Label(payment.getLabel() == null || payment.getType() == Payment.Type.FAKE_MIX || payment.getType() == Payment.Type.MIX ? payment.toString().substring(0, 8) + "..." : payment.getLabel(), outputGlyph);
|
||||
boolean addressLabel = payment.getLabel() == null || payment.getType() == Payment.Type.FAKE_MIX || payment.getType() == Payment.Type.MIX;
|
||||
Label recipientLabel = new Label(addressLabel ? payment.toString().substring(0, 8) + "..." : payment.getLabel(), outputGlyph);
|
||||
recipientLabel.getStyleClass().add("output-label");
|
||||
recipientLabel.getStyleClass().add(labelledPayment ? "payment-label" : "recipient-label");
|
||||
if(addressLabel) {
|
||||
recipientLabel.setSkin(new AddressLabelSkin(recipientLabel));
|
||||
}
|
||||
Wallet toWallet = walletTx.getToWallet(AppServices.get().getOpenWallets().keySet(), payment);
|
||||
WalletNode toNode = payment instanceof WalletNodePayment walletNodePayment ? walletNodePayment.getWalletNode() : null;
|
||||
Wallet toBip47Wallet = getBip47SendWallet(payment);
|
||||
@@ -742,6 +747,7 @@ public class TransactionDiagram extends GridPane {
|
||||
recipientTooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY));
|
||||
recipientTooltip.setShowDuration(Duration.INDEFINITE);
|
||||
recipientTooltip.setWrapText(true);
|
||||
recipientTooltip.setSkin(new AddressTooltipSkin(recipientTooltip));
|
||||
Window activeWindow = AppServices.getActiveWindow();
|
||||
if(activeWindow != null) {
|
||||
recipientTooltip.setMaxWidth(activeWindow.getWidth());
|
||||
@@ -782,10 +788,12 @@ public class TransactionDiagram extends GridPane {
|
||||
String changeDesc = changeAddress.toString().substring(0, 8) + "...";
|
||||
Label changeLabel = new Label(changeDesc, overGapLimit ? getChangeWarningGlyph() : getChangeGlyph());
|
||||
changeLabel.getStyleClass().addAll("output-label", "change-label");
|
||||
changeLabel.setSkin(new AddressLabelSkin(changeLabel));
|
||||
Tooltip changeTooltip = new Tooltip("Change of " + getSatsValue(changeEntry.getValue()) + " sats to " + changeNode + "\n" + walletTx.getChangeAddress(changeNode).toString() + (overGapLimit ? "\nAddress is beyond the gap limit!" : ""));
|
||||
changeTooltip.getStyleClass().add("change-label");
|
||||
changeTooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY));
|
||||
changeTooltip.setShowDuration(Duration.INDEFINITE);
|
||||
changeTooltip.setSkin(new AddressTooltipSkin(changeTooltip));
|
||||
changeLabel.setTooltip(changeTooltip);
|
||||
actionBox.getChildren().add(changeLabel);
|
||||
|
||||
|
||||
@@ -240,6 +240,7 @@ public class TransactionDiagramLabel extends HBox {
|
||||
icon.setGraphic(glyph);
|
||||
|
||||
CopyableLabel label = new CopyableLabel();
|
||||
label.setSkin(new AddressTextFieldSkin(label));
|
||||
label.setFont(Font.font("Fragment Mono Italic", 13));
|
||||
label.setText(text);
|
||||
|
||||
|
||||
@@ -57,7 +57,11 @@ public class UtxosTreeTable extends CoinTreeTable {
|
||||
addressCol.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, UtxoEntry.AddressStatus> param) -> {
|
||||
return ((UtxoEntry)param.getValue().getValue()).addressStatusProperty();
|
||||
});
|
||||
addressCol.setCellFactory(p -> new AddressCell());
|
||||
addressCol.setCellFactory(p -> {
|
||||
AddressCell addressCell = new AddressCell();
|
||||
addressCell.setSkin(new AddressTreeTableCellSkin<>(addressCell));
|
||||
return addressCell;
|
||||
});
|
||||
addressCol.setSortable(true);
|
||||
addressCol.setComparator(Comparator.comparing(o -> o.getAddress().toString()));
|
||||
getColumns().add(addressCol);
|
||||
|
||||
@@ -6,16 +6,19 @@ import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.drongo.protocol.NonStandardScriptException;
|
||||
import com.sparrowwallet.drongo.protocol.TransactionOutput;
|
||||
import com.sparrowwallet.drongo.silentpayments.SilentPaymentAddress;
|
||||
import com.sparrowwallet.sparrow.UnitFormat;
|
||||
import com.sparrowwallet.sparrow.BaseController;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.TransactionTabData;
|
||||
import com.sparrowwallet.sparrow.UnitFormat;
|
||||
import com.sparrowwallet.sparrow.control.AddressLabelSkin;
|
||||
import com.sparrowwallet.sparrow.event.TransactionTabsClosedEvent;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import javafx.application.Platform;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.scene.chart.PieChart;
|
||||
import javafx.scene.control.ContextMenu;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.MenuItem;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.input.Clipboard;
|
||||
@@ -93,6 +96,19 @@ public abstract class TransactionFormController extends BaseController {
|
||||
Tooltip.install(data.getNode(), tooltip);
|
||||
data.pieValueProperty().addListener((observable, oldValue, newValue) -> tooltip.setText(newValue + "%"));
|
||||
});
|
||||
|
||||
Platform.runLater(() -> applyAddressLabelSkinToLegend(pie));
|
||||
}
|
||||
|
||||
private void applyAddressLabelSkinToLegend(PieChart pie) {
|
||||
pie.lookupAll(".chart-legend-item").forEach(node -> {
|
||||
if(node instanceof Label label) {
|
||||
String text = label.getText();
|
||||
label.setSkin(new AddressLabelSkin(label));
|
||||
label.setText("");
|
||||
label.setText(text);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void close() {
|
||||
|
||||
@@ -387,6 +387,7 @@ public class PaymentController extends WalletFormController implements Initializ
|
||||
|
||||
address.textProperty().addListener(addressListener);
|
||||
address.setContextMenu(address.getCustomContextMenu(Collections.emptyList()));
|
||||
address.setSkin(new AddressTextFieldSkin(address));
|
||||
|
||||
label.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||
maxButton.setDisable(!isMaxButtonEnabled());
|
||||
|
||||
@@ -79,6 +79,7 @@ public class ReceiveController extends WalletFormController implements Initializ
|
||||
|
||||
@Override
|
||||
public void initializeView() {
|
||||
address.setSkin(new AddressTextFieldSkin(address));
|
||||
initializeScriptField(scriptPubKeyArea);
|
||||
|
||||
displayAddress.managedProperty().bind(displayAddress.visibleProperty());
|
||||
|
||||
Reference in New Issue
Block a user