add relative timelock spinner

This commit is contained in:
Craig Raw 2020-04-07 11:30:36 +02:00
parent 731be1a60c
commit 924e7d6b45
3 changed files with 262 additions and 9 deletions

View file

@ -0,0 +1,254 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.protocol.TransactionInput;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.control.Spinner;
import javafx.scene.control.SpinnerValueFactory;
import javafx.scene.control.TextFormatter;
import javafx.scene.input.InputEvent;
import javafx.util.StringConverter;
import java.time.Duration;
public class RelativeTimelockSpinner extends Spinner<Duration> {
// Mode represents the unit that is currently being edited.
// For convenience expose methods for incrementing and decrementing that
// unit, and for selecting the appropriate portion in a spinner's editor
enum Mode {
DAYS {
@Override
Duration increment(Duration time, int steps) {
return checkBounds(time, time.plusDays(steps));
}
@Override
void select(RelativeTimelockSpinner spinner) {
int index = spinner.getEditor().getText().indexOf('d');
spinner.getEditor().selectRange(0, index);
}
},
HOURS {
@Override
Duration increment(Duration time, int steps) {
return checkBounds(time, time.plusHours(steps));
}
@Override
void select(RelativeTimelockSpinner spinner) {
int dayIndex = spinner.getEditor().getText().indexOf('d');
int start = dayIndex > -1 ? dayIndex + 2 : 0;
int hrIndex = spinner.getEditor().getText().indexOf('h');
spinner.getEditor().selectRange(start, hrIndex);
}
},
MINUTES {
@Override
Duration increment(Duration time, int steps) {
return checkBounds(time, time.plusMinutes(steps*TransactionInput.RELATIVE_TIMELOCK_SECONDS_INCREMENT/60));
}
@Override
void select(RelativeTimelockSpinner spinner) {
int hrIndex = spinner.getEditor().getText().indexOf('h');
int start = hrIndex > -1 ? hrIndex + 2 : 0;
int minIndex = spinner.getEditor().getText().indexOf('m');
spinner.getEditor().selectRange(start, minIndex);
}
},
SECONDS {
@Override
Duration increment(Duration time, int steps) {
return time.plusSeconds(steps*TransactionInput.RELATIVE_TIMELOCK_SECONDS_INCREMENT);
}
@Override
void select(RelativeTimelockSpinner spinner) {
int index = spinner.getEditor().getText().indexOf('m');
spinner.getEditor().selectRange(index + 2, spinner.getEditor().getText().length() - 1);
}
};
abstract Duration increment(Duration time, int steps);
abstract void select(RelativeTimelockSpinner spinner);
Duration decrement(Duration time, int steps) {
return increment(time, -steps);
}
Duration checkBounds(Duration oldTime, Duration time) {
long totalSeconds = time.getSeconds();
if(totalSeconds < 0) {
return Duration.ZERO;
}
long maxSeconds = TransactionInput.RELATIVE_TIMELOCK_VALUE_MASK * TransactionInput.RELATIVE_TIMELOCK_SECONDS_INCREMENT;
if(totalSeconds > maxSeconds) {
return Duration.ofSeconds(maxSeconds);
}
return time;
}
}
// Property containing the current editing mode:
private final ObjectProperty<Mode> mode = new SimpleObjectProperty<>(Mode.MINUTES);
public ObjectProperty<Mode> modeProperty() {
return mode;
}
public final Mode getMode() {
return modeProperty().get();
}
public final void setMode(Mode mode) {
modeProperty().set(mode);
}
public RelativeTimelockSpinner(Duration time) {
setEditable(true);
StringConverter<Duration> localTimeConverter = new StringConverter<>() {
@Override
public String toString(Duration time) {
if(time.getSeconds()/86400 > 0) {
return time.getSeconds()/86400 + "d " + (time.getSeconds()/3600)%24 + "h";
} else if(time.getSeconds()/3600 > 0) {
return (time.getSeconds()/3600)%24 + "h " + (time.getSeconds()/60)%60 + "m";
} else {
return (time.getSeconds()/60)%60 + "m " + time.getSeconds()%60 + "s";
}
}
@Override
public Duration fromString(String string) {
long totalSeconds = getTotalSecondsFromString(string);
double rounded = Math.round((double)totalSeconds/TransactionInput.RELATIVE_TIMELOCK_SECONDS_INCREMENT);
long roundedSeconds = (long)rounded*TransactionInput.RELATIVE_TIMELOCK_SECONDS_INCREMENT;
Duration duration = Duration.ofSeconds(roundedSeconds);
if(totalSeconds > 86400) {
if(!string.equals(this.toString(duration))) {
if (roundedSeconds < totalSeconds) {
duration = duration.plusSeconds(TransactionInput.RELATIVE_TIMELOCK_SECONDS_INCREMENT);
} else {
duration = duration.minusSeconds(TransactionInput.RELATIVE_TIMELOCK_SECONDS_INCREMENT);
}
}
}
long maxSeconds = TransactionInput.RELATIVE_TIMELOCK_VALUE_MASK * TransactionInput.RELATIVE_TIMELOCK_SECONDS_INCREMENT;
if(roundedSeconds > maxSeconds) {
return Duration.ofSeconds(maxSeconds);
}
return duration;
}
private long getTotalSecondsFromString(String string) {
String[] tokens = string.split(" ");
int days = 0, hours = 0, minutes = 0, seconds = 0;
for(int i = 0; i < tokens.length; i++) {
int value = Integer.parseInt(tokens[i].substring(0, tokens[i].length()-1));
if(tokens[i].endsWith("d")) {
days = value;
} else if(tokens[i].endsWith("h")) {
hours = value;
} else if(tokens[i].endsWith("m")) {
minutes = value;
} else if(tokens[i].endsWith("s")) {
seconds = value;
}
}
return ((days * 24 + hours) * 60 + minutes) * 60 + seconds;
}
};
// The textFormatter both manages the text <-> LocalTime conversion,
// and vetoes any edits that are not valid. We just make sure we have
// two colons and only digits in between:
TextFormatter<Duration> textFormatter = new TextFormatter<Duration>(localTimeConverter, Duration.ZERO, c -> {
String newText = c.getControlNewText();
if(newText.matches("[0-9]{0,4}d [0-9]{0,2}h") ||
newText.matches("[0-9]{0,2}h [0-9]{0,2}m") ||
newText.matches("[0-9]{0,2}m [0-9]{0,2}s")) {
return c;
}
return null;
});
// The spinner value factory defines increment and decrement by
// delegating to the current editing mode:
SpinnerValueFactory<Duration> valueFactory = new SpinnerValueFactory<>() {
{
setConverter(localTimeConverter);
setValue(time);
}
@Override
public void decrement(int steps) {
checkMode();
setValue(mode.get().decrement(getValue(), steps));
mode.get().select(RelativeTimelockSpinner.this);
}
@Override
public void increment(int steps) {
checkMode();
setValue(mode.get().increment(getValue(), steps));
mode.get().select(RelativeTimelockSpinner.this);
}
private void checkMode() {
String text = RelativeTimelockSpinner.this.getEditor().getText();
if(mode.get() == Mode.DAYS && text.indexOf('d') < 0) {
RelativeTimelockSpinner.this.mode.set(Mode.HOURS);
} else if(mode.get() == Mode.HOURS && text.indexOf('h') < 0) {
RelativeTimelockSpinner.this.mode.set(Mode.MINUTES);
} else if(mode.get() == Mode.MINUTES && text.indexOf('m') < 0) {
RelativeTimelockSpinner.this.mode.set(Mode.HOURS);
} else if(mode.get() == Mode.SECONDS && text.indexOf('s') < 0) {
RelativeTimelockSpinner.this.mode.set(Mode.MINUTES);
}
}
};
this.setValueFactory(valueFactory);
this.getEditor().setTextFormatter(textFormatter);
// Update the mode when the user interacts with the editor.
// This is a bit of a hack, e.g. calling spinner.getEditor().positionCaret()
// could result in incorrect state. Directly observing the caretPostion
// didn't work well though; getting that to work properly might be
// a better approach in the long run.
this.getEditor().addEventHandler(InputEvent.ANY, e -> {
int caretPos = this.getEditor().getCaretPosition();
int dayIndex = this.getEditor().getText().indexOf('d');
int hrIndex = this.getEditor().getText().indexOf('h');
int minIndex = this.getEditor().getText().indexOf('m');
if(caretPos <= dayIndex) {
mode.set(Mode.DAYS);
} else if (caretPos <= hrIndex) {
mode.set(Mode.HOURS);
} else if (caretPos <= minIndex) {
mode.set(Mode.MINUTES);
} else {
mode.set(Mode.SECONDS);
}
});
// When the mode changes, select the new portion:
mode.addListener((obs, oldMode, newMode) -> newMode.select(this));
}
public RelativeTimelockSpinner() {
this(Duration.ZERO);
}
}

View file

@ -3,6 +3,7 @@ package com.sparrowwallet.sparrow.transaction;
import com.sparrowwallet.drongo.protocol.*; import com.sparrowwallet.drongo.protocol.*;
import com.sparrowwallet.drongo.psbt.PSBTInput; import com.sparrowwallet.drongo.psbt.PSBTInput;
import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.control.RelativeTimelockSpinner;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.fxml.Initializable; import javafx.fxml.Initializable;
import javafx.scene.control.*; import javafx.scene.control.*;
@ -13,6 +14,7 @@ import tornadofx.control.Fieldset;
import org.fxmisc.flowless.VirtualizedScrollPane; import org.fxmisc.flowless.VirtualizedScrollPane;
import java.net.URL; import java.net.URL;
import java.time.Duration;
import java.time.Instant; import java.time.Instant;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.ZoneId; import java.time.ZoneId;
@ -87,7 +89,7 @@ public class InputController extends TransactionFormController implements Initia
private Spinner<Integer> locktimeRelativeBlocks; private Spinner<Integer> locktimeRelativeBlocks;
@FXML @FXML
private Spinner<Integer> locktimeRelativeSeconds; private RelativeTimelockSpinner locktimeRelativeSeconds;
@FXML @FXML
private ComboBox<String> locktimeRelativeCombo; private ComboBox<String> locktimeRelativeCombo;
@ -217,10 +219,6 @@ public class InputController extends TransactionFormController implements Initia
}); });
locktimeRelativeBlocks.setValueFactory(new SpinnerValueFactory.IntegerSpinnerValueFactory(0, (int)TransactionInput.RELATIVE_TIMELOCK_VALUE_MASK, 0)); locktimeRelativeBlocks.setValueFactory(new SpinnerValueFactory.IntegerSpinnerValueFactory(0, (int)TransactionInput.RELATIVE_TIMELOCK_VALUE_MASK, 0));
locktimeRelativeSeconds.setValueFactory(new SpinnerValueFactory.IntegerSpinnerValueFactory(0,
(int)TransactionInput.RELATIVE_TIMELOCK_VALUE_MASK*TransactionInput.RELATIVE_TIMELOCK_SECONDS_INCREMENT, 0,
TransactionInput.RELATIVE_TIMELOCK_SECONDS_INCREMENT));
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) -> {
@ -240,7 +238,7 @@ public class InputController extends TransactionFormController implements Initia
locktimeRelativeBlocks.valueFactoryProperty().get().setValue((int)txInput.getRelativeLocktime()); locktimeRelativeBlocks.valueFactoryProperty().get().setValue((int)txInput.getRelativeLocktime());
locktimeRelativeCombo.getSelectionModel().select(0); locktimeRelativeCombo.getSelectionModel().select(0);
} else { } else {
locktimeRelativeSeconds.valueFactoryProperty().get().setValue((int)txInput.getRelativeLocktime() * TransactionInput.RELATIVE_TIMELOCK_SECONDS_INCREMENT); locktimeRelativeSeconds.valueFactoryProperty().get().setValue(Duration.ofSeconds(txInput.getRelativeLocktime() * TransactionInput.RELATIVE_TIMELOCK_SECONDS_INCREMENT));
locktimeRelativeCombo.getSelectionModel().select(1); locktimeRelativeCombo.getSelectionModel().select(1);
} }
locktimeToggleGroup.selectToggle(locktimeRelativeType); locktimeToggleGroup.selectToggle(locktimeRelativeType);
@ -272,7 +270,7 @@ public class InputController extends TransactionFormController implements Initia
Integer value = locktimeRelativeBlocks.getValue(); Integer value = locktimeRelativeBlocks.getValue();
txInput.setSequenceNumber(value & TransactionInput.RELATIVE_TIMELOCK_VALUE_MASK); txInput.setSequenceNumber(value & TransactionInput.RELATIVE_TIMELOCK_VALUE_MASK);
} else { } else {
Integer value = locktimeRelativeSeconds.getValue() / TransactionInput.RELATIVE_TIMELOCK_SECONDS_INCREMENT; long value = locktimeRelativeSeconds.getValue().toSeconds() / TransactionInput.RELATIVE_TIMELOCK_SECONDS_INCREMENT;
txInput.setSequenceNumber((value & TransactionInput.RELATIVE_TIMELOCK_VALUE_MASK) | TransactionInput.RELATIVE_TIMELOCK_TYPE_FLAG); txInput.setSequenceNumber((value & TransactionInput.RELATIVE_TIMELOCK_VALUE_MASK) | TransactionInput.RELATIVE_TIMELOCK_TYPE_FLAG);
} }
EventManager.get().notify(transaction); EventManager.get().notify(transaction);

View file

@ -11,6 +11,7 @@
<?import javafx.collections.FXCollections?> <?import javafx.collections.FXCollections?>
<?import java.lang.String?> <?import java.lang.String?>
<?import org.controlsfx.control.ToggleSwitch?> <?import org.controlsfx.control.ToggleSwitch?>
<?import com.sparrowwallet.sparrow.control.RelativeTimelockSpinner?>
<GridPane alignment="TOP_CENTER" hgap="10.0" prefHeight="500.0" prefWidth="620.0" stylesheets="@input.css, @../general.css" vgap="10.0" xmlns="http://javafx.com/javafx/10.0.2-internal" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.sparrowwallet.sparrow.transaction.InputController"> <GridPane alignment="TOP_CENTER" hgap="10.0" prefHeight="500.0" prefWidth="620.0" stylesheets="@input.css, @../general.css" vgap="10.0" 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>
@ -126,12 +127,12 @@
</Field> </Field>
<Field fx:id="locktimeRelativeField" text="Value:"> <Field fx:id="locktimeRelativeField" text="Value:">
<Spinner fx:id="locktimeRelativeBlocks" editable="true" prefWidth="110"/> <Spinner fx:id="locktimeRelativeBlocks" editable="true" prefWidth="110"/>
<Spinner 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>
<FXCollections fx:factory="observableArrayList"> <FXCollections fx:factory="observableArrayList">
<String fx:value="blocks" /> <String fx:value="blocks" />
<String fx:value="seconds" /> <String fx:value="time" />
</FXCollections> </FXCollections>
</items> </items>
</ComboBox> </ComboBox>