259 lines
9.1 KiB
Java
259 lines
9.1 KiB
Java
package ctbrec.ui;
|
|
import java.util.regex.Pattern;
|
|
|
|
import javafx.application.Application;
|
|
import javafx.beans.binding.Bindings;
|
|
import javafx.beans.binding.IntegerBinding;
|
|
import javafx.beans.property.ReadOnlyIntegerProperty;
|
|
import javafx.beans.property.ReadOnlyIntegerWrapper;
|
|
import javafx.geometry.Insets;
|
|
import javafx.scene.Scene;
|
|
import javafx.scene.control.IndexRange;
|
|
import javafx.scene.control.Label;
|
|
import javafx.scene.control.TextField;
|
|
import javafx.scene.layout.VBox;
|
|
import javafx.stage.Stage;
|
|
|
|
public class TimeTextFieldTest extends Application {
|
|
|
|
@Override
|
|
public void start(Stage primaryStage) {
|
|
VBox root = new VBox(5);
|
|
root.setPadding(new Insets(5));
|
|
Label hrLabel = new Label();
|
|
Label minLabel = new Label();
|
|
Label secLabel = new Label();
|
|
TimeTextField timeTextField = new TimeTextField();
|
|
hrLabel.textProperty().bind(Bindings.format("Hours: %d", timeTextField.hoursProperty()));
|
|
minLabel.textProperty().bind(Bindings.format("Minutes: %d", timeTextField.minutesProperty()));
|
|
secLabel.textProperty().bind(Bindings.format("Seconds: %d", timeTextField.secondsProperty()));
|
|
|
|
root.getChildren().addAll(timeTextField, hrLabel, minLabel, secLabel);
|
|
Scene scene = new Scene(root);
|
|
primaryStage.setScene(scene);
|
|
primaryStage.show();
|
|
}
|
|
|
|
public static void main(String[] args) {
|
|
launch(args);
|
|
|
|
}
|
|
|
|
public static class TimeTextField extends TextField {
|
|
|
|
enum Unit {
|
|
HOURS, MINUTES, SECONDS
|
|
};
|
|
|
|
private final Pattern timePattern;
|
|
private final ReadOnlyIntegerWrapper hours;
|
|
private final ReadOnlyIntegerWrapper minutes;
|
|
private final ReadOnlyIntegerWrapper seconds;
|
|
|
|
public TimeTextField() {
|
|
this("00:00:00");
|
|
}
|
|
|
|
public TimeTextField(String time) {
|
|
super(time);
|
|
timePattern = Pattern.compile("\\d\\d:\\d\\d:\\d\\d");
|
|
if (!validate(time)) {
|
|
throw new IllegalArgumentException("Invalid time: " + time);
|
|
}
|
|
hours = new ReadOnlyIntegerWrapper(this, "hours");
|
|
minutes = new ReadOnlyIntegerWrapper(this, "minutes");
|
|
seconds = new ReadOnlyIntegerWrapper(this, "seconds");
|
|
hours.bind(new TimeTextField.TimeUnitBinding(Unit.HOURS));
|
|
minutes.bind(new TimeTextField.TimeUnitBinding(Unit.MINUTES));
|
|
seconds.bind(new TimeTextField.TimeUnitBinding(Unit.SECONDS));
|
|
}
|
|
|
|
public ReadOnlyIntegerProperty hoursProperty() {
|
|
return hours.getReadOnlyProperty();
|
|
}
|
|
|
|
public int getHours() {
|
|
return hours.get();
|
|
}
|
|
|
|
public ReadOnlyIntegerProperty minutesProperty() {
|
|
return minutes.getReadOnlyProperty();
|
|
}
|
|
|
|
public int getMinutes() {
|
|
return minutes.get();
|
|
}
|
|
|
|
public ReadOnlyIntegerProperty secondsProperty() {
|
|
return seconds.getReadOnlyProperty();
|
|
}
|
|
|
|
public int getSeconds() {
|
|
return seconds.get();
|
|
}
|
|
|
|
@Override
|
|
public void appendText(String text) {
|
|
// Ignore this. Our text is always 8 characters long, we cannot append anything
|
|
}
|
|
|
|
@Override
|
|
public boolean deleteNextChar() {
|
|
|
|
boolean success = false;
|
|
|
|
// If there's a selection, delete it:
|
|
final IndexRange selection = getSelection();
|
|
if (selection.getLength() > 0) {
|
|
int selectionEnd = selection.getEnd();
|
|
this.deleteText(selection);
|
|
this.positionCaret(selectionEnd);
|
|
success = true;
|
|
} else {
|
|
// If the caret preceeds a digit, replace that digit with a zero and move the caret forward. Else just move the caret forward.
|
|
int caret = this.getCaretPosition();
|
|
if (caret % 3 != 2) { // not preceeding a colon
|
|
String currentText = this.getText();
|
|
setText(currentText.substring(0, caret) + "0" + currentText.substring(caret + 1));
|
|
success = true;
|
|
}
|
|
this.positionCaret(Math.min(caret + 1, this.getText().length()));
|
|
}
|
|
return success;
|
|
}
|
|
|
|
@Override
|
|
public boolean deletePreviousChar() {
|
|
|
|
boolean success = false;
|
|
|
|
// If there's a selection, delete it:
|
|
final IndexRange selection = getSelection();
|
|
if (selection.getLength() > 0) {
|
|
int selectionStart = selection.getStart();
|
|
this.deleteText(selection);
|
|
this.positionCaret(selectionStart);
|
|
success = true;
|
|
} else {
|
|
// If the caret is after a digit, replace that digit with a zero and move the caret backward. Else just move the caret back.
|
|
int caret = this.getCaretPosition();
|
|
if (caret % 3 != 0) { // not following a colon
|
|
String currentText = this.getText();
|
|
setText(currentText.substring(0, caret - 1) + "0" + currentText.substring(caret));
|
|
success = true;
|
|
}
|
|
this.positionCaret(Math.max(caret - 1, 0));
|
|
}
|
|
return success;
|
|
}
|
|
|
|
@Override
|
|
public void deleteText(IndexRange range) {
|
|
this.deleteText(range.getStart(), range.getEnd());
|
|
}
|
|
|
|
@Override
|
|
public void deleteText(int begin, int end) {
|
|
// Replace all digits in the given range with zero:
|
|
StringBuilder builder = new StringBuilder(this.getText());
|
|
for (int c = begin; c < end; c++) {
|
|
if (c % 3 != 2) { // Not at a colon:
|
|
builder.replace(c, c + 1, "0");
|
|
}
|
|
}
|
|
this.setText(builder.toString());
|
|
}
|
|
|
|
@Override
|
|
public void insertText(int index, String text) {
|
|
// Handle an insert by replacing the range from index to index+text.length() with text, if that results in a valid string:
|
|
StringBuilder builder = new StringBuilder(this.getText());
|
|
builder.replace(index, index + text.length(), text);
|
|
final String testText = builder.toString();
|
|
if (validate(testText)) {
|
|
this.setText(testText);
|
|
}
|
|
this.positionCaret(index + text.length());
|
|
}
|
|
|
|
@Override
|
|
public void replaceSelection(String replacement) {
|
|
final IndexRange selection = this.getSelection();
|
|
if (selection.getLength() == 0) {
|
|
this.insertText(selection.getStart(), replacement);
|
|
} else {
|
|
this.replaceText(selection.getStart(), selection.getEnd(), replacement);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void replaceText(IndexRange range, String text) {
|
|
this.replaceText(range.getStart(), range.getEnd(), text);
|
|
}
|
|
|
|
@Override
|
|
public void replaceText(int begin, int end, String text) {
|
|
if (begin == end) {
|
|
this.insertText(begin, text);
|
|
} else {
|
|
// only handle this if text.length() is equal to the number of characters being replaced, and if the replacement results in a valid string:
|
|
if (text.length() == end - begin) {
|
|
StringBuilder builder = new StringBuilder(this.getText());
|
|
builder.replace(begin, end, text);
|
|
String testText = builder.toString();
|
|
if (validate(testText)) {
|
|
this.setText(testText);
|
|
}
|
|
this.positionCaret(end);
|
|
}
|
|
}
|
|
}
|
|
|
|
private boolean validate(String time) {
|
|
if (!timePattern.matcher(time).matches()) {
|
|
return false;
|
|
}
|
|
String[] tokens = time.split(":");
|
|
assert tokens.length == 3;
|
|
try {
|
|
int hours = Integer.parseInt(tokens[0]);
|
|
int mins = Integer.parseInt(tokens[1]);
|
|
int secs = Integer.parseInt(tokens[2]);
|
|
if (hours < 0 || hours > 23) {
|
|
return false;
|
|
}
|
|
if (mins < 0 || mins > 59) {
|
|
return false;
|
|
}
|
|
if (secs < 0 || secs > 59) {
|
|
return false;
|
|
}
|
|
return true;
|
|
} catch (NumberFormatException nfe) {
|
|
// regex matching should assure we never reach this catch block
|
|
assert false;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private final class TimeUnitBinding extends IntegerBinding {
|
|
|
|
final Unit unit;
|
|
|
|
TimeUnitBinding(Unit unit) {
|
|
this.bind(textProperty());
|
|
this.unit = unit;
|
|
}
|
|
|
|
@Override
|
|
protected int computeValue() {
|
|
// Crazy enum magic
|
|
String token = getText().split(":")[unit.ordinal()];
|
|
return Integer.parseInt(token);
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
}
|