diff --git a/build.gradle b/build.gradle index e04e2fc..5df78f7 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ plugins { } group 'app.nush' -version '1.3.8-beta' +version '1.3.9-beta' applicationName = 'Exam Clock' mainClassName = 'app.nush.examclock.Main' @@ -20,7 +20,7 @@ repositories { // } } -double jdkVersion = 1.8 +double jdkVersion = 1.14 println "JDK version = ${jdkVersion}" sourceCompatibility = 1.14 @@ -33,7 +33,7 @@ dependencies { implementation "org.openjfx:javafx-graphics:11:win" implementation "org.openjfx:javafx-graphics:11:mac" implementation 'com.google.code.gson:gson:2.8.6' - implementation 'no.tornado:tornadofx-controls:1.0.4' + implementation 'no.tornado:tornadofx-controls:1.0.6' implementation 'io.socket:socket.io-client:1.0.0' implementation group: 'com.dlsc.preferencesfx', name: 'preferencesfx-core', version: preferenceFx } diff --git a/src/main/java/app/nush/examclock/ExamClock.java b/src/main/java/app/nush/examclock/ExamClock.java index 34eb9a4..f816097 100644 --- a/src/main/java/app/nush/examclock/ExamClock.java +++ b/src/main/java/app/nush/examclock/ExamClock.java @@ -29,6 +29,7 @@ public class ExamClock extends Application { public static Preferences preferences; private static ExamClock instance; + private static Stage stage; private MainController controller; public static void main(String[] args) { @@ -39,15 +40,19 @@ public static ExamClock getInstance() { return instance; } + public static Stage getStage() { + return stage; + } + @Override public void start(Stage primaryStage) { + stage = primaryStage; try { instance = this; preferences = Preferences.userNodeForPackage(ExamClock.class); FXMLLoader loader = new FXMLLoader(getClass().getResource("/fxml_main.fxml")); Parent root = loader.load(); controller = loader.getController(); - controller.setStage(primaryStage); Scene scene = new Scene(root); scene.getStylesheets().add("/main.css"); scene.getStylesheets().add("/theme.css"); diff --git a/src/main/java/app/nush/examclock/controllers/AddExamController.java b/src/main/java/app/nush/examclock/controllers/AddExamController.java index e93f912..e8978ef 100644 --- a/src/main/java/app/nush/examclock/controllers/AddExamController.java +++ b/src/main/java/app/nush/examclock/controllers/AddExamController.java @@ -1,5 +1,6 @@ package app.nush.examclock.controllers; +import app.nush.examclock.display.TimePicker; import app.nush.examclock.model.Exam; import javafx.event.ActionEvent; import javafx.fxml.FXML; @@ -41,12 +42,12 @@ public class AddExamController { public DatePicker date_input; public Spinner duration_hours; public Spinner duration_minutes; - public TextField start_time_input; - public TextField end_time_input; + public TimePicker start_time_input; + public TimePicker end_time_input; private MainController mainController; - private static LocalTime parseTime(String time, int index) { + public static LocalTime parseTime(String time, int index) { if (index >= timeFormatters.length) throw new DateTimeParseException("No match found", time, 0); try { return LocalTime.parse(time.replace(" ", ""), timeFormatters[index]); @@ -72,23 +73,10 @@ public LocalDate fromString(String dateString) { } }); date_input.setValue(LocalDate.now()); - - start_time_input.textProperty().addListener((observable, newv, oldv) -> { + start_time_input.timeProperty.addListener((observable, oldValue, newValue) -> end_time_input.timeProperty.set(newValue.plusHours(duration_hours.getValue()).plusMinutes(duration_minutes.getValue()))); + end_time_input.timeProperty.addListener((observable, oldValue, newValue) -> { try { - LocalTime parsed = parseTime(start_time_input.getText().toLowerCase(), 0); - start_time_input.setUserData(parsed); - end_time_input.setText(timeFormatters[0].format(parsed.plusHours(duration_hours.getValue()).plusMinutes(duration_minutes.getValue()))); - } catch (DateTimeParseException e) { -// if (start_time_input.getUserData() != null) -// start_time_input.setText(timeFormatter.format((LocalTime) start_time_input.getUserData())); - } - }); - end_time_input.textProperty().addListener((observable, newv, oldv) -> { - try { - LocalTime parsed = parseTime(end_time_input.getText().toLowerCase(), 0); - end_time_input.setUserData(parsed); - LocalTime start = (LocalTime) start_time_input.getUserData(); - int minutes = (int) start.until(parsed, ChronoUnit.MINUTES); + int minutes = (int) start_time_input.timeProperty.get().until(newValue, ChronoUnit.MINUTES); duration_hours.getValueFactory().setValue(minutes / 60); duration_minutes.getValueFactory().setValue(minutes % 60); } catch (DateTimeParseException e) { @@ -96,14 +84,8 @@ public LocalDate fromString(String dateString) { // end_time_input.setText(timeFormatter.format((LocalTime) end_time_input.getUserData())); } }); - duration_hours.valueProperty().addListener((observable, oldValue, newValue) -> { - if (start_time_input.getUserData() == null) return; - end_time_input.setText(timeFormatters[0].format(((LocalTime) start_time_input.getUserData()).plusHours(duration_hours.getValue()).plusMinutes(duration_minutes.getValue()))); - }); - duration_minutes.valueProperty().addListener((observable, oldValue, newValue) -> { - if (start_time_input.getUserData() == null) return; - end_time_input.setText(timeFormatters[0].format(((LocalTime) start_time_input.getUserData()).plusHours(duration_hours.getValue()).plusMinutes(duration_minutes.getValue()))); - }); + duration_hours.valueProperty().addListener((observable, oldValue, newValue) -> end_time_input.timeProperty.set(start_time_input.timeProperty.get().plusHours(duration_hours.getValue()).plusMinutes(duration_minutes.getValue()))); + duration_minutes.valueProperty().addListener((observable, oldValue, newValue) -> end_time_input.timeProperty.set(start_time_input.timeProperty.get().plusHours(duration_hours.getValue()).plusMinutes(duration_minutes.getValue()))); } /** diff --git a/src/main/java/app/nush/examclock/controllers/MainController.java b/src/main/java/app/nush/examclock/controllers/MainController.java index 43e0b8c..07af297 100644 --- a/src/main/java/app/nush/examclock/controllers/MainController.java +++ b/src/main/java/app/nush/examclock/controllers/MainController.java @@ -65,10 +65,6 @@ public class MainController { * if male toilet is occupied. */ public SimpleBooleanProperty toiletMaleOccupied = new SimpleBooleanProperty(false); - /** - * The Stage. - */ - public Stage stage; /** * The Add exam stage. */ @@ -201,6 +197,12 @@ public void initialize() throws IOException { initAddExamStage(); initConnectionStage(); + PreferenceController.nightMode.addListener((observable, oldValue, newValue) -> { + addExamStage.getScene().getStylesheets().removeAll("/theme.dark.css", "/theme.light.css"); + addExamStage.getScene().getStylesheets().add(newValue ? "/theme.dark.css" : "/theme.light.css"); + connectStage.getScene().getStylesheets().removeAll("/theme.dark.css", "/theme.light.css"); + connectStage.getScene().getStylesheets().add(newValue ? "/theme.dark.css" : "/theme.light.css"); + }); preferenceController.initPreferences(); // load preferences after adding listeners loadExams(null); // load exams from disk @@ -394,8 +396,8 @@ public void showConnection(ActionEvent event) { } @FXML - public void importExams(ActionEvent actionEvent) { - File file = fileChooser.showOpenDialog(stage); + public void importExams(ActionEvent event) { + File file = fileChooser.showOpenDialog(ExamClock.getStage()); if (file == null) return; try { String str = new String(Files.readAllBytes(Paths.get(file.toURI()))); @@ -412,8 +414,8 @@ public void importExams(ActionEvent actionEvent) { } @FXML - public void exportExams(ActionEvent actionEvent) { - File file = fileChooser.showSaveDialog(stage); + public void exportExams(ActionEvent event) { + File file = fileChooser.showSaveDialog(ExamClock.getStage()); if (file == null) return; try { Files.write(Paths.get(file.toURI()), gson.toJson(exams).getBytes()); @@ -423,7 +425,7 @@ public void exportExams(ActionEvent actionEvent) { } @FXML - public void about(ActionEvent actionEvent) { + public void about(ActionEvent event) { try { Stage stage = new Stage(); Group logo = FXMLLoader.load(getClass().getResource("/logo_light.fxml")); @@ -463,14 +465,10 @@ public void about(ActionEvent actionEvent) { } @FXML - public void help(ActionEvent actionEvent) { + public void help(ActionEvent event) { ExamClock.getInstance().getHostServices().showDocument("https://github.com/appventure-nush/exam-clock-2020/blob/master/README.md"); } - public void setStage(Stage stage) { - this.stage = stage; - } - public void onClose(WindowEvent event) { stop(); connectionController.onClose(event); diff --git a/src/main/java/app/nush/examclock/controllers/PreferenceController.java b/src/main/java/app/nush/examclock/controllers/PreferenceController.java index 66bd7f4..d0ddffb 100644 --- a/src/main/java/app/nush/examclock/controllers/PreferenceController.java +++ b/src/main/java/app/nush/examclock/controllers/PreferenceController.java @@ -142,8 +142,8 @@ public void initPreferences() { */ public void attachListeners() { nightMode.addListener((observable, oldValue, newValue) -> { - controller.stage.getScene().getStylesheets().removeAll("/theme.dark.css", "/theme.light.css"); - controller.stage.getScene().getStylesheets().add(newValue ? "/theme.dark.css" : "/theme.light.css"); + ExamClock.getStage().getScene().getStylesheets().removeAll("/theme.dark.css", "/theme.light.css"); + ExamClock.getStage().getScene().getStylesheets().add(newValue ? "/theme.dark.css" : "/theme.light.css"); preferencesFx.getView().getScene().getStylesheets().removeAll("/theme.dark.css", "/theme.light.css"); preferencesFx.getView().getScene().getStylesheets().addAll("/theme.css", nightMode.get() ? "/theme.dark.css" : "/theme.light.css"); }); diff --git a/src/main/java/app/nush/examclock/display/TimePicker.java b/src/main/java/app/nush/examclock/display/TimePicker.java new file mode 100644 index 0000000..51a6def --- /dev/null +++ b/src/main/java/app/nush/examclock/display/TimePicker.java @@ -0,0 +1,46 @@ +package app.nush.examclock.display; + +import app.nush.examclock.model.CustomBinding; +import javafx.beans.property.SimpleIntegerProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.geometry.Bounds; +import javafx.scene.control.TextField; + +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; + +import static app.nush.examclock.controllers.AddExamController.parseTime; + +public class TimePicker extends TextField { + public static final DateTimeFormatter defaultFormatter = DateTimeFormatter.ofPattern("hh:mm a"); + private final TimePopup popup; + public SimpleObjectProperty timeProperty; + public SimpleIntegerProperty hour; + public SimpleIntegerProperty minute; + + public TimePicker() { + timeProperty = new SimpleObjectProperty<>(); + hour = new SimpleIntegerProperty(0); + minute = new SimpleIntegerProperty(0); + + popup = new TimePopup(this); + + CustomBinding.bindBidirectional(timeProperty, hour, LocalTime::getHour, hour -> LocalTime.of(hour.intValue(), minute.get())); + CustomBinding.bindBidirectional(timeProperty, minute, LocalTime::getMinute, minute -> LocalTime.of(hour.get(), minute.intValue())); + CustomBinding.bindBidirectional(timeProperty, textProperty(), localTime -> localTime.format(defaultFormatter), text -> { + try { + return parseTime(text, 0); + } catch (Exception ignored) { + return timeProperty.get(); + } + }); + + setPromptText("00:00 am"); + focusedProperty().addListener((observable, oldValue, newValue) -> { + if (!oldValue && newValue) { //gain focus + Bounds bounds = localToScreen(getBoundsInLocal()); + popup.show(TimePicker.this, bounds.getMinX(), bounds.getMaxY()); + } + }); + } +} diff --git a/src/main/java/app/nush/examclock/display/TimePopup.java b/src/main/java/app/nush/examclock/display/TimePopup.java new file mode 100644 index 0000000..b106e67 --- /dev/null +++ b/src/main/java/app/nush/examclock/display/TimePopup.java @@ -0,0 +1,55 @@ +package app.nush.examclock.display; + +import app.nush.examclock.model.CustomBinding; +import javafx.scene.Node; +import javafx.scene.control.PopupControl; +import javafx.scene.control.ScrollPane; +import javafx.scene.control.Skin; +import javafx.scene.layout.HBox; +import tornadofx.control.ListItem; +import tornadofx.control.ListMenu; + +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +public class TimePopup extends PopupControl { + + public TimePopup(TimePicker picker) { + HBox root = new HBox(); + root.setPrefHeight(300); + ListMenu hourMenu = new ListMenu(); + List hourMenuItems = IntStream.range(0, 24).mapToObj(i -> new ListItem(String.valueOf(i))).collect(Collectors.toList()); + hourMenu.getChildren().addAll(hourMenuItems); + + ListMenu minuteMenu = new ListMenu(); + List minuteMenuItems = IntStream.range(0, 60).mapToObj(i -> new ListItem(String.valueOf(i))).collect(Collectors.toList()); + minuteMenu.getChildren().addAll(minuteMenuItems); + + CustomBinding.bindBidirectional(hourMenu.activeProperty(), picker.hour, listItem -> Integer.parseInt(listItem.getText()), hour -> hourMenuItems.get((Integer) hour)); + CustomBinding.bindBidirectional(minuteMenu.activeProperty(), picker.minute, listItem -> Integer.parseInt(listItem.getText()), minute -> minuteMenuItems.get((Integer) minute)); + root.getChildren().addAll(new ScrollPane(hourMenu) {{ + setVbarPolicy(ScrollBarPolicy.ALWAYS); + setHbarPolicy(ScrollBarPolicy.NEVER); + }}, new ScrollPane(minuteMenu) {{ + setVbarPolicy(ScrollBarPolicy.ALWAYS); + setHbarPolicy(ScrollBarPolicy.NEVER); + }}); + + setSkin(new Skin() { + public TimePopup getSkinnable() { + return TimePopup.this; + } + + public Node getNode() { + return root; + } + + public void dispose() { + } + }); + setHideOnEscape(true); + setAutoFix(true); + setAutoHide(true); + } +} diff --git a/src/main/java/app/nush/examclock/model/CustomBinding.java b/src/main/java/app/nush/examclock/model/CustomBinding.java new file mode 100644 index 0000000..500759b --- /dev/null +++ b/src/main/java/app/nush/examclock/model/CustomBinding.java @@ -0,0 +1,37 @@ +package app.nush.examclock.model; + +import javafx.beans.property.Property; +import javafx.beans.value.ChangeListener; +import javafx.beans.value.ObservableValue; +import javafx.beans.value.WritableValue; + +import java.util.function.Function; + +public class CustomBinding { + + public static void bindBidirectional(Property propertyA, Property propertyB, Function updateB, Function updateA) { + addFlaggedChangeListener(propertyA, propertyB, updateB); + addFlaggedChangeListener(propertyB, propertyA, updateA); + } + + public static void bind(Property propertyA, Property propertyB, Function updateB) { + addFlaggedChangeListener(propertyA, propertyB, updateB); + } + + private static void addFlaggedChangeListener(ObservableValue propertyX, WritableValue propertyY, Function updateY) { + propertyX.addListener(new ChangeListener() { + private boolean alreadyCalled = false; + + @Override + public void changed(ObservableValue observable, X oldValue, X newValue) { + if (alreadyCalled) return; + try { + alreadyCalled = true; + propertyY.setValue(updateY.apply(newValue)); + } finally { + alreadyCalled = false; + } + } + }); + } +} \ No newline at end of file diff --git a/src/main/resources/fxml_add_exam.fxml b/src/main/resources/fxml_add_exam.fxml index af232dd..5f61049 100644 --- a/src/main/resources/fxml_add_exam.fxml +++ b/src/main/resources/fxml_add_exam.fxml @@ -1,12 +1,13 @@ +
-
+
@@ -19,10 +20,10 @@ - + - +