mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2024-11-02 20:36:44 +00:00
add relative timelock spinner
This commit is contained in:
parent
731be1a60c
commit
924e7d6b45
3 changed files with 262 additions and 9 deletions
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in a new issue