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

This commit is contained in:
Craig Raw
2026-09-16 11:07:42 +02:00
parent 1fb4e8bb48
commit 6cde97adea
5 changed files with 48 additions and 31 deletions
@@ -24,13 +24,13 @@ final class CoinAxisFormatter extends StringConverter<Number> {
}
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);
@@ -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<String> {
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<String> {
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<String> {
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<String> {
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);
}
@@ -59,7 +59,7 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
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<List<Payment>> {
private static class UnitFormatDoubleCellType extends SpreadsheetCellType<Double> {
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<List<Payment>> {
@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<List<Payment>> {
private static class UnitFormatDoubleConverter extends StringConverterWithFormat<Double> {
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<List<Payment>> {
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<List<Payment>> {
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
@@ -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);
@@ -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);