package rmblworx.tools.timey.gui; import java.io.IOException; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.List; import java.util.ResourceBundle; import javafx.application.Platform; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import javafx.collections.FXCollections; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import javafx.concurrent.Task; import javafx.event.Event; import javafx.event.EventHandler; import javafx.fxml.FXML; import javafx.fxml.FXMLLoader; import javafx.scene.Node; import javafx.scene.Parent; import javafx.scene.Scene; import javafx.scene.control.Button; import javafx.scene.control.CheckBox; import javafx.scene.control.ProgressBar; import javafx.scene.control.TableCell; import javafx.scene.control.TableColumn; import javafx.scene.control.TableColumn.CellDataFeatures; import javafx.scene.control.TableView; import javafx.scene.input.MouseButton; import javafx.scene.input.MouseEvent; import javafx.scene.text.Text; import javafx.stage.Modality; import javafx.stage.Stage; import javafx.stage.StageStyle; import javafx.util.Callback; import rmblworx.tools.timey.ITimey; import rmblworx.tools.timey.event.AlarmExpiredEvent; import rmblworx.tools.timey.event.TimeyEvent; import rmblworx.tools.timey.event.TimeyEventListener; import rmblworx.tools.timey.vo.AlarmDescriptor; /* * Copyright 2014-2015 Christian Raue * MIT License http://opensource.org/licenses/mit-license.php */ /** * Controller für die Alarm-GUI. * @author Christian Raue {@literal <christian.raue@gmail.com>} */ public class AlarmController extends Controller implements TimeyEventListener { /** * Wie lange (in ms) die "alle löschen"-Schaltfläche gedrückt werden muss, um die Aktion tatsächlich auszulösen. */ public static final long TIME_TO_PRESS_DELETE_ALL_BUTTON = 1000L; /** * Formatiert Zeitstempel als Datum/Zeit-Werte. */ private final DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm:ss"); @FXML private ResourceBundle resources; @FXML private Node alarmContainer; @FXML private Node alarmProgressContainer; @FXML private TableView<Alarm> alarmTable; @FXML private TableColumn<Alarm, LocalDateTime> alarmDateTimeColumn; @FXML private TableColumn<Alarm, String> alarmDescriptionColumn; @FXML private TableColumn<Alarm, Boolean> alarmEnabledColumn; @FXML private Button alarmEditButton; @FXML private Button alarmDeleteButton; @FXML private Button alarmDeleteAllButton; @FXML private ProgressBar alarmDeleteAllProgress; private Stage dialogStage; /** * Zeit (in ms), wann die "alle löschen"-Schaltfläche gedrückt wurde. */ private volatile Long deleteAllButtonPressed; @FXML private void initialize() { // Spalten mit Attributen verknüpfen alarmEnabledColumn .setCellValueFactory(new Callback<TableColumn.CellDataFeatures<Alarm, Boolean>, ObservableValue<Boolean>>() { public ObservableValue<Boolean> call(final CellDataFeatures<Alarm, Boolean> param) { return param.getValue().enabledProperty(); } }); alarmDateTimeColumn .setCellValueFactory(new Callback<TableColumn.CellDataFeatures<Alarm, LocalDateTime>, ObservableValue<LocalDateTime>>() { public ObservableValue<LocalDateTime> call(final CellDataFeatures<Alarm, LocalDateTime> param) { return param.getValue().dateTimeProperty(); } }); alarmDescriptionColumn.setCellValueFactory(new Callback<TableColumn.CellDataFeatures<Alarm, String>, ObservableValue<String>>() { public ObservableValue<String> call(final CellDataFeatures<Alarm, String> param) { return param.getValue().descriptionProperty(); } }); // Checkbox in "aktiviert"-Spalte rendern alarmEnabledColumn.setCellFactory(new Callback<TableColumn<Alarm, Boolean>, TableCell<Alarm, Boolean>>() { public TableCell<Alarm, Boolean> call(final TableColumn<Alarm, Boolean> param) { return new TableCell<Alarm, Boolean>() { protected void updateItem(final Boolean item, final boolean empty) { super.updateItem(item, empty); if (empty || item == null) { setGraphic(null); return; } final CheckBox checkBox = new CheckBox(); checkBox.setDisable(true); checkBox.setSelected(item); setGraphic(checkBox); } }; } }); // Formatierung der Datum/Zeit-Spalte alarmDateTimeColumn.setCellFactory(new Callback<TableColumn<Alarm, LocalDateTime>, TableCell<Alarm, LocalDateTime>>() { public TableCell<Alarm, LocalDateTime> call(final TableColumn<Alarm, LocalDateTime> param) { return new TableCell<Alarm, LocalDateTime>() { protected void updateItem(final LocalDateTime item, final boolean empty) { super.updateItem(item, empty); setText(empty ? null : dateTimeFormatter.format(item)); } }; } }); // Platzhaltertext (für Tabelle ohne Einträge) setzen alarmTable.setPlaceholder(new Text(resources.getString("noAlarmsDefined.placeholder"))); // Bearbeiten- und Löschen-Schaltflächen nur aktivieren, wenn Eintrag ausgewählt alarmTable.getSelectionModel().selectedItemProperty().addListener(new ChangeListener<Alarm>() { public void changed(final ObservableValue<? extends Alarm> property, final Alarm oldValue, final Alarm newValue) { Platform.runLater(new Runnable() { public void run() { final boolean isItemSelected = newValue != null; alarmEditButton.setDisable(!isItemSelected); alarmDeleteButton.setDisable(!isItemSelected); } }); } }); // "alle löschen"-Schaltfläche nur aktivieren, wenn Einträge vorhanden alarmTable.getItems().addListener(new ListChangeListener<Alarm>() { public void onChanged(final ListChangeListener.Change<? extends Alarm> change) { Platform.runLater(new Runnable() { public void run() { alarmDeleteAllButton.setDisable(change.getList().isEmpty()); } }); } }); Platform.runLater(new Runnable() { public void run() { reloadAlarms(); } }); initializeDeleteAllButton(); final TimeyEventListener eventListener = this; Platform.runLater(new Runnable() { public void run() { getGuiHelper().getFacade().addEventListener(eventListener); } }); } /** * Initialisiert die "alle löschen"-Schaltfläche mit allen nötigen {@code EventHandler}n. */ private void initializeDeleteAllButton() { // Initialzustand für "alle löschen"-Schaltfläche alarmEditButton.setDisable(true); alarmDeleteButton.setDisable(true); alarmDeleteAllButton.setDisable(true); alarmDeleteAllButton.setGraphic(null); // beim Drücken der "alle löschen"-Schaltfläche Fortschrittsbalken einblenden alarmDeleteAllButton.setOnMousePressed(new EventHandler<Event>() { public void handle(final Event event) { Platform.runLater(new Runnable() { public void run() { alarmDeleteAllButton.setText(null); alarmDeleteAllButton.setGraphic(alarmDeleteAllProgress); } }); final Task<Void> task = new Task<Void>() { private static final long SLEEP_TIME = 10L; public Void call() throws InterruptedException { deleteAllButtonPressed = System.currentTimeMillis(); while (deleteAllButtonPressed != null) { try { final long duration = System.currentTimeMillis() - deleteAllButtonPressed; updateProgress(duration, TIME_TO_PRESS_DELETE_ALL_BUTTON); if (duration >= TIME_TO_PRESS_DELETE_ALL_BUTTON) { break; } Thread.sleep(SLEEP_TIME); } catch (final NullPointerException e) { // Könnte beim Zugriff auf deleteAllButtonPressed auftreten. Nicht vorher prüfbar, da volatil. break; } } return null; } }; task.progressProperty().addListener(new ChangeListener<Number>() { public void changed(final ObservableValue<? extends Number> property, final Number oldValue, final Number newValue) { alarmDeleteAllProgress.setProgress(newValue.doubleValue()); } }); getGuiHelper().runInThread(task, resources); } }); // beim Verlassen der "alle löschen"-Schaltfläche Fortschrittsbalken stoppen alarmDeleteAllButton.setOnMouseExited(new EventHandler<Event>() { public void handle(final Event event) { deleteAllButtonPressed = null; } }); // beim Loslassen der "alle löschen"-Schaltfläche Fortschrittsbalken ausblenden und evtl. alle Alarme löschen, wenn vollständig alarmDeleteAllButton.setOnMouseReleased(new EventHandler<Event>() { public void handle(final Event event) { if (deleteAllButtonPressed != null && alarmDeleteAllProgress.getProgress() >= 1) { deleteAllButtonPressed = null; showProgress(); getGuiHelper().runInThread(new Task<Void>() { public Void call() { final ITimey facade = getGuiHelper().getFacade(); for (final Alarm alarm : alarmTable.getItems()) { facade.removeAlarm(AlarmDescriptorConverter.getAsAlarmDescriptor(alarm)); } Platform.runLater(new Runnable() { public void run() { reloadAlarms(); alarmDeleteAllButton.setText(resources.getString("alarmDeleteAllButton.label")); alarmDeleteAllButton.setGraphic(null); hideProgress(); } }); return null; } }, resources); } else { deleteAllButtonPressed = null; Platform.runLater(new Runnable() { public void run() { alarmDeleteAllButton.setText(resources.getString("alarmDeleteAllButton.label")); alarmDeleteAllButton.setGraphic(null); } }); } } }); } /** * Aktion bei Betätigen der Hinzufügen-Schaltfläche. */ @FXML private void handleAddButtonAction() { Platform.runLater(new Runnable() { public void run() { final Alarm alarm = new Alarm(); if (showAlarmEditDialog(alarm, resources.getString("alarmEdit.title.add"))) { showProgress(); getGuiHelper().runInThread(new Task<Void>() { public Void call() { alarmTable.getItems().add(alarm); refreshTable(false); getGuiHelper().getFacade().setAlarm(AlarmDescriptorConverter.getAsAlarmDescriptor(alarm)); /* * Neuen Alarm auswählen. * Muss verzögert ausgeführt werden, um Darstellungsproblem beim Hinzufügen des ersten Alarms zu vermeiden. * (Hinweis von https://community.oracle.com/message/10389376#10389376.) */ Platform.runLater(new Runnable() { public void run() { alarmTable.scrollTo(alarm); alarmTable.getSelectionModel().select(alarm); } }); hideProgress(); return null; } }, resources); } } }); } /** * Aktion bei Betätigen der Bearbeiten-Schaltfläche. */ @FXML private void handleEditButtonAction() { Platform.runLater(new Runnable() { public void run() { editAlarm(); } }); } /** * Aktion bei Betätigen der Löschen-Schaltfläche. */ @FXML private void handleDeleteButtonAction() { Platform.runLater(new Runnable() { public void run() { final Alarm alarm = alarmTable.getSelectionModel().getSelectedItem(); if (alarm != null) { showProgress(); getGuiHelper().runInThread(new Task<Void>() { public Void call() { alarmTable.getItems().remove(alarm); getGuiHelper().getFacade().removeAlarm(AlarmDescriptorConverter.getAsAlarmDescriptor(alarm)); hideProgress(); return null; } }, resources); } } }); } /** * Aktion bei Klick auf Tabelle. * @param event Mausereignis */ @FXML private void handleTableClick(final MouseEvent event) { // Eintrag bearbeiten bei Doppelklick if (event.getButton().equals(MouseButton.PRIMARY) && event.getClickCount() > 1) { editAlarm(); } } /** * Bearbeitet den ausgewählten Alarm. */ private void editAlarm() { final Alarm alarm = alarmTable.getSelectionModel().getSelectedItem(); if (alarm != null) { final AlarmDescriptor oldAlarm = AlarmDescriptorConverter.getAsAlarmDescriptor(alarm); // Instanz vorm Bearbeiten anlegen if (showAlarmEditDialog(alarm, resources.getString("alarmEdit.title.edit"))) { showProgress(); getGuiHelper().runInThread(new Task<Void>() { public Void call() { refreshTable(); final ITimey facade = getGuiHelper().getFacade(); facade.removeAlarm(oldAlarm); facade.setAlarm(AlarmDescriptorConverter.getAsAlarmDescriptor(alarm)); hideProgress(); return null; } }, resources); } } } /** * Öffnet den Dialog zum Hinzufügen/Bearbeiten eines Alarms. * @param alarm Alarm * @param title Titel des Fensters * @return ob der Alarm geändert wurde */ private boolean showAlarmEditDialog(final Alarm alarm, final String title) { try { final FXMLLoader loader = new FXMLLoader(getClass().getResource("AlarmEditDialog.fxml"), resources); final Parent root = (Parent) loader.load(); dialogStage = new Stage(StageStyle.UTILITY); dialogStage.setScene(new Scene(root)); dialogStage.setTitle(title); dialogStage.setResizable(false); dialogStage.initModality(Modality.APPLICATION_MODAL); final AlarmEditDialogController controller = loader.getController(); controller.setGuiHelper(getGuiHelper()); controller.setDialogStage(dialogStage); controller.setExistingAlarms(alarmTable.getItems()); controller.setAlarm(alarm); dialogStage.showAndWait(); dialogStage = null; return controller.isChanged(); } catch (final IOException e) { e.printStackTrace(); return false; } } public Stage getDialogStage() { return dialogStage; } /** * Aktualisiert den Inhalt der Tabelle. */ private void refreshTable() { refreshTable(true); } /** * Aktualisiert den Inhalt der Tabelle. * @param preserveSelection Ob die Auswahl erhalten bleiben soll. */ private void refreshTable(final boolean preserveSelection) { final Alarm selectedItem = alarmTable.getSelectionModel().getSelectedItem(); Platform.runLater(new Runnable() { public void run() { FXCollections.sort(alarmTable.getItems()); if (preserveSelection) { alarmTable.scrollTo(selectedItem); alarmTable.getSelectionModel().select(selectedItem); } } }); } private void showProgress() { switchProgress(true); } private void hideProgress() { switchProgress(false); } private void switchProgress(final boolean visible) { Platform.runLater(new Runnable() { public void run() { alarmProgressContainer.setVisible(visible); /* * Sichtbarkeit vom alarmContainer nicht ändern, um Probleme bei der Fokussierung von Tabellenzeilen (bzw. der Tabelle * insgesamt) zu vermeiden. Stattdessen mit Hintergrundfarbe für alarmProgressContainer arbeiten. */ } }); } /** * Lädt die Alarme aus der Datenbank. */ private void reloadAlarms() { showProgress(); getGuiHelper().runInThread(new Task<Void>() { public Void call() { final List<Alarm> alarms = AlarmDescriptorConverter.getAsAlarms(getGuiHelper().getFacade().getAllAlarms()); final int selectedIndex = alarmTable.getSelectionModel().getSelectedIndex(); final ObservableList<Alarm> tableData = alarmTable.getItems(); Platform.runLater(new Runnable() { public void run() { tableData.clear(); tableData.addAll(alarms); refreshTable(false); alarmTable.getSelectionModel().select(selectedIndex); hideProgress(); } }); return null; } }, resources); } /** * {@inheritDoc} */ @Override public final void handleEvent(final TimeyEvent event) { if (event instanceof AlarmExpiredEvent) { reloadAlarms(); final Alarm alarm = AlarmDescriptorConverter.getAsAlarm(((AlarmExpiredEvent) event).getAlarmDescriptor()); getGuiHelper().showTrayMessageWithFallbackToDialog(resources.getString("alarm.event.triggered.title"), alarm.getDescription(), resources); getGuiHelper().playSoundInThread(alarm.getSound(), resources); } } }