From 6cde97adea73a8a7090c08e438ee960d00ffd380 Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Wed, 16 Sep 2026 11:07:42 +0200 Subject: [PATCH] ignore amount digits beyond the selected unit precision in the send tab amount and fee fields and the send to many grid, instead of truncating them in the payment --- .../sparrow/control/CoinAxisFormatter.java | 4 +-- .../sparrow/control/CoinTextFormatter.java | 30 ++++++++++++------- .../sparrow/control/SendToManyDialog.java | 29 +++++++++++------- .../sparrow/wallet/PaymentController.java | 7 +++-- .../sparrow/wallet/SendController.java | 9 +++--- 5 files changed, 48 insertions(+), 31 deletions(-) diff --git a/src/main/java/com/sparrowwallet/sparrow/control/CoinAxisFormatter.java b/src/main/java/com/sparrowwallet/sparrow/control/CoinAxisFormatter.java index 48a5f254..4e640bd1 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/CoinAxisFormatter.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/CoinAxisFormatter.java @@ -24,13 +24,13 @@ final class CoinAxisFormatter extends StringConverter { } Double value = bitcoinUnit.getValue(object.longValue()); - return new CoinTextFormatter(unitFormat).getCoinFormat().format(value); + return new CoinTextFormatter(unitFormat, bitcoinUnit).getCoinFormat().format(value); } @Override public Number fromString(String string) { try { - Number number = new CoinTextFormatter(unitFormat).getCoinFormat().parse(string); + Number number = new CoinTextFormatter(unitFormat, bitcoinUnit).getCoinFormat().parse(string); return bitcoinUnit.getSatsValue(number.doubleValue()); } catch (ParseException e) { throw new RuntimeException(e); diff --git a/src/main/java/com/sparrowwallet/sparrow/control/CoinTextFormatter.java b/src/main/java/com/sparrowwallet/sparrow/control/CoinTextFormatter.java index b037e60f..9de1f1f5 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/CoinTextFormatter.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/CoinTextFormatter.java @@ -1,5 +1,6 @@ package com.sparrowwallet.sparrow.control; +import com.sparrowwallet.drongo.BitcoinUnit; import com.sparrowwallet.sparrow.UnitFormat; import javafx.scene.control.TextFormatter; import javafx.scene.control.TextInputControl; @@ -11,8 +12,8 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; public class CoinTextFormatter extends TextFormatter { - public CoinTextFormatter(UnitFormat unitFormat) { - super(new CoinFilter(unitFormat == null ? UnitFormat.DOT : unitFormat)); + public CoinTextFormatter(UnitFormat unitFormat, BitcoinUnit bitcoinUnit) { + super(new CoinFilter(unitFormat == null ? UnitFormat.DOT : unitFormat, bitcoinUnit)); } public UnitFormat getUnitFormat() { @@ -27,11 +28,16 @@ public class CoinTextFormatter extends TextFormatter { private final UnitFormat unitFormat; private final DecimalFormat coinFormat; private final Pattern coinValidation; + private final Pattern anyPrecisionAmount; - public CoinFilter(UnitFormat unitFormat) { + public CoinFilter(UnitFormat unitFormat, BitcoinUnit bitcoinUnit) { this.unitFormat = unitFormat; this.coinFormat = new DecimalFormat("###,###.########", unitFormat.getDecimalFormatSymbols()); - this.coinValidation = Pattern.compile("[\\d" + Pattern.quote(unitFormat.getGroupingSeparator()) + "]*(" + Pattern.quote(unitFormat.getDecimalSeparator()) + "\\d{0,8})?"); + String integer = "[\\d" + Pattern.quote(unitFormat.getGroupingSeparator()) + "]*"; + //A satoshi is indivisible, so a sats amount has no fractional part to validate + String fraction = bitcoinUnit == BitcoinUnit.SATOSHIS ? "" : "(" + Pattern.quote(unitFormat.getDecimalSeparator()) + "\\d{0,8})?"; + this.coinValidation = Pattern.compile(integer + fraction); + this.anyPrecisionAmount = Pattern.compile(integer + "(" + Pattern.quote(unitFormat.getDecimalSeparator()) + "\\d*)?"); } @Override @@ -51,12 +57,13 @@ public class CoinTextFormatter extends TextFormatter { commasRemoved = newText.length() - noFractionCommaText.length(); } - Matcher matcher = coinValidation.matcher(noFractionCommaText); - boolean validAmount = matcher.matches(); + boolean validAmount = coinValidation.matcher(noFractionCommaText).matches(); if(!validAmount) { - matcher.reset(); - if(matcher.find()) { - noFractionCommaText = matcher.group(); + //The amount a pasted text starts with is taken, unless it is more precise than the unit allows - that is ignored rather than truncated, + //so a digit typed beyond the last place leaves the field as it was + Matcher leadingAmount = anyPrecisionAmount.matcher(noFractionCommaText); + if(leadingAmount.find() && coinValidation.matcher(leadingAmount.group()).matches()) { + noFractionCommaText = leadingAmount.group(); } else { return null; } @@ -78,12 +85,13 @@ public class CoinTextFormatter extends TextFormatter { Number value = coinFormat.parse(noFractionCommaText); String correct = coinFormat.format(value.doubleValue()); + //Trailing fractional zeros and a trailing separator are left as typed so the fraction can still be entered, but only where the entire text is a valid amount String compare = newText; - if(compare.contains(unitFormat.getDecimalSeparator()) && compare.endsWith("0")) { + if(validAmount && compare.contains(unitFormat.getDecimalSeparator()) && compare.endsWith("0")) { compare = compare.replaceAll("0*$", ""); } - if(compare.endsWith(unitFormat.getDecimalSeparator())) { + if(validAmount && compare.endsWith(unitFormat.getDecimalSeparator())) { compare = compare.substring(0, compare.length() - 1); } diff --git a/src/main/java/com/sparrowwallet/sparrow/control/SendToManyDialog.java b/src/main/java/com/sparrowwallet/sparrow/control/SendToManyDialog.java index ce611257..cdc663f0 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/SendToManyDialog.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/SendToManyDialog.java @@ -59,7 +59,7 @@ public class SendToManyDialog extends Dialog> { this.wallet = wallet; this.bitcoinUnit = bitcoinUnit; this.unitFormat = unitFormat == null ? UnitFormat.DOT : unitFormat; - this.amountCellType = new UnitFormatDoubleCellType(this.unitFormat); + this.amountCellType = new UnitFormatDoubleCellType(this.unitFormat, bitcoinUnit); final DialogPane dialogPane = new SendToManyDialogPane(); setDialogPane(dialogPane); @@ -373,10 +373,12 @@ public class SendToManyDialog extends Dialog> { private static class UnitFormatDoubleCellType extends SpreadsheetCellType { private final UnitFormat unitFormat; + private final BitcoinUnit bitcoinUnit; - UnitFormatDoubleCellType(UnitFormat unitFormat) { - super(new UnitFormatDoubleConverter(unitFormat)); + UnitFormatDoubleCellType(UnitFormat unitFormat, BitcoinUnit bitcoinUnit) { + super(new UnitFormatDoubleConverter(unitFormat, bitcoinUnit)); this.unitFormat = unitFormat; + this.bitcoinUnit = bitcoinUnit; } @Override @@ -392,7 +394,7 @@ public class SendToManyDialog extends Dialog> { @Override public SpreadsheetCellEditor createEditor(SpreadsheetView view) { - return new UnitFormatDoubleEditor(view, unitFormat); + return new UnitFormatDoubleEditor(view, unitFormat, bitcoinUnit); } @Override @@ -432,9 +434,11 @@ public class SendToManyDialog extends Dialog> { private static class UnitFormatDoubleConverter extends StringConverterWithFormat { private final UnitFormat unitFormat; + private final BitcoinUnit bitcoinUnit; - UnitFormatDoubleConverter(UnitFormat unitFormat) { + UnitFormatDoubleConverter(UnitFormat unitFormat, BitcoinUnit bitcoinUnit) { this.unitFormat = unitFormat; + this.bitcoinUnit = bitcoinUnit; } @Override @@ -442,11 +446,14 @@ public class SendToManyDialog extends Dialog> { if(str == null || str.isEmpty()) { return null; } - String normalised = str.trim() - .replaceAll(Pattern.quote(unitFormat.getGroupingSeparator()), "") - .replaceAll(Pattern.quote(unitFormat.getDecimalSeparator()), "."); + String groupingStripped = str.trim().replaceAll(Pattern.quote(unitFormat.getGroupingSeparator()), ""); try { - return Double.valueOf(normalised); + //A sats amount with a fraction is not read, as in a CSV import, rather than truncated when paid - a paste of it clears the cell like any text that is not an amount + if(bitcoinUnit == BitcoinUnit.SATOSHIS) { + return (double)Long.parseLong(groupingStripped); + } + + return Double.valueOf(groupingStripped.replaceAll(Pattern.quote(unitFormat.getDecimalSeparator()), ".")); } catch(NumberFormatException e) { return null; } @@ -473,11 +480,11 @@ public class SendToManyDialog extends Dialog> { private final UnitFormat unitFormat; private final TextField textField; - UnitFormatDoubleEditor(SpreadsheetView view, UnitFormat unitFormat) { + UnitFormatDoubleEditor(SpreadsheetView view, UnitFormat unitFormat, BitcoinUnit bitcoinUnit) { super(view); this.unitFormat = unitFormat; this.textField = new TextField(); - this.textField.setTextFormatter(new CoinTextFormatter(unitFormat)); + this.textField.setTextFormatter(new CoinTextFormatter(unitFormat, bitcoinUnit)); } @Override diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/PaymentController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/PaymentController.java index 33eec388..456432a9 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/PaymentController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/PaymentController.java @@ -410,12 +410,13 @@ public class PaymentController extends WalletFormController implements Initializ sendController.updateTransaction(); }); - amount.setTextFormatter(new CoinTextFormatter(Config.get().getUnitFormat())); + amountUnit.getSelectionModel().select(BitcoinUnit.BTC.equals(sendController.getBitcoinUnit(Config.get().getBitcoinUnit())) ? 0 : 1); + amount.setTextFormatter(new CoinTextFormatter(Config.get().getUnitFormat(), amountUnit.getValue())); amount.textProperty().addListener(amountListener); - amountUnit.getSelectionModel().select(BitcoinUnit.BTC.equals(sendController.getBitcoinUnit(Config.get().getBitcoinUnit())) ? 0 : 1); amountUnit.valueProperty().addListener((observable, oldValue, newValue) -> { Long value = getRecipientValueSats(oldValue); + amount.setTextFormatter(new CoinTextFormatter(Config.get().getUnitFormat(), newValue)); if(value != null) { UnitFormat unitFormat = Config.get().getUnitFormat() == null ? UnitFormat.DOT : Config.get().getUnitFormat(); DecimalFormat df = new DecimalFormat("#.#", unitFormat.getDecimalFormatSymbols()); @@ -939,7 +940,7 @@ public class PaymentController extends WalletFormController implements Initializ public void unitFormatChanged(UnitFormatChangedEvent event) { if(amount.getTextFormatter() instanceof CoinTextFormatter coinTextFormatter && coinTextFormatter.getUnitFormat() != event.getUnitFormat()) { Long value = getRecipientValueSats(coinTextFormatter.getUnitFormat(), amountUnit.getSelectionModel().getSelectedItem()); - amount.setTextFormatter(new CoinTextFormatter(event.getUnitFormat())); + amount.setTextFormatter(new CoinTextFormatter(event.getUnitFormat(), amountUnit.getValue())); if(value != null) { setRecipientValueSats(value); diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java index 2db24e0e..41f08e3f 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java @@ -362,13 +362,14 @@ public class SendController extends WalletFormController implements Initializabl }; }); - fee.setTextFormatter(new CoinTextFormatter(Config.get().getUnitFormat())); - fee.textProperty().addListener(feeListener); - BitcoinUnit unit = getBitcoinUnit(Config.get().getBitcoinUnit()); feeAmountUnit.getSelectionModel().select(BitcoinUnit.BTC.equals(unit) ? 0 : 1); + fee.setTextFormatter(new CoinTextFormatter(Config.get().getUnitFormat(), feeAmountUnit.getValue())); + fee.textProperty().addListener(feeListener); + feeAmountUnit.valueProperty().addListener((observable, oldValue, newValue) -> { Long value = getFeeValueSats(oldValue); + fee.setTextFormatter(new CoinTextFormatter(Config.get().getUnitFormat(), newValue)); if(value != null) { setFeeValueSats(value); } @@ -1568,7 +1569,7 @@ public class SendController extends WalletFormController implements Initializabl setFeeRate(getFeeRate()); if(fee.getTextFormatter() instanceof CoinTextFormatter coinTextFormatter && coinTextFormatter.getUnitFormat() != event.getUnitFormat()) { Long value = getFeeValueSats(coinTextFormatter.getUnitFormat(), feeAmountUnit.getSelectionModel().getSelectedItem()); - fee.setTextFormatter(new CoinTextFormatter(event.getUnitFormat())); + fee.setTextFormatter(new CoinTextFormatter(event.getUnitFormat(), feeAmountUnit.getValue())); if(value != null) { setFeeValueSats(value);