format display of addresses in 4 character chunks
What changed, and why it matters
This commit changes how Bitcoin addresses are shown on screen in the Sparrow Wallet app. Instead of displaying one long continuous string, addresses are now split into 4-character chunks with alternating colors to make them easier to read and verify. It is a user-interface improvement, not a security fix or vulnerability.
No security action required. Review as a normal UI/UX change. If desired, verify that the new skins correctly handle edge cases such as very long addresses, truncated text, and high-DPI rendering, but these are quality concerns rather than security issues.
Security signals we found
No security-relevant signals detected in the diff
Change is purely presentational UI formatting
No input validation logic changes beyond address detection for display purposes
No cryptographic, networking, or privilege changes
Evidence from the diff
The commit introduces several new JavaFX skins (AddressLabelSkin, AddressTextFieldSkin, AddressTooltipSkin, AddressTreeTableCellSkin) that render address text as 4-character chunks using a TextFlow overlay. The original control text is made transparent and the overlay provides visual grouping. Address detection is performed via prefix matching against the current network (P2PKH/P2SH prefixes, Bech32 HRP, Silent Payments HRP) and character-set validation for Base58 and Bech32 alphabets. The skins are applied across labels, text fields, tooltips, tree-table cells, and pie-chart legend items. CSS rules are added to style the chunks and alternate chunks.
Changed components
com.sparrowwallet.sparrow.control.AddressLabelcom.sparrowwallet.sparrow.control.AddressLabelSkincom.sparrowwallet.sparrow.control.AddressTextFieldSkincom.sparrowwallet.sparrow.control.AddressTooltipSkincom.sparrowwallet.sparrow.control.AddressTreeTablecom.sparrowwallet.sparrow.control.AddressTreeTableCellSkincom.sparrowwallet.sparrow.control.EntryCellcom.sparrowwallet.sparrow.control.MessageSignDialogcom.sparrowwallet.sparrow.control.PrivateKeySweepDialogcom.sparrowwallet.sparrow.control.SearchWalletDialogcom.sparrowwallet.sparrow.control.TransactionDiagramcom.sparrowwallet.sparrow.control.TransactionDiagramLabelcom.sparrowwallet.sparrow.control.UtxosTreeTablecom.sparrowwallet.sparrow.transaction.TransactionFormControllercom.sparrowwallet.sparrow.wallet.PaymentControllercom.sparrowwallet.sparrow.wallet.ReceiveControllerdarktheme.cssgeneral.cssInspect captured patch +758 / −8
diff --git a/src/main/java/com/sparrowwallet/sparrow/control/AddressLabel.java b/src/main/java/com/sparrowwallet/sparrow/control/AddressLabel.java
index 9965bfc..7547d9d 100644
--- a/src/main/java/com/sparrowwallet/sparrow/control/AddressLabel.java
+++ b/src/main/java/com/sparrowwallet/sparrow/control/AddressLabel.java
@@ -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());
diff --git a/src/main/java/com/sparrowwallet/sparrow/control/AddressLabelSkin.java b/src/main/java/com/sparrowwallet/sparrow/control/AddressLabelSkin.java
new file mode 100644
index 0000000..c5eec84
--- /dev/null
+++ b/src/main/java/com/sparrowwallet/sparrow/control/AddressLabelSkin.java
@@ -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) {}
+}
diff --git a/src/main/java/com/sparrowwallet/sparrow/control/AddressTextFieldSkin.java b/src/main/java/com/sparrowwallet/sparrow/control/AddressTextFieldSkin.java
new file mode 100644
index 0000000..9b18beb
--- /dev/null
+++ b/src/main/java/com/sparrowwallet/sparrow/control/AddressTextFieldSkin.java
@@ -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) {}
+}
diff --git a/src/main/java/com/sparrowwallet/sparrow/control/AddressTooltipSkin.java b/src/main/java/com/sparrowwallet/sparrow/control/AddressTooltipSkin.java
new file mode 100644
index 0000000..79e2c98
--- /dev/null
+++ b/src/main/java/com/sparrowwallet/sparrow/control/AddressTooltipSkin.java
@@ -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) {}
+}
diff --git a/src/main/java/com/sparrowwallet/sparrow/control/AddressTreeTable.java b/src/main/java/com/sparrowwallet/sparrow/control/AddressTreeTable.java
index 673e5a2..9f9ab8f 100644
--- a/src/main/java/com/sparrowwallet/sparrow/control/AddressTreeTable.java
+++ b/src/main/java/com/sparrowwallet/sparrow/control/AddressTreeTable.java
@@ -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);
diff --git a/src/main/java/com/sparrowwallet/sparrow/control/AddressTreeTableCellSkin.java b/src/main/java/com/sparrowwallet/sparrow/control/AddressTreeTableCellSkin.java
new file mode 100644
index 0000000..68168d2
--- /dev/null
+++ b/src/main/java/com/sparrowwallet/sparrow/control/AddressTreeTableCellSkin.java
@@ -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();
+ }
+}
diff --git a/src/main/java/com/sparrowwallet/sparrow/control/EntryCell.java b/src/main/java/com/sparrowwallet/sparrow/control/EntryCell.java
index 4a7224a..53ad4bd 100644
--- a/src/main/java/com/sparrowwallet/sparrow/control/EntryCell.java
+++ b/src/main/java/com/sparrowwallet/sparrow/control/EntryCell.java
@@ -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");
diff --git a/src/main/java/com/sparrowwallet/sparrow/control/MessageSignDialog.java b/src/main/java/com/sparrowwallet/sparrow/control/MessageSignDialog.java
index fac7e84..25a2ea7 100644
--- a/src/main/java/com/sparrowwallet/sparrow/control/MessageSignDialog.java
+++ b/src/main/java/com/sparrowwallet/sparrow/control/MessageSignDialog.java
@@ -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) {
diff --git a/src/main/java/com/sparrowwallet/sparrow/control/PrivateKeySweepDialog.java b/src/main/java/com/sparrowwallet/sparrow/control/PrivateKeySweepDialog.java
index ff713f3..2f0da1b 100644
--- a/src/main/java/com/sparrowwallet/sparrow/control/PrivateKeySweepDialog.java
+++ b/src/main/java/com/sparrowwallet/sparrow/control/PrivateKeySweepDialog.java
@@ -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())));
diff --git a/src/main/java/com/sparrowwallet/sparrow/control/SearchWalletDialog.java b/src/main/java/com/sparrowwallet/sparrow/control/SearchWalletDialog.java
index b844567..c5e0fb3 100644
--- a/src/main/java/com/sparrowwallet/sparrow/control/SearchWalletDialog.java
+++ b/src/main/java/com/sparrowwallet/sparrow/control/SearchWalletDialog.java
@@ -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);
diff --git a/src/main/java/com/sparrowwallet/sparrow/control/TransactionDiagram.java b/src/main/java/com/sparrowwallet/sparrow/control/TransactionDiagram.java
index ee252b0..59d2fd8 100644
--- a/src/main/java/com/sparrowwallet/sparrow/control/TransactionDiagram.java
+++ b/src/main/java/com/sparrowwallet/sparrow/control/TransactionDiagram.java
@@ -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);
diff --git a/src/main/java/com/sparrowwallet/sparrow/control/TransactionDiagramLabel.java b/src/main/java/com/sparrowwallet/sparrow/control/TransactionDiagramLabel.java
index 825bd2e..8c62a60 100644
--- a/src/main/java/com/sparrowwallet/sparrow/control/TransactionDiagramLabel.java
+++ b/src/main/java/com/sparrowwallet/sparrow/control/TransactionDiagramLabel.java
@@ -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);
diff --git a/src/main/java/com/sparrowwallet/sparrow/control/UtxosTreeTable.java b/src/main/java/com/sparrowwallet/sparrow/control/UtxosTreeTable.java
index dfe86a8..d657dae 100644
--- a/src/main/java/com/sparrowwallet/sparrow/control/UtxosTreeTable.java
+++ b/src/main/java/com/sparrowwallet/sparrow/control/UtxosTreeTable.java
@@ -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);
diff --git a/src/main/java/com/sparrowwallet/sparrow/transaction/TransactionFormController.java b/src/main/java/com/sparrowwallet/sparrow/transaction/TransactionFormController.java
index cfe2b81..1915374 100644
--- a/src/main/java/com/sparrowwallet/sparrow/transaction/TransactionFormController.java
+++ b/src/main/java/com/sparrowwallet/sparrow/transaction/TransactionFormController.java
@@ -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() {
diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/PaymentController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/PaymentController.java
index 8494bdc..76b402d 100644
--- a/src/main/java/com/sparrowwallet/sparrow/wallet/PaymentController.java
+++ b/src/main/java/com/sparrowwallet/sparrow/wallet/PaymentController.java
@@ -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());
diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/ReceiveController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/ReceiveController.java
index 0d15bb0..0e901a8 100644
--- a/src/main/java/com/sparrowwallet/sparrow/wallet/ReceiveController.java
+++ b/src/main/java/com/sparrowwallet/sparrow/wallet/ReceiveController.java
@@ -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());
diff --git a/src/main/resources/com/sparrowwallet/sparrow/darktheme.css b/src/main/resources/com/sparrowwallet/sparrow/darktheme.css
index 49a1ac5..8bd7563 100644
--- a/src/main/resources/com/sparrowwallet/sparrow/darktheme.css
+++ b/src/main/resources/com/sparrowwallet/sparrow/darktheme.css
@@ -376,4 +376,20 @@ HorizontalHeaderColumn > TableColumnHeader.column-header.table-column{
.root .block-unconfirmed .block-left {
-fx-fill: #4e4846;
-}
\ No newline at end of file
+}
+
+.root .label .address-chunk {
+ -fx-fill: lightgray;
+}
+
+.root .tree-table-view .address-chunk, .root .text-field .address-chunk, .root .tooltip .address-chunk {
+ -fx-fill: white;
+}
+
+.root .address-chunk.alternate {
+ -fx-fill: derive(-fx-text-inner-color, -35%);
+}
+
+.root .tree-table-view .tree-table-row-cell:selected .address-chunk.alternate {
+ -fx-fill: derive(-fx-selection-bar-text, -35%);
+}
diff --git a/src/main/resources/com/sparrowwallet/sparrow/general.css b/src/main/resources/com/sparrowwallet/sparrow/general.css
index bb76d42..a419944 100644
--- a/src/main/resources/com/sparrowwallet/sparrow/general.css
+++ b/src/main/resources/com/sparrowwallet/sparrow/general.css
@@ -331,4 +331,37 @@ CellView > .text-input.text-field {
.tooltip {
-fx-show-delay: 200ms;
-}
\ No newline at end of file
+}
+
+.address-field-caret {
+ -fx-fill: -fx-text-inner-color;
+ -fx-stroke: -fx-text-inner-color;
+}
+
+.address-chunk {
+ -fx-fill: -fx-text-inner-color;
+}
+
+.tree-table-view .tree-table-row-cell:selected .address-chunk {
+ -fx-fill: -fx-selection-bar-text;
+}
+
+.tooltip .address-chunk {
+ -fx-fill: white;
+}
+
+.address-chunk.alternate {
+ -fx-fill: derive(-fx-text-inner-color, 70%);
+}
+
+.tooltip .address-chunk.alternate {
+ -fx-fill: derive(gray, 60%);
+}
+
+.tree-table-view:focused .tree-table-row-cell:selected .address-chunk.alternate {
+ -fx-fill: derive(-fx-selection-bar-text, -15%);
+}
+
+.tree-table-view .tree-table-row-cell:selected .address-chunk.alternate {
+ -fx-fill: derive(-fx-selection-bar-text, 60%);
+}
Why this scored 18/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.