improve validation and focus handling in integer spinners

This commit is contained in:
Craig Raw 2022-05-17 10:52:43 +02:00
parent 766a8c267f
commit c51f3d9e66
9 changed files with 128 additions and 22 deletions

View file

@ -0,0 +1,83 @@
package com.sparrowwallet.sparrow.control;
import javafx.beans.NamedArg;
import javafx.scene.control.Spinner;
import javafx.scene.control.SpinnerValueFactory;
import javafx.util.converter.IntegerStringConverter;
public class IntegerSpinner extends Spinner<Integer> {
public IntegerSpinner() {
super();
setupEditor();
}
public IntegerSpinner(@NamedArg("min") int min,
@NamedArg("max") int max,
@NamedArg("initialValue") int initialValue) {
super(min, max, initialValue);
setupEditor();
}
public IntegerSpinner(@NamedArg("min") int min,
@NamedArg("max") int max,
@NamedArg("initialValue") int initialValue,
@NamedArg("amountToStepBy") int amountToStepBy) {
super(min, max, initialValue, amountToStepBy);
setupEditor();
}
private void setupEditor() {
getEditor().focusedProperty().addListener((observable, oldValue, newValue) -> {
if(newValue != null && !newValue) {
commitValue();
}
});
getEditor().textProperty().addListener((observable, oldValue, newValue) -> {
if(!newValue.matches("\\d*")) {
getEditor().setText(newValue.replaceAll("[^\\d]", ""));
}
});
}
public static class ValueFactory extends SpinnerValueFactory.IntegerSpinnerValueFactory {
public ValueFactory(@NamedArg("min") int min,
@NamedArg("max") int max) {
super(min, max);
setupConverter(min);
}
public ValueFactory(@NamedArg("min") int min,
@NamedArg("max") int max,
@NamedArg("initialValue") int initialValue) {
super(min, max, initialValue);
setupConverter(initialValue);
}
public ValueFactory(@NamedArg("min") int min,
@NamedArg("max") int max,
@NamedArg("initialValue") int initialValue,
@NamedArg("amountToStepBy") int amountToStepBy) {
super(min, max, initialValue, amountToStepBy);
setupConverter(initialValue);
}
private void setupConverter(Integer defaultValue) {
setConverter(new IntegerStringConverter() {
@Override
public Integer fromString(String value) {
if(value == null) {
return null;
}
value = value.trim();
if(value.length() < 1) {
return defaultValue;
}
return Integer.valueOf(value);
}
});
}
}
}

View file

@ -82,7 +82,7 @@ public class HeadersController extends TransactionFormController implements Init
private TransactionDiagram transactionDiagram; private TransactionDiagram transactionDiagram;
@FXML @FXML
private Spinner<Integer> version; private IntegerSpinner version;
@FXML @FXML
private CopyableLabel segwit; private CopyableLabel segwit;
@ -112,10 +112,10 @@ public class HeadersController extends TransactionFormController implements Init
private Field locktimeDateField; private Field locktimeDateField;
@FXML @FXML
private Spinner<Integer> locktimeNone; private IntegerSpinner locktimeNone;
@FXML @FXML
private Spinner<Integer> locktimeBlock; private IntegerSpinner locktimeBlock;
@FXML @FXML
private Hyperlink locktimeCurrentHeight; private Hyperlink locktimeCurrentHeight;
@ -243,8 +243,12 @@ public class HeadersController extends TransactionFormController implements Init
updateTxId(); updateTxId();
version.setValueFactory(new SpinnerValueFactory.IntegerSpinnerValueFactory(1, 2, (int)tx.getVersion())); version.setValueFactory(new IntegerSpinner.ValueFactory(1, 2, (int)tx.getVersion()));
version.valueProperty().addListener((obs, oldValue, newValue) -> { version.valueProperty().addListener((obs, oldValue, newValue) -> {
if(newValue == null || newValue < 1 || newValue > 2) {
return;
}
tx.setVersion(newValue); tx.setVersion(newValue);
if(oldValue != null) { if(oldValue != null) {
EventManager.get().post(new TransactionChangedEvent(tx)); EventManager.get().post(new TransactionChangedEvent(tx));
@ -305,9 +309,9 @@ public class HeadersController extends TransactionFormController implements Init
futureDateWarning.managedProperty().bind(futureDateWarning.visibleProperty()); futureDateWarning.managedProperty().bind(futureDateWarning.visibleProperty());
futureDateWarning.setVisible(false); futureDateWarning.setVisible(false);
locktimeNone.setValueFactory(new SpinnerValueFactory.IntegerSpinnerValueFactory(0, (int)Transaction.MAX_BLOCK_LOCKTIME-1, 0)); locktimeNone.setValueFactory(new IntegerSpinner.ValueFactory(0, (int)Transaction.MAX_BLOCK_LOCKTIME-1, 0));
if(tx.getLocktime() < Transaction.MAX_BLOCK_LOCKTIME) { if(tx.getLocktime() < Transaction.MAX_BLOCK_LOCKTIME) {
locktimeBlock.setValueFactory(new SpinnerValueFactory.IntegerSpinnerValueFactory(0, (int)Transaction.MAX_BLOCK_LOCKTIME-1, (int)tx.getLocktime())); locktimeBlock.setValueFactory(new IntegerSpinner.ValueFactory(0, (int)Transaction.MAX_BLOCK_LOCKTIME-1, (int)tx.getLocktime()));
if(tx.getLocktime() == 0) { if(tx.getLocktime() == 0) {
locktimeToggleGroup.selectToggle(locktimeNoneType); locktimeToggleGroup.selectToggle(locktimeNoneType);
} else { } else {
@ -316,13 +320,17 @@ public class HeadersController extends TransactionFormController implements Init
LocalDateTime date = Instant.now().atZone(ZoneId.systemDefault()).toLocalDateTime(); LocalDateTime date = Instant.now().atZone(ZoneId.systemDefault()).toLocalDateTime();
locktimeDate.setDateTimeValue(date); locktimeDate.setDateTimeValue(date);
} else { } else {
locktimeBlock.setValueFactory(new SpinnerValueFactory.IntegerSpinnerValueFactory(0, (int)Transaction.MAX_BLOCK_LOCKTIME-1)); locktimeBlock.setValueFactory(new IntegerSpinner.ValueFactory(0, (int)Transaction.MAX_BLOCK_LOCKTIME-1));
LocalDateTime date = Instant.ofEpochSecond(tx.getLocktime()).atZone(ZoneId.systemDefault()).toLocalDateTime(); LocalDateTime date = Instant.ofEpochSecond(tx.getLocktime()).atZone(ZoneId.systemDefault()).toLocalDateTime();
locktimeDate.setDateTimeValue(date); locktimeDate.setDateTimeValue(date);
locktimeToggleGroup.selectToggle(locktimeDateType); locktimeToggleGroup.selectToggle(locktimeDateType);
} }
locktimeBlock.valueProperty().addListener((obs, oldValue, newValue) -> { locktimeBlock.valueProperty().addListener((obs, oldValue, newValue) -> {
if(newValue == null || newValue < 0 || newValue >= Transaction.MAX_BLOCK_LOCKTIME) {
return;
}
tx.setLocktime(newValue); tx.setLocktime(newValue);
locktimeCurrentHeight.setVisible(headersForm.isEditable() && AppServices.getCurrentBlockHeight() != null && newValue < AppServices.getCurrentBlockHeight()); locktimeCurrentHeight.setVisible(headersForm.isEditable() && AppServices.getCurrentBlockHeight() != null && newValue < AppServices.getCurrentBlockHeight());
futureBlockWarning.setVisible(AppServices.getCurrentBlockHeight() != null && newValue > AppServices.getCurrentBlockHeight()); futureBlockWarning.setVisible(AppServices.getCurrentBlockHeight() != null && newValue > AppServices.getCurrentBlockHeight());

View file

@ -104,7 +104,7 @@ public class InputController extends TransactionFormController implements Initia
private CopyableLabel locktimeAbsolute; private CopyableLabel locktimeAbsolute;
@FXML @FXML
private Spinner<Integer> locktimeRelativeBlocks; private IntegerSpinner locktimeRelativeBlocks;
@FXML @FXML
private RelativeTimelockSpinner locktimeRelativeSeconds; private RelativeTimelockSpinner locktimeRelativeSeconds;
@ -406,7 +406,7 @@ public class InputController extends TransactionFormController implements Initia
} }
}); });
locktimeRelativeBlocks.setValueFactory(new SpinnerValueFactory.IntegerSpinnerValueFactory(0, (int)TransactionInput.RELATIVE_TIMELOCK_VALUE_MASK, 0)); locktimeRelativeBlocks.setValueFactory(new IntegerSpinner.ValueFactory(0, (int)TransactionInput.RELATIVE_TIMELOCK_VALUE_MASK, 0));
locktimeRelativeBlocks.managedProperty().bind(locktimeRelativeBlocks.visibleProperty()); locktimeRelativeBlocks.managedProperty().bind(locktimeRelativeBlocks.visibleProperty());
locktimeRelativeSeconds.managedProperty().bind(locktimeRelativeSeconds.visibleProperty()); locktimeRelativeSeconds.managedProperty().bind(locktimeRelativeSeconds.visibleProperty());
locktimeRelativeCombo.getSelectionModel().selectedItemProperty().addListener((ov, old_toggle, new_toggle) -> { locktimeRelativeCombo.getSelectionModel().selectedItemProperty().addListener((ov, old_toggle, new_toggle) -> {
@ -433,6 +433,10 @@ public class InputController extends TransactionFormController implements Initia
} }
locktimeRelativeBlocks.valueProperty().addListener((obs, oldValue, newValue) -> { locktimeRelativeBlocks.valueProperty().addListener((obs, oldValue, newValue) -> {
if(newValue == null || newValue < 0 || newValue > TransactionInput.RELATIVE_TIMELOCK_VALUE_MASK) {
return;
}
setRelativeLocktime(txInput, transaction, oldValue != null); setRelativeLocktime(txInput, transaction, oldValue != null);
}); });
locktimeRelativeSeconds.valueProperty().addListener((obs, oldValue, newValue) -> { locktimeRelativeSeconds.valueProperty().addListener((obs, oldValue, newValue) -> {

View file

@ -3,6 +3,7 @@ package com.sparrowwallet.sparrow.wallet;
import com.sparrowwallet.drongo.wallet.Wallet; import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.control.DateStringConverter; import com.sparrowwallet.sparrow.control.DateStringConverter;
import com.sparrowwallet.sparrow.control.IntegerSpinner;
import com.sparrowwallet.sparrow.event.SettingsChangedEvent; import com.sparrowwallet.sparrow.event.SettingsChangedEvent;
import javafx.collections.FXCollections; import javafx.collections.FXCollections;
import javafx.collections.ObservableList; import javafx.collections.ObservableList;
@ -10,8 +11,6 @@ import javafx.fxml.FXML;
import javafx.fxml.Initializable; import javafx.fxml.Initializable;
import javafx.scene.control.ComboBox; import javafx.scene.control.ComboBox;
import javafx.scene.control.DatePicker; import javafx.scene.control.DatePicker;
import javafx.scene.control.Spinner;
import javafx.scene.control.SpinnerValueFactory;
import javafx.util.StringConverter; import javafx.util.StringConverter;
import java.net.URL; import java.net.URL;
@ -28,7 +27,7 @@ public class AdvancedController implements Initializable {
private DatePicker birthDate; private DatePicker birthDate;
@FXML @FXML
private Spinner<Integer> gapLimit; private IntegerSpinner gapLimit;
@FXML @FXML
private ComboBox<Integer> watchLast; private ComboBox<Integer> watchLast;
@ -50,8 +49,12 @@ public class AdvancedController implements Initializable {
} }
}); });
gapLimit.setValueFactory(new SpinnerValueFactory.IntegerSpinnerValueFactory(Wallet.DEFAULT_LOOKAHEAD, 9999, wallet.getGapLimit())); gapLimit.setValueFactory(new IntegerSpinner.ValueFactory(Wallet.DEFAULT_LOOKAHEAD, 9999, wallet.getGapLimit()));
gapLimit.valueProperty().addListener((observable, oldValue, newValue) -> { gapLimit.valueProperty().addListener((observable, oldValue, newValue) -> {
if(newValue == null || newValue < Wallet.DEFAULT_LOOKAHEAD || newValue > 9999) {
return;
}
wallet.setGapLimit(newValue); wallet.setGapLimit(newValue);
if(!watchLast.getItems().equals(getWatchListItems(wallet))) { if(!watchLast.getItems().equals(getWatchListItems(wallet))) {
Integer value = watchLast.getValue(); Integer value = watchLast.getValue();

View file

@ -7,6 +7,7 @@ import com.sparrowwallet.drongo.wallet.StandardAccount;
import com.sparrowwallet.drongo.wallet.Wallet; import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.AppServices; import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.control.IntegerSpinner;
import com.sparrowwallet.sparrow.event.MixToConfigChangedEvent; import com.sparrowwallet.sparrow.event.MixToConfigChangedEvent;
import com.sparrowwallet.sparrow.whirlpool.Whirlpool; import com.sparrowwallet.sparrow.whirlpool.Whirlpool;
import javafx.collections.FXCollections; import javafx.collections.FXCollections;
@ -31,7 +32,7 @@ public class MixToController implements Initializable {
private ComboBox<Wallet> mixToWallets; private ComboBox<Wallet> mixToWallets;
@FXML @FXML
private Spinner<Integer> minMixes; private IntegerSpinner minMixes;
@FXML @FXML
private ComboBox<IndexRange> indexRange; private ComboBox<IndexRange> indexRange;
@ -94,8 +95,12 @@ public class MixToController implements Initializable {
}); });
int initialMinMixes = mixConfig.getMinMixes() == null ? Whirlpool.DEFAULT_MIXTO_MIN_MIXES : mixConfig.getMinMixes(); int initialMinMixes = mixConfig.getMinMixes() == null ? Whirlpool.DEFAULT_MIXTO_MIN_MIXES : mixConfig.getMinMixes();
minMixes.setValueFactory(new SpinnerValueFactory.IntegerSpinnerValueFactory(1, 10000, initialMinMixes)); minMixes.setValueFactory(new IntegerSpinner.ValueFactory(1, 10000, initialMinMixes));
minMixes.valueProperty().addListener((observable, oldValue, newValue) -> { minMixes.valueProperty().addListener((observable, oldValue, newValue) -> {
if(newValue == null || newValue < 1 || newValue > 10000) {
return;
}
mixConfig.setMinMixes(newValue); mixConfig.setMinMixes(newValue);
EventManager.get().post(new MixToConfigChangedEvent(wallet)); EventManager.get().post(new MixToConfigChangedEvent(wallet));
}); });

View file

@ -28,6 +28,7 @@
<?import javafx.scene.control.TabPane?> <?import javafx.scene.control.TabPane?>
<?import javafx.scene.control.Tab?> <?import javafx.scene.control.Tab?>
<?import com.sparrowwallet.sparrow.control.TransactionDiagram?> <?import com.sparrowwallet.sparrow.control.TransactionDiagram?>
<?import com.sparrowwallet.sparrow.control.IntegerSpinner?>
<GridPane hgap="10.0" vgap="10.0" styleClass="tx-pane" xmlns="http://javafx.com/javafx/10.0.2-internal" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.sparrowwallet.sparrow.transaction.HeadersController" stylesheets="@headers.css, @transaction.css, @../general.css"> <GridPane hgap="10.0" vgap="10.0" styleClass="tx-pane" xmlns="http://javafx.com/javafx/10.0.2-internal" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.sparrowwallet.sparrow.transaction.HeadersController" stylesheets="@headers.css, @transaction.css, @../general.css">
<padding> <padding>
@ -71,7 +72,7 @@
<Form GridPane.columnIndex="0" GridPane.rowIndex="0"> <Form GridPane.columnIndex="0" GridPane.rowIndex="0">
<Fieldset text="Headers" inputGrow="SOMETIMES"> <Fieldset text="Headers" inputGrow="SOMETIMES">
<Field text="Version:"> <Field text="Version:">
<Spinner fx:id="version" prefWidth="60" editable="true" /> <IntegerSpinner fx:id="version" prefWidth="60" editable="true" />
</Field> </Field>
<Field text="Type:"> <Field text="Type:">
<CopyableLabel fx:id="segwit" /> <CopyableLabel fx:id="segwit" />
@ -94,10 +95,10 @@
</SegmentedButton> </SegmentedButton>
</Field> </Field>
<Field fx:id="locktimeNoneField" text="Block:"> <Field fx:id="locktimeNoneField" text="Block:">
<Spinner fx:id="locktimeNone" disable="true" prefWidth="120"/> <IntegerSpinner fx:id="locktimeNone" disable="true" prefWidth="120"/>
</Field> </Field>
<Field fx:id="locktimeBlockField" text="Block:"> <Field fx:id="locktimeBlockField" text="Block:">
<Spinner fx:id="locktimeBlock" editable="true" max="499999999" prefWidth="120"/> <IntegerSpinner fx:id="locktimeBlock" editable="true" max="499999999" prefWidth="120"/>
<Hyperlink fx:id="locktimeCurrentHeight" text="Set current height" onAction="#setLocktimeToCurrentHeight" /> <Hyperlink fx:id="locktimeCurrentHeight" text="Set current height" onAction="#setLocktimeToCurrentHeight" />
<Label fx:id="futureBlockWarning"> <Label fx:id="futureBlockWarning">
<graphic> <graphic>

View file

@ -17,6 +17,7 @@
<?import com.sparrowwallet.sparrow.control.AddressLabel?> <?import com.sparrowwallet.sparrow.control.AddressLabel?>
<?import com.sparrowwallet.sparrow.control.UnlabeledToggleSwitch?> <?import com.sparrowwallet.sparrow.control.UnlabeledToggleSwitch?>
<?import com.sparrowwallet.sparrow.control.ScriptArea?> <?import com.sparrowwallet.sparrow.control.ScriptArea?>
<?import com.sparrowwallet.sparrow.control.IntegerSpinner?>
<GridPane hgap="10.0" vgap="10.0" styleClass="tx-pane" stylesheets="@input.css, @transaction.css, @../script.css, @../general.css" xmlns="http://javafx.com/javafx/10.0.2-internal" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.sparrowwallet.sparrow.transaction.InputController"> <GridPane hgap="10.0" vgap="10.0" styleClass="tx-pane" stylesheets="@input.css, @transaction.css, @../script.css, @../general.css" xmlns="http://javafx.com/javafx/10.0.2-internal" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.sparrowwallet.sparrow.transaction.InputController">
<padding> <padding>
@ -122,7 +123,7 @@
<CopyableLabel fx:id="locktimeAbsolute" /> <CopyableLabel fx:id="locktimeAbsolute" />
</Field> </Field>
<Field fx:id="locktimeRelativeField" text="Value:"> <Field fx:id="locktimeRelativeField" text="Value:">
<Spinner fx:id="locktimeRelativeBlocks" editable="true" prefWidth="110"/> <IntegerSpinner fx:id="locktimeRelativeBlocks" editable="true" prefWidth="110"/>
<RelativeTimelockSpinner fx:id="locktimeRelativeSeconds" editable="true" prefWidth="110"/> <RelativeTimelockSpinner fx:id="locktimeRelativeSeconds" editable="true" prefWidth="110"/>
<ComboBox fx:id="locktimeRelativeCombo"> <ComboBox fx:id="locktimeRelativeCombo">
<items> <items>

View file

@ -10,6 +10,7 @@
<?import tornadofx.control.Field?> <?import tornadofx.control.Field?>
<?import com.sparrowwallet.sparrow.control.HelpLabel?> <?import com.sparrowwallet.sparrow.control.HelpLabel?>
<?import javafx.geometry.Insets?> <?import javafx.geometry.Insets?>
<?import com.sparrowwallet.sparrow.control.IntegerSpinner?>
<BorderPane stylesheets="@../general.css" styleClass="line-border" xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml" fx:controller="com.sparrowwallet.sparrow.wallet.AdvancedController"> <BorderPane stylesheets="@../general.css" styleClass="line-border" xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml" fx:controller="com.sparrowwallet.sparrow.wallet.AdvancedController">
<center> <center>
@ -31,7 +32,7 @@
<HelpLabel helpText="The date of the earliest transaction (used to avoid scanning the entire blockchain)."/> <HelpLabel helpText="The date of the earliest transaction (used to avoid scanning the entire blockchain)."/>
</Field> </Field>
<Field text="Gap limit:"> <Field text="Gap limit:">
<Spinner fx:id="gapLimit" editable="true" prefWidth="90" /> <IntegerSpinner fx:id="gapLimit" editable="true" prefWidth="90" />
<HelpLabel helpText="Change how far ahead to look for additional transactions beyond the highest derivation with previous transaction outputs."/> <HelpLabel helpText="Change how far ahead to look for additional transactions beyond the highest derivation with previous transaction outputs."/>
</Field> </Field>
<Field text="Watch addresses:"> <Field text="Watch addresses:">

View file

@ -11,8 +11,8 @@
<?import javafx.geometry.Insets?> <?import javafx.geometry.Insets?>
<?import javafx.collections.FXCollections?> <?import javafx.collections.FXCollections?>
<?import javafx.scene.control.ComboBox?> <?import javafx.scene.control.ComboBox?>
<?import javafx.scene.control.Spinner?>
<?import com.samourai.whirlpool.client.wallet.beans.IndexRange?> <?import com.samourai.whirlpool.client.wallet.beans.IndexRange?>
<?import com.sparrowwallet.sparrow.control.IntegerSpinner?>
<BorderPane stylesheets="@../general.css" styleClass="line-border" xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml" fx:controller="com.sparrowwallet.sparrow.wallet.MixToController"> <BorderPane stylesheets="@../general.css" styleClass="line-border" xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml" fx:controller="com.sparrowwallet.sparrow.wallet.MixToController">
<center> <center>
@ -34,7 +34,7 @@
<HelpLabel helpText="Select an open wallet to mix to."/> <HelpLabel helpText="Select an open wallet to mix to."/>
</Field> </Field>
<Field text="Minimum mixes:"> <Field text="Minimum mixes:">
<Spinner fx:id="minMixes" editable="true" prefWidth="90" /> <IntegerSpinner fx:id="minMixes" editable="true" prefWidth="90" />
<HelpLabel helpText="The minimum number of mixes required before mixing to the selected wallet.\nTo ensure privacy, each mix beyond this number will have a 75% probability to mix to the selected wallet."/> <HelpLabel helpText="The minimum number of mixes required before mixing to the selected wallet.\nTo ensure privacy, each mix beyond this number will have a 75% probability to mix to the selected wallet."/>
</Field> </Field>
</Fieldset> </Fieldset>