A Reminder App using JavaFX - Java Source Code
AppGui.java
package com.javaquizplayer.examples.reminderapp; import javafx.scene.Parent; import javafx.scene.layout.VBox; import javafx.scene.layout.HBox; import javafx.scene.text.Text; import javafx.scene.paint.Color; import javafx.scene.control.TableView; import javafx.scene.control.TableColumn; import javafx.scene.control.ListView; import javafx.scene.control.DatePicker; import javafx.scene.control.Dialog; import javafx.scene.control.CheckBox; import javafx.scene.control.Button; import javafx.scene.control.ButtonType; import jfxtras.scene.control.LocalDatePicker; import javafx.scene.control.Alert; import javafx.scene.control.Alert.AlertType; import javafx.scene.control.cell.CheckBoxTableCell; import javafx.scene.control.cell.PropertyValueFactory; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.beans.value.ObservableValue; import javafx.beans.value.ChangeListener; import javafx.beans.property.SimpleBooleanProperty; import javax.annotation.PostConstruct; import java.time.LocalDate; import java.time.LocalTime; import java.util.Optional; import java.util.TimerTask; import java.util.Timer; import org.springframework.dao.DataAccessException; /* * Class builds the GUI for the app. * Gets the app's data from the database and shows in the reminders table. * Starts a Timer and schedules its reminder notification task. * Has functions to add, delete and update reminders. */ public class AppGui { private TableView<Reminder> table; private ListView<String> list; private LocalDatePicker localDatePicker; private Text actionStatus; private HBox hbPane; private Timer timer; private DataAccessException dataAccessException; private DataAccess dataAccess; private CheckRemindersTask remindersTask; private ReminderDialog reminderDialog; /* * Constructor. * Constructs the app's GUI. */ public AppGui(DataAccess dataAccess, CheckRemindersTask remindersTask, ReminderDialog reminderDialog) { this.dataAccess = dataAccess; this.remindersTask = remindersTask; this.reminderDialog = reminderDialog; // List view and calendar list = new ListView<String>(ReminderGroup.getAsFormattedStrings()); list.getSelectionModel().selectedIndexProperty().addListener( new ListSelectChangeListener()); localDatePicker = new LocalDatePicker(); // VBox with list and calendar VBox vbox1 = new VBox(15); vbox1.getChildren().addAll(list, localDatePicker); // Buttons in a hbox Button newBtn = new Button("New"); newBtn.setOnAction(actionEvent -> newReminderRoutine()); Button updBtn = new Button("Update"); updBtn.setOnAction(actionEvent -> updateReminderRoutine()); Button delBtn = new Button("Delete"); delBtn.setOnAction(actionEvent -> deleteReminderRoutine()); HBox btnHb = new HBox(10); btnHb.getChildren().addAll(newBtn, updBtn, delBtn); // Table view, columns and properties (row and column) table = new TableView<Reminder>(); TableColumn<Reminder, String> nameCol = new TableColumn<>("Name"); nameCol.setCellValueFactory(new PropertyValueFactory<Reminder, String>("name")); nameCol.setMaxWidth(1f * Integer.MAX_VALUE * 45); // 45% of table width TableViewRowAndCellFactories cellFactories = new TableViewRowAndCellFactories(); TableColumn<Reminder, LocalDate> dateCol = new TableColumn<>("Date"); dateCol.setMaxWidth(1f * Integer.MAX_VALUE * 15); dateCol.setCellValueFactory(new PropertyValueFactory<Reminder, LocalDate>("date")); dateCol.setCellFactory(column -> cellFactories.getTableCellWithDateFormatting()); TableColumn<Reminder, LocalTime> timeCol = new TableColumn<>("Time"); timeCol.setMaxWidth(1f * Integer.MAX_VALUE * 12); timeCol.setCellValueFactory(new PropertyValueFactory<Reminder, LocalTime>("time")); timeCol.setCellFactory(column -> cellFactories.getTableCellWithTimeFormatting()); TableColumn<Reminder, Boolean> priorityCol = new TableColumn<>("Priority"); priorityCol.setMaxWidth(1f * Integer.MAX_VALUE * 12); priorityCol.setCellValueFactory(new PropertyValueFactory<Reminder, Boolean>("priority")); priorityCol.setCellFactory(column -> { CheckBoxTableCell<Reminder, Boolean> cell = new CheckBoxTableCell<>(); cell.setAlignment(Pos.CENTER); return cell; }); TableColumn<Reminder, Boolean> completedCol = new TableColumn<>("Completed"); completedCol.setMaxWidth(1f * Integer.MAX_VALUE * 15); completedCol.setCellValueFactory(new PropertyValueFactory<Reminder, Boolean>("completed")); completedCol.setCellFactory(column -> { CheckBoxTableCell<Reminder, Boolean> cell = new CheckBoxTableCell<>(); cell.setAlignment(Pos.CENTER); return cell; }); table.getColumns().addAll(nameCol, dateCol,timeCol, priorityCol, completedCol); table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); table.setPrefWidth(600); table.setRowFactory(tableView -> cellFactories.getTooltipTableRow()); // Table row item double-click mouse event table.setOnMousePressed(mouseEvent -> { if ((mouseEvent.isPrimaryButtonDown()) && (mouseEvent.getClickCount() == 2)) { updateReminderRoutine(); } }); // Status message text actionStatus = new Text(); actionStatus.setFill(Color.FIREBRICK); // Vbox with buttons hbox, table view and status text VBox vbox2 = new VBox(15); vbox2.getChildren().addAll(btnHb, table, actionStatus); // Hbox with vbox1 and vbox2 hbPane = new HBox(15); hbPane.setPadding(new Insets(15)); hbPane.setAlignment(Pos.CENTER); hbPane.getChildren().addAll(vbox1, vbox2); } /* * Gets all reminders from database and populates the table. * Initiates the reminder timer task. */ @PostConstruct private void init() { try { dataAccess.loadRemindersFromDb(); } catch(DataAccessException ex) { dataAccessException = ex; return; } table.setItems(dataAccess.getAllReminders()); list.getSelectionModel().selectFirst(); table.requestFocus(); table.getSelectionModel().selectFirst(); initiateReminderTimer(); actionStatus.setText("Welcome to Reminders!"); } private void initiateReminderTimer() { timer = new Timer(); long zeroDelay = 0L; long period = 60000L; // 60 * 1000 = 1 min // The timer runs once (first time) for the overdue reminders // and subsequently the scheduled task every (one) minute timer.schedule(remindersTask, zeroDelay, period); } /* * The following three get methods are referred from the AppStarter class. */ public DataAccessException getAppDatabaseException() { return dataAccessException; } public Parent getView() { return hbPane; } public Timer getTimer() { return timer; } /* * Reminder group list's item selection change listener class. * On selecting a group the group's reminders are shown in the table. * For example, selecting Priority shows only the reminders with * priority == true. */ private class ListSelectChangeListener implements ChangeListener<Number> { @Override public void changed(ObservableValue<? extends Number> ov, Number oldVal, Number newVal) { int ix = newVal.intValue(); if (ix >= 0) { String groupStr = list.getItems().get(ix); ReminderGroup group = ReminderGroup.getGroup(groupStr); actionStatus.setText(""); table.setItems(dataAccess.getTableDataForGroup(group)); table.requestFocus(); table.getSelectionModel().selectFirst(); } } } /* * Routine for new reminder button action. * The new reminder is edited in the ReminderDialog * and is inserted in the table and the database. */ private void newReminderRoutine() { LocalDate reminderDate = localDatePicker.getLocalDate(); reminderDate = (reminderDate == null || reminderDate.isBefore(LocalDate.now())) ? LocalDate.now() : reminderDate; Dialog<Reminder> dialog = reminderDialog.create(reminderDate); Optional<Reminder> result = dialog.showAndWait(); if (result.isPresent()) { try { dataAccess.addReminder(result.get()); } catch(DataAccessException ex) { String msg = "A database error occurred while inserting the new reminder, exiting the app."; AppStarter.showExceptionAlertAndExitTheApp(ex, msg); } refreshTable(); actionStatus.setText("New reminder added."); table.getSelectionModel().selectLast(); } else { table.requestFocus(); } } /* * Refreshes the table with the updated rows after a * reminder is added, updated or deleted. */ private void refreshTable() { table.setItems(dataAccess.getAllReminders()); list.getSelectionModel().selectFirst(); table.requestFocus(); } /* * Routine for update reminder button action. * The selected reminder in the table is edited in the * ReminderDialog and is updated in the table and the database. */ public void updateReminderRoutine() { Reminder rem = table.getSelectionModel().getSelectedItem(); if ((table.getItems().isEmpty()) || (rem == null)) { return; } int ix = dataAccess.getAllReminders().indexOf(rem); Dialog<Reminder> dialog = reminderDialog.create(rem); Optional<Reminder> result = dialog.showAndWait(); if (result.isPresent()) { try { dataAccess.updateReminder(ix, result.get()); } catch(DataAccessException ex) { String msg = "A database error occurred while updating the reminder, exiting the app."; AppStarter.showExceptionAlertAndExitTheApp(ex, msg); } refreshTable(); actionStatus.setText("Reminder updated."); table.getSelectionModel().select(ix); } else { table.requestFocus(); } } /* * Routine for delete reminder button action. * Deletes the selected reminder from table and the database. */ private void deleteReminderRoutine() { Reminder rem = table.getSelectionModel().getSelectedItem(); if (table.getItems().isEmpty() || rem == null) { return; } Alert confirmAlert = getConfirmAlertForDelete(rem); Optional<ButtonType> result = confirmAlert.showAndWait(); if ((result.isPresent()) && (result.get() == ButtonType.OK)) { try{ dataAccess.deleteReminder(rem); } catch(DataAccessException ex) { String msg = "A database error occurred while deleting the reminder, exiting the app."; AppStarter.showExceptionAlertAndExitTheApp(ex, msg); } refreshTable(); actionStatus.setText("Reminder deleted."); table.getSelectionModel().selectFirst(); } else { table.requestFocus(); } } private Alert getConfirmAlertForDelete(Reminder rem) { Alert alert = new Alert(AlertType.CONFIRMATION); alert.setHeaderText(null); alert.setTitle("Reminder"); alert.setContentText("Delete reminder? " + rem); return alert; } }
Reminder.java
package com.javaquizplayer.examples.reminderapp; import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.SimpleBooleanProperty; import java.time.LocalDate; import java.time.LocalTime; import java.time.temporal.ChronoUnit; /* * Class represents a reminder. * * NOTE: The reminder's time is captured as LocalTime which is truncated to * the minutes.For example, 10:23:47.31987 is truncated to 10:23. * See the time attribute's setter method below. * Also, see the overridden equals method. */ public class Reminder { private SimpleStringProperty name; private SimpleStringProperty notes; private SimpleObjectProperty<LocalDate> date; private SimpleObjectProperty<LocalTime> time; private SimpleBooleanProperty priority; private SimpleBooleanProperty completed; public Reminder () { name = new SimpleStringProperty(); notes = new SimpleStringProperty(""); date = new SimpleObjectProperty<LocalDate>(); time = new SimpleObjectProperty<LocalTime>(); priority = new SimpleBooleanProperty(); completed = new SimpleBooleanProperty(false); } public String getName() { return name.get(); } public SimpleStringProperty nameProperty() { return name; } public void setName(String s) { name.set(s); } public String getNotes() { return notes.get(); } public void setNotes(String s) { notes.set(s); } public LocalDate getDate() { return date.get(); } public SimpleObjectProperty<LocalDate> dateProperty() { return date; } public void setDate(LocalDate d) { date.set(d); } public LocalTime getTime() { return time.get(); } public SimpleObjectProperty<LocalTime> timeProperty() { return time; } public void setTime(LocalTime t) { // time is stored truncated to minutes time.set(t.truncatedTo(ChronoUnit.MINUTES)); } public boolean getPriority() { return priority.get(); } public SimpleBooleanProperty priorityProperty() { return priority; } public void setPriority(boolean b) { priority.set(b); } public boolean getCompleted() { return completed.get(); } public SimpleBooleanProperty completedProperty() { return completed; } public void setCompleted(boolean b) { completed.set(b); } @Override public String toString() { return name.get(); } /* * Reminders are equal when their names are same on the same date. */ @Override public boolean equals(Object obj) { if (obj instanceof Reminder) { Reminder r = (Reminder) obj; if ((r.getName().equals(name.get())) && (r.getDate().isEqual(date.get()))) { return true; } } return false; } }
ReminderGroup.java
package com.javaquizplayer.examples.reminderapp; import java.util.Map; import java.util.HashMap; import java.util.List; import java.util.ArrayList; import java.util.EnumSet; import javafx.collections.FXCollections; import javafx.collections.ObservableList; /* * Enum defines the reminder groups. This enum also stores the enum constant * formatted strings as a List<String> and a Map<String, ReminderGroup> for * populating a ListView and lookup respectively. The formatted string * for a constant is for example, "Completed" and COMPLETED respectively. */ public enum ReminderGroup { REMINDERS, TODAY, OVERDUE, COMPLETED, PRIORITY; /* Map with group formatted string as key and enum constant as value */ private static final Map<String, ReminderGroup> map = new HashMap<>(); /* List with group as formatted strings */ private static final List<String> list = new ArrayList<>(); static { for(ReminderGroup group : EnumSet.allOf(ReminderGroup.class)) { String str = getEnumString(group); map.put(str, group); list.add(str); } } /* * Formats enum's string, for example: from REMINDERS to Reminders. */ private static String getEnumString(ReminderGroup group) { String s = group.toString().toLowerCase(); return (s.substring(0, 1).toUpperCase() + s.substring(1, s.length())); } public static ReminderGroup getGroup(String s) { return map.get(s); } public static ObservableList<String> getAsFormattedStrings() { return FXCollections.observableList(list); } }
TableViewRowAndCellFactories.java
package com.javaquizplayer.examples.reminderapp; import java.time.LocalDate; import java.time.LocalTime; import javafx.scene.control.TableRow; import javafx.scene.control.TableCell; import javafx.scene.control.Tooltip; import java.time.format.DateTimeFormatter; import javafx.geometry.Pos; /* * Class has methods to return row and cell factories to show * tooltip for each row and format date and time coulmns. These * are applied the table view's and its respective columns in the app. */ public class TableViewRowAndCellFactories { /* * Returns TableCell with custom date formatting: dd.MMM.yyyy (10.MAR.2017) */ public TableCell<Reminder, LocalDate> getTableCellWithDateFormatting() { TableCell<Reminder, LocalDate> cell = new TableCell<Reminder, LocalDate>() { @Override protected void updateItem(LocalDate date, boolean empty) { super.updateItem(date, empty); if (date == null || empty) { setText(null); } else { setText(date.format(DateTimeFormatter.ofPattern("dd.MMM.yyyy"))); } } }; cell.setAlignment(Pos.CENTER); return cell; } /* * Returns TableCell with custom time formatting: HH:mm (10:25) */ public TableCell<Reminder, LocalTime> getTableCellWithTimeFormatting() { TableCell<Reminder, LocalTime> cell = new TableCell<Reminder, LocalTime>() { @Override protected void updateItem(LocalTime time, boolean empty) { super.updateItem(time, empty); if (time == null || empty) { setText(null); } else { setText(time.format(DateTimeFormatter.ofPattern("HH:mm"))); } } }; cell.setAlignment(Pos.CENTER); return cell; } /* * Returns TableRow with tooltip - reminder notes text (upto 100 chars). */ public TableRow<Reminder> getTooltipTableRow() { TableRow<Reminder> row = new TableRow<Reminder>() { @Override public void updateItem(Reminder rem, boolean empty) { super.updateItem(rem, empty); if (empty) { setTooltip(null); } else { String notes = rem.getNotes(); if ((notes == null) || (notes.isEmpty())) { setTooltip(null); } else { notes = (notes.length() > 100) ? notes.substring(0, 100) + "..." : notes; setTooltip(new Tooltip(notes)); } } } }; return row; } }
ReminderDialog.java
package com.javaquizplayer.examples.reminderapp; import javafx.scene.layout.VBox; import javafx.scene.layout.HBox; import javafx.scene.control.Dialog; import javafx.scene.control.TextField; import javafx.scene.control.TextArea; import javafx.scene.control.CheckBox; import javafx.scene.control.Button; import javafx.scene.control.ButtonType; import javafx.scene.control.ButtonBar.ButtonData; import javafx.scene.control.Tooltip; import javafx.scene.control.DatePicker; import javafx.scene.control.DateCell; import javafx.scene.control.Alert; import javafx.scene.control.Alert.AlertType; import javafx.scene.control.TextFormatter; import javafx.event.ActionEvent; import javafx.geometry.Pos; import javafx.geometry.Insets; import jfxtras.scene.control.LocalTimePicker; import java.time.LocalDate; import java.time.LocalTime; import java.util.Optional; /* * Class has methods to create dialogs to accept Reminder info. * There are two methods: for creating a new reminder and update an existing one. * The methods return respective dialog to the app. The reminder data is * validated before returning the reminder value to the application. */ public class ReminderDialog { private DataAccess dataAccess; private boolean newReminder; // indicates if a reminder is new or not /* Reminder name - maximum and minimum characters allowed. */ private static final int NAME_MAX_CHARS = 25; private static final int NAME_MIN_CHARS = 5; /* Reminder notes - maximum characters allowed. */ private static final int NOTES_MAX_CHARS = 200; public ReminderDialog(DataAccess dataAccess) { this.dataAccess = dataAccess; } /* * Returns a dialog for new Reminders. The method accepts the * selected date from the LocalDatePicker in the app. */ public Dialog<Reminder> create(LocalDate reminderDate) { newReminder = true; Dialog<Reminder> dialog = getDialog("New Reminder"); TextField textFld = getTextField(""); CheckBox priorityCheckBox = getPriorityCheckbox(false); TextArea textArea = getTextArea(""); DatePicker datePicker = getDatePicker(reminderDate); LocalTimePicker timePicker = getTimePicker(LocalTime.now()); Optional<CheckBox> completedCheckBoxOpt = Optional.empty(); HBox hb = getHBoxWithWidgets(textFld, priorityCheckBox); VBox vb = getVBoxWithWidgets(hb, textArea, datePicker, timePicker, completedCheckBoxOpt); dialog.getDialogPane().setContent(vb); ButtonType okButtonType = getDialogButtonType(dialog); String oldName = ""; LocalDate oldDate = null; Button okButton = getOkButton(dialog, okButtonType, oldName, textFld, oldDate, datePicker, timePicker); setResultConverterOnDialog(dialog, textFld, textArea, datePicker, timePicker, priorityCheckBox, completedCheckBoxOpt, okButtonType); return dialog; } private Dialog<Reminder> getDialog(String title) { Dialog<Reminder> dialog = new Dialog<>(); dialog.setTitle(title); dialog.setResizable(false); return dialog; } private TextField getTextField(String name) { TextField textFld = new TextField(name); textFld.setPrefColumnCount(20); textFld.setTooltip(new Tooltip("Reminder name must be " + NAME_MIN_CHARS + " to " + NAME_MAX_CHARS + " characters")); textFld.setPromptText("Reminder name (" + NAME_MIN_CHARS + " to " + NAME_MAX_CHARS + " chars)"); return textFld; } private TextArea getTextArea(String text) { TextArea textArea = new TextArea(text); textArea.setPromptText("Notes upto " + NOTES_MAX_CHARS + " characters."); textArea.setTooltip(new Tooltip("Reminder notes upto " + NOTES_MAX_CHARS + " characters")); textArea.setPrefRowCount(3); textArea.setPrefColumnCount(20); textArea.setWrapText(true); // The text formatter limits the number of characters entered in // the notes text area to NOTES_MAX_CHARS. // The text formatter has a filter as constructor parameter (is of type // UnaryOperator<TextFormatter.Change>). textArea.setTextFormatter(new TextFormatter<String>(change -> change.getControlNewText().length() <= NOTES_MAX_CHARS ? change : null)); return textArea; } private CheckBox getPriorityCheckbox(boolean isChecked) { CheckBox checkBox = new CheckBox("Priority"); checkBox.setSelected(isChecked); return checkBox; } private CheckBox getCompletedCheckbox(boolean isChecked) { CheckBox checkBox = new CheckBox("Completed"); checkBox.setSelected(isChecked); return checkBox; } private DatePicker getDatePicker(LocalDate date) { DatePicker datePicker = new DatePicker(date); datePicker.setTooltip(new Tooltip("Reminder date: click the icon to open calendar")); datePicker.setEditable(false); datePicker.setDayCellFactory(dtPicker -> { return getCellWithDateDisabledBeforeToday(); }); return datePicker; } private DateCell getCellWithDateDisabledBeforeToday() { DateCell cell = new DateCell() { @Override public void updateItem(LocalDate date, boolean empty) { super.updateItem(date, empty); if ((! empty) && (date.isBefore(LocalDate.now()))) { setDisable(true); } } }; return cell; } private LocalTimePicker getTimePicker(LocalTime time) { LocalTimePicker timePicker = new LocalTimePicker(time); timePicker.setTooltip(new Tooltip("Reminder time: slide knobs to set the hour and minutes")); return timePicker; } private HBox getHBoxWithWidgets(TextField textFld, CheckBox checkBox) { HBox hb = new HBox(15); hb.setAlignment(Pos.BASELINE_CENTER); hb.getChildren().addAll(textFld, checkBox); return hb; } private VBox getVBoxWithWidgets(HBox hb, TextArea text, DatePicker datePicker, LocalTimePicker timePicker, Optional%lt;CheckBox> checkBoxOptional) { VBox vb = new VBox(15); vb.setPadding(new Insets(15)); if (checkBoxOptional.isPresent()) { vb.getChildren().addAll(hb, text, datePicker, timePicker, checkBoxOptional.get()); } else { // Completed check box doesn't exist in case of a new reminder vb.getChildren().addAll(hb, text, datePicker, timePicker); } return vb; } private ButtonType getDialogButtonType(Dialog dialog) { ButtonType okButtonType = new ButtonType("Okay", ButtonData.OK_DONE); dialog.getDialogPane().getButtonTypes().add(okButtonType); return okButtonType; } /* * Constructs the 'Okay' button for the dialog and registers an ActionEvent * event filter with it. The event handler validates the input data. * In case the input data is not valid, the event is stopped from further * propagation (doesn't allow the click okay button action). */ private Button getOkButton(Dialog dialog, ButtonType okButtonType, String oldName, TextField textFld, LocalDate oldDate, DatePicker datePicker, LocalTimePicker timePicker) { Button okButton = (Button) dialog.getDialogPane().lookupButton(okButtonType); okButton.addEventFilter(ActionEvent.ACTION, event -> { if (! validate(oldName, textFld.getText(), oldDate, datePicker.getValue(), timePicker.getLocalTime())) { showValidationAlertDialog(); event.consume(); } }); return okButton; } /* * Reminder validation in this dialog. * The name must be between NAME_MIN_CHARS and NAME_MAX_CHARS characters * and unique for a date. Date and time must be in future for new reminders. */ private boolean validate(String oldName, String name, LocalDate oldDate, LocalDate date, LocalTime time) { name = name.trim(); if ((name.length() < NAME_MIN_CHARS) || (name.length() > NAME_MAX_CHARS)) { return false; } if ((newReminder) && (LocalDate.now().isEqual(date)) && (LocalTime.now().isAfter(time))) { return false; } if ((! newReminder) && (name.equals(oldName)) && (date.isEqual(oldDate))) { return true; } for (Reminder r : dataAccess.getAllReminders()) { if ((r.getName().equals(name)) && (r.getDate().isEqual(date))) { return false; } } return true; } private void showValidationAlertDialog() { Alert alert = new Alert(AlertType.NONE); alert.setTitle("Reminder"); alert.getDialogPane().getButtonTypes().add(ButtonType.OK); String s = "Name must be " + NAME_MIN_CHARS + " to " + NAME_MAX_CHARS + " characters and should be unique within a date."; String s1 = (newReminder) ? " The date and time must be in future." : ""; alert.setContentText(s + s1); alert.show(); } private void setResultConverterOnDialog(Dialog dialog, TextField textFld, TextArea textArea, DatePicker datePicker, LocalTimePicker timePicker, CheckBox priority, Optional<CheckBox> completedOpt, ButtonType okButtonType) { // The result converter returns the entered Reminder instance in case // of 'Okay' button is clicked, or null in case the dialog is cancelled. // The reminder is built from the dialog's widget values after validation. // This reminder instance is returned by the dialog as an Optional in the app. dialog.setResultConverter(buttonType -> { if (buttonType == okButtonType) { Reminder rem = new Reminder(); rem.setName(textFld.getText().trim()); rem.setNotes(textArea.getText().trim()); rem.setDate(datePicker.getValue()); rem.setTime(timePicker.getLocalTime()); rem.setPriority(priority.isSelected()); if (completedOpt.isPresent()) { rem.setCompleted(completedOpt.get().isSelected()); } else { // Completed checkbox doesn't exist in case of a new reminder rem.setCompleted(false); } return rem; } return null; }); } /* * Returns a dialog for updating reminders. The method accepts the * reminder being updated. */ public Dialog<Reminder> create(Reminder rem) { newReminder = false; String oldName = rem.getName(); LocalDate oldDate = rem.getDate(); Dialog<Reminder> dialog = getDialog("Reminder"); TextField textFld = getTextField(oldName); CheckBox priorityCheckBox = getPriorityCheckbox(rem.getPriority()); TextArea textArea = getTextArea(rem.getNotes()); DatePicker datePicker = getDatePicker(oldDate); LocalTimePicker timePicker = getTimePicker(rem.getTime()); CheckBox completedCheckBox = getCompletedCheckbox(rem.getCompleted()); Optional<CheckBox> completedCheckBoxOpt = Optional.of(completedCheckBox); HBox hb = getHBoxWithWidgets(textFld, priorityCheckBox); VBox vb = getVBoxWithWidgets(hb, textArea, datePicker, timePicker, completedCheckBoxOpt); dialog.getDialogPane().setContent(vb); ButtonType okButtonType = getDialogButtonType(dialog); Button okButton = getOkButton(dialog, okButtonType, oldName, textFld, oldDate, datePicker, timePicker); setResultConverterOnDialog(dialog, textFld, textArea, datePicker, timePicker, priorityCheckBox, completedCheckBoxOpt, okButtonType); return dialog; } }
CheckRemindersTask.java
package com.javaquizplayer.examples.reminderapp; import java.time.LocalTime; import java.time.LocalDate; import java.time.format.FormatStyle; import java.time.format.DateTimeFormatter; import java.awt.Toolkit; import java.awt.geom.Point2D; import java.util.List; import java.util.ArrayList; import java.util.TimerTask; import java.util.stream.Collectors; import java.util.function.Predicate; import javafx.scene.control.Alert; import javafx.scene.control.Alert.AlertType; import javafx.scene.control.ButtonType; import javafx.stage.Screen; import javafx.geometry.Rectangle2D; import javafx.application.Platform; import javafx.collections.ObservableList; /* * Class extends TimerTask and overrides its run() method. * The class is the input task for the Timer which schedules the * tasks and is initiated from the app. The tasks: * (i) one task at the start of the app and (ii) subsequent tasks * at one minute interval thru the duration of the app. * The first task notifies all the overdue reminders. The later * tasks notify the due reminder at that minute. * The reminder notification is shown in an Alert. */ public class CheckRemindersTask extends TimerTask { private DataAccess dataAccess; /* * The predicate initially is set to this value, which is applied only once. * The second and subsequent execution of the timer task uses * ReminderPredicates.now. See the run() method. */ private Predicate<Reminder> predicate = ReminderPredicates.OVER_DUE; private List<Point2D.Double> alertPositions; private int nextAlertPosIndex; public CheckRemindersTask(DataAccess dataAccess) { this.dataAccess = dataAccess; createAlertPositions(); nextAlertPosIndex = 0; } /* * When a reminder is due a notification is shown in an alert. * When there are multiple alerts at the same minute they are * positioned on the screen without overlapping each other. * The positions are pre-defined and stored in a collection. */ private void createAlertPositions() { Rectangle2D r2d = Screen.getPrimary().getVisualBounds(); double x = r2d.getWidth() / 5; double y = r2d.getHeight() / 5; Point2D.Double position = null; alertPositions = new ArrayList<Point2D.Double>(); for (int i = 0; i < 10; i++) { position = new Point2D.Double(x, y); alertPositions.add(position); x += 25; y += 25; } } /* * Filters all the reminders with supplied criteria (predicate) * and shows the reminder alerts in the app. */ @Override public void run() { List<Reminder> dueRems = getDueRems(); showNotifications(dueRems); predicate = ReminderPredicates.DUE_NOW; } private List<Reminder> getDueRems() { ObservableList<Reminder> rems = dataAccess.getAllReminders(); return rems.stream() .filter(predicate) .collect(Collectors.toList()); } /* * Show notifications; * muliple alerts at the same time at different positions. * The alerts are displayed at 3 second interval. */ private void showNotifications(List<Reminder> remsNow) { for (Reminder r : remsNow) { Platform.runLater(() -> showReminderAlert(r)); try { Thread.sleep(3000); // 3 seconds } catch (InterruptedException e) { } } } private void showReminderAlert(Reminder rem) { Alert alert = new Alert(AlertType.NONE); // a modal alert // Format alert content String name = rem.getName(); alert.setTitle("Reminder - " + name); alert.getDialogPane().getButtonTypes().add(ButtonType.OK); String content = name + " " + (rem.getPriority() ? "[Priority]" : "") + "\n" + rem.getDate().format(DateTimeFormatter.ofLocalizedDate(FormatStyle.LONG)) + " " + rem.getTime().format(DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT)); alert.setContentText(content); // Get alert's position Point2D.Double p = getNextAlertPosition(); alert.setX(p.getX()); alert.setY(p.getY()); // Display alert alert.show(); Toolkit.getDefaultToolkit().beep(); } private Point2D.Double getNextAlertPosition() { Point2D.Double p = alertPositions.get(nextAlertPosIndex); nextAlertPosIndex = (nextAlertPosIndex == 9) ? nextAlertPosIndex = 0 : ++nextAlertPosIndex; return p; } }
ReminderPredicates.java
package com.javaquizplayer.examples.reminderapp; import java.util.function.Predicate; import java.time.LocalDate; import java.time.LocalTime; import java.time.temporal.ChronoUnit; /* * Class has definitions of Predicates used in this app. These are defined as * public static members and are used in various classes. */ public class ReminderPredicates { public static final Predicate<Reminder> ALL = r -> true; public static final Predicate<Reminder> COMPLETED = r -> (r.getCompleted() == true); public static final Predicate<Reminder> PRIORITY = r -> (r.getPriority() == true); public static final Predicate<Reminder> TODAYS = r -> r.getDate().isEqual(LocalDate.now()); private static final Predicate<Reminder> PAST_DAYS = r -> r.getDate().isBefore(LocalDate.now()); private static final Predicate<Reminder> OVER_DUE_TODAYS_TIME = r -> r.getTime().isBefore(LocalTime.now()); public static final Predicate<Reminder> OVER_DUE = COMPLETED.negate() .and(PAST_DAYS .or(TODAYS .and(OVER_DUE_TODAYS_TIME))); private static final Predicate<Reminder> TIME_NOW = r -> r.getTime().equals(LocalTime.now().truncatedTo(ChronoUnit.MINUTES)); public static final Predicate<Reminder> DUE_NOW = TODAYS.and(TIME_NOW).and(COMPLETED.negate()); }
DataAccess.java
package com.javaquizplayer.examples.reminderapp; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import java.util.List; import java.util.stream.Collectors; import java.util.function.Predicate; /* * Class is a data accessor for the app. Interacts with the database access * program DatabaseJdbcAccess. Maintains the reminders data in a collection. * Has methods to get all, add, update and delete reminders used by the app. * Also, has a method to return reminders for a given ReminderGroup. */ public class DataAccess { private DatabaseJdbcAccess dbAccess; private ObservableList<Reminder> remData; public DataAccess(DatabaseJdbcAccess databaseJdbcAccess) { dbAccess = databaseJdbcAccess; } /* * This method retrieves all the reminders from the database at the start * of the application. The reminders are populated in the GUI and also * stored locally in this class as a collection. */ public void loadRemindersFromDb() { remData = FXCollections.observableList(dbAccess.getAllReminders()); } public ObservableList<Reminder> getAllReminders() { return remData; } public void addReminder(Reminder rem) { remData.add(rem); dbAccess.addReminder(rem); } public void updateReminder(int ix, Reminder rem) { Reminder old = remData.get(ix); remData.set(ix, rem); dbAccess.updateReminder(old, rem); } public void deleteReminder(Reminder rem) { remData.remove(rem); dbAccess.deleteReminder(rem); } public ObservableList<Reminder> getTableDataForGroup(ReminderGroup group) { Predicate<Reminder> remPredicate = null; switch (group) { case PRIORITY: remPredicate = ReminderPredicates.PRIORITY; break; case COMPLETED: remPredicate = ReminderPredicates.COMPLETED; break; case TODAY: remPredicate = ReminderPredicates.TODAYS; break; case OVERDUE: remPredicate = ReminderPredicates.OVER_DUE; break; default: remPredicate = ReminderPredicates.ALL; } List<Reminder> result = remData.stream() .filter(remPredicate) .collect(Collectors.toList()); return FXCollections.observableList(result); } }
AppConfig.java
package com.javaquizplayer.examples.reminderapp; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Import; @Configuration @Import(DatabaseJdbcConfig.class) public class AppConfig { @Bean public AppGui appGui(DataAccess dataAccess, CheckRemindersTask checkRemindersTask, ReminderDialog reminderDialog) { return new AppGui(dataAccess, checkRemindersTask, reminderDialog); } @Bean public ReminderDialog reminderDialog(DataAccess dataAccess) { return new ReminderDialog(dataAccess); } @Bean public CheckRemindersTask checkRemindersTask(DataAccess dataAccess) { return new CheckRemindersTask(dataAccess); } @Bean public DataAccess dataAccess(DatabaseJdbcAccess databaseJdbcAccess) { return new DataAccess(databaseJdbcAccess); } }
DatabaseJdbcConfig.java
package com.javaquizplayer.examples.reminderapp; import javax.sql.DataSource; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.datasource.SingleConnectionDataSource; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class DatabaseJdbcConfig { private static final String JDBC_DRIVER = "org.hsqldb.jdbc.JDBCDriver"; /* ifexists=true connection property disallows creating a new database.*/ private static final String CONNECTION_URL = "jdbc:hsqldb:file:db/remindersDB;ifexists=true;"; @Bean public DataSource dataSource() { // SingleConnectionDataSource wraps a single JDBC Connection // which is not closed after use. SingleConnectionDataSource ds = new SingleConnectionDataSource(); ds.setDriverClassName(JDBC_DRIVER); ds.setUrl(CONNECTION_URL); ds.setUsername(""); ds.setPassword(""); return ds; } @Bean public JdbcTemplate jdbcTemplate(DataSource dataSource) { return new JdbcTemplate(dataSource); } @Bean public DatabaseJdbcAccess databaseJdbcAccess(JdbcTemplate jdbcTemplate) { return new DatabaseJdbcAccess(jdbcTemplate); } }
DatabaseJdbcAccess.java
package com.javaquizplayer.examples.reminderapp; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.RowMapper; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Date; import java.sql.Time; import java.util.List; import java.time.LocalDate; import java.time.LocalTime; /* * Class has methods to get all, add, update and delete reminders from the * database. Shutsdown the database. * * NOTE: The database date and time column types are compatible with java.sql.Date * and Time respectively. The app captures date and time as java.time.LocalDate * and LocalTime respectively. These are converted to and from using the methods * in java.sql.Date and Time (for example, Date.valueOf(LocalDate) and * Date.toLocalDate()). */ public class DatabaseJdbcAccess { private JdbcTemplate jdbcTemplate; private static final String GET_ALL_REMINDERS = "SELECT * FROM REMINDERS_TABLE"; private static final String INSERT_REMINDER = "INSERT INTO REMINDERS_TABLE VALUES (?, ?, ?, ?, ?, ?)"; private static final String UPDATE_REMINDER = "UPDATE REMINDERS_TABLE SET name=?, notes=?, date=?, time=?, priority=?, completed=? WHERE name=? AND date=?"; private static final String DELETE_REMINDER = "DELETE FROM REMINDERS_TABLE WHERE name=? AND date=?"; public DatabaseJdbcAccess(JdbcTemplate template) { jdbcTemplate = template; } public List<Reminder> getAllReminders() { return jdbcTemplate.query(GET_ALL_REMINDERS, new RowMapper<Reminder>() { @Override public Reminder mapRow(ResultSet rs, int rowNum) throws SQLException { Reminder r = new Reminder(); r.setName(rs.getString("name")); r.setNotes(rs.getString("notes")); r.setDate(rs.getDate("date").toLocalDate()); r.setTime(rs.getTime("time").toLocalTime()); r.setPriority(rs.getBoolean("priority")); r.setCompleted(rs.getBoolean("completed")); return r; } } ); } public void addReminder(Reminder r) { jdbcTemplate.update(INSERT_REMINDER, r.getName(), r.getNotes(), Date.valueOf(r.getDate()), Time.valueOf(r.getTime()), r.getPriority(), r.getCompleted() ); } public void updateReminder(Reminder old, Reminder r) { jdbcTemplate.update(UPDATE_REMINDER, r.getName(), r.getNotes(), Date.valueOf(r.getDate()), Time.valueOf(r.getTime()), r.getPriority(), r.getCompleted(), old.getName(), Date.valueOf(old.getDate()) ); } public void deleteReminder(Reminder r) { jdbcTemplate.update(DELETE_REMINDER, r.getName(), Date.valueOf(r.getDate()) ); } public void shutdownDatabase() { // SHUTDOWN is a HSQLDB command to close the database. jdbcTemplate.execute("SHUTDOWN"); } }
AppStarter.java
package com.javaquizplayer.examples.reminderapp; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.support.AbstractApplicationContext; import org.springframework.dao.DataAccessException; import javafx.application.Application; import javafx.application.Platform; import javafx.stage.Stage; import javafx.scene.Scene; import javafx.scene.Parent; import javafx.scene.control.Alert; import javafx.scene.control.Alert.AlertType; import java.util.Timer; import java.awt.Toolkit; /* * The reminder app's starter program. * Loads the Spring's application context and launches the main GUI. */ public class AppStarter extends Application { private AbstractApplicationContext context; public static void main(String... args) { Application.launch(args); } /* * Loads the spring application context from the Java-based configuration. * Gets the main GUI and displays. * In case of an initial database exception, shows an alert and * terminates the app. */ @Override public void start(Stage primaryStage) { context = new AnnotationConfigApplicationContext(AppConfig.class); AppGui appGui = context.getBean(AppGui.class); Parent mainView = appGui.getView(); primaryStage.setScene(new Scene(mainView, 925, 450)); // w x h primaryStage.setResizable(false); primaryStage.setTitle("Reminders App"); primaryStage.show(); // Check if any database exception while loading initial data. // Exit the app in such case. DataAccessException ex = appGui.getAppDatabaseException(); if (ex != null) { String msg = "There is an error while loading reminders from the database, exiting the app."; showExceptionAlertAndExitTheApp(ex, msg); } } /* * NOTE: this method is commonly acccessed from the AppGui class. */ public static void showExceptionAlertAndExitTheApp(Exception ex, String msg) { System.out.println("#### Error Message: " + ex.getMessage()); Toolkit.getDefaultToolkit().beep(); Alert alert = new Alert(AlertType.ERROR); alert.setTitle("Reminders - Error");; alert.setContentText(msg); alert.showAndWait(); Platform.exit(); } /* * Shutsdown the database. * Terminates the timer discarding the scheduled tasks. * Closes this application context destroying all beans. */ @Override public void stop() { context.getBean(DatabaseJdbcAccess.class).shutdownDatabase(); Timer timer = context.getBean(AppGui.class).getTimer(); if (timer != null) { timer.cancel(); } context.close(); } }
Return to top
This page uses Java code formatting from http://hilite.me/.