diff --git a/src/main/java/com/sparrowwallet/sparrow/control/AddressLabel.java b/src/main/java/com/sparrowwallet/sparrow/control/AddressLabel.java index 9965bfcf..7547d9df 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 00000000..c5eec84e --- /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 textListener; + private final ChangeListener 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 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 findAddresses(String text) { + List 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 00000000..9b18beb7 --- /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 textListener; + private final ChangeListener 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 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 leftProperty() { + if(getSkinnable() instanceof CustomTextField customTextField) { + return customTextField.leftProperty(); + } + + return new SimpleObjectProperty<>(); + } + + @Override + public ObjectProperty 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 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 findAddresses(String text) { + List 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 00000000..79e2c984 --- /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 { + private final Tooltip tooltip; + private final TextFlow textFlow; + private final ChangeListener 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 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 findAddresses(String text) { + List 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 673e5a27..9f9ab8ff 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 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 00000000..68168d23 --- /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 extends TreeTableCellSkin { + private final TextFlow displayFlow; + private final ChangeListener textListener; + private final Text ellipsisText; + private String currentDisplayedText; + + public AddressTreeTableCellSkin(TreeTableCell 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 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 4a7224a9..53ad4bd0 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 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 fac7e84a..25a2ea79 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 { 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 ff713f3d..2f0da1bf 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 { 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 b844567d..c5e0fb3e 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 { 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 { entryCol.setCellValueFactory((TreeTableColumn.CellDataFeatures 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 ee252b0d..59d2fd88 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 825bd2e5..8c62a601 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 dfe86a8e..d657daed 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 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 cfe2b81a..19153745 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 8494bdc4..76b402dd 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 0d15bb09..0e901a8c 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 49a1ac55..8bd75636 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 bb76d42c..a4199449 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%); +}