mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2024-12-23 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.psbt.PSBTInput;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.control.RelativeTimelockSpinner;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.fxml.Initializable;
|
||||
import javafx.scene.control.*;
|
||||
|
@ -13,6 +14,7 @@ import tornadofx.control.Fieldset;
|
|||
import org.fxmisc.flowless.VirtualizedScrollPane;
|
||||
|
||||
import java.net.URL;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
|
@ -87,7 +89,7 @@ public class InputController extends TransactionFormController implements Initia
|
|||
private Spinner<Integer> locktimeRelativeBlocks;
|
||||
|
||||
@FXML
|
||||
private Spinner<Integer> locktimeRelativeSeconds;
|
||||
private RelativeTimelockSpinner locktimeRelativeSeconds;
|
||||
|
||||
@FXML
|
||||
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));
|
||||
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());
|
||||
locktimeRelativeSeconds.managedProperty().bind(locktimeRelativeSeconds.visibleProperty());
|
||||
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());
|
||||
locktimeRelativeCombo.getSelectionModel().select(0);
|
||||
} 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);
|
||||
}
|
||||
locktimeToggleGroup.selectToggle(locktimeRelativeType);
|
||||
|
@ -272,7 +270,7 @@ public class InputController extends TransactionFormController implements Initia
|
|||
Integer value = locktimeRelativeBlocks.getValue();
|
||||
txInput.setSequenceNumber(value & TransactionInput.RELATIVE_TIMELOCK_VALUE_MASK);
|
||||
} 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);
|
||||
}
|
||||
EventManager.get().notify(transaction);
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
<?import javafx.collections.FXCollections?>
|
||||
<?import java.lang.String?>
|
||||
<?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">
|
||||
<padding>
|
||||
|
@ -126,12 +127,12 @@
|
|||
</Field>
|
||||
<Field fx:id="locktimeRelativeField" text="Value:">
|
||||
<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">
|
||||
<items>
|
||||
<FXCollections fx:factory="observableArrayList">
|
||||
<String fx:value="blocks" />
|
||||
<String fx:value="seconds" />
|
||||
<String fx:value="time" />
|
||||
</FXCollections>
|
||||
</items>
|
||||
</ComboBox>
|
||||
|
|
Loading…
Reference in a new issue