package jfxtras.internal.scene.control.skin.agenda.icalendar.base24hour.popup;
import java.io.IOException;
import java.net.URL;
import java.time.DateTimeException;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit;
import java.time.temporal.Temporal;
import java.time.temporal.TemporalAmount;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.ResourceBundle;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonBar.ButtonData;
import javafx.scene.control.ButtonType;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Label;
import javafx.scene.control.Tab;
import javafx.scene.control.TextArea;
import javafx.scene.control.TextField;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.VBox;
import javafx.util.Callback;
import jfxtras.icalendarfx.components.VComponent;
import jfxtras.icalendarfx.components.VDisplayable;
import jfxtras.icalendarfx.properties.component.descriptive.Categories;
import jfxtras.icalendarfx.properties.component.descriptive.Description;
import jfxtras.icalendarfx.properties.component.descriptive.Location;
import jfxtras.icalendarfx.properties.component.descriptive.Summary;
import jfxtras.icalendarfx.properties.component.time.DateTimeEnd;
import jfxtras.icalendarfx.properties.component.time.DateTimeStart;
import jfxtras.icalendarfx.utilities.DateTimeUtilities;
import jfxtras.internal.scene.control.skin.agenda.icalendar.base24hour.CategorySelectionGridPane;
import jfxtras.internal.scene.control.skin.agenda.icalendar.base24hour.Settings;
import jfxtras.scene.control.LocalDateTextField;
import jfxtras.scene.control.LocalDateTimeTextField;
import jfxtras.scene.control.agenda.TemporalUtilities;
import jfxtras.scene.control.agenda.icalendar.ICalendarAgenda;
/**
* Base controller for editing descriptive properties in a {@link VDisplayable} component.
* Edits the following properties: {@link DateTimeStart}, {@link DateTimeEnd}, {@link Summary}, {@link Description}
* {@link Location}, {@link Categories}
* <p>
* When a {@link VComponent} has a {@link DateTimeStart }as a date only (no time) and changes to date/time the
* control uses {@link ZonedDateTime} date/time with {@link DEFAULT_ZONE_ID} time zone.
* <p>The {@link ICalendarAgenda} control has a number of features, including:
*
* @author David Bal
*
* @param <T> subclass of {@link VDisplayable}
*/
public abstract class EditDescriptiveVBox<T extends VDisplayable<T>> extends VBox
{
@FXML private ResourceBundle resources; // ResourceBundle that was given to the FXMLLoader
public ResourceBundle getResources() { return resources; }
final static ZoneId DEFAULT_ZONE_ID = ZoneId.systemDefault(); // can be changed to "Z" if preferred
@FXML GridPane timeGridPane; // contains either LocalDateTextField or LocalDateTimeTextField depending if wholeDayCheckBox is selected
LocalDateTimeTextField startDateTimeTextField = new LocalDateTimeTextField(); // start of recurrence
LocalDateTextField startDateTextField = new LocalDateTextField(); // start of recurrence when wholeDayCheckBox is selected
protected final static LocalTime DEFAULT_START_TIME = LocalTime.of(10, 0); // default time
@FXML Label endLabel;
@FXML private CheckBox wholeDayCheckBox;
@FXML TextField summaryTextField;
@FXML TextArea descriptionTextArea;
@FXML Label locationLabel;
@FXML TextField locationTextField;
@FXML TextField categoryTextField;
@FXML private CategorySelectionGridPane categorySelectionGridPane;
@FXML private Button saveComponentButton;
@FXML private Button cancelComponentButton;
@FXML private Button saveRepeatButton;
@FXML private Button cancelRepeatButton;
@FXML private Button deleteComponentButton;
@FXML private Tab appointmentTab;
@FXML private Tab repeatableTab;
public EditDescriptiveVBox( )
{
super();
loadFxml(EditDescriptiveVBox.class.getResource("EditDescriptive.fxml"), this);
categorySelectionGridPane.getStylesheets().addAll(getStylesheets());
startDateTimeTextField.setId("startDateTimeTextField");
startDateTextField.setId("startDateTextField");
startDateTimeTextField.setId("startDateTimeTextField");
}
final private ChangeListener<? super LocalDate> startDateTextListener = (observable, oldValue, newValue) -> synchStartDate(oldValue, newValue);
/** Update startDateTimeTextField when startDateTextField changes */
void synchStartDate(LocalDate oldValue, LocalDate newValue)
{
startRecurrenceProperty.set(newValue);
startDateTimeTextField.localDateTimeProperty().removeListener(startDateTimeTextListener);
LocalDateTime newDateTime = startDateTimeTextField.getLocalDateTime().with(newValue);
startDateTimeTextField.setLocalDateTime(newDateTime);
startDateTimeTextField.localDateTimeProperty().addListener(startDateTimeTextListener);
}
final private ChangeListener<? super LocalDateTime> startDateTimeTextListener = (observable, oldValue, newValue) -> synchStartDateTime(oldValue, newValue);
/** Update startDateTextField when startDateTimeTextField changes */
void synchStartDateTime(LocalDateTime oldValue, LocalDateTime newValue)
{
// System.out.println("start date2:" + newValue);
if (startOriginalRecurrence.isSupported(ChronoUnit.NANOS)) // ZoneDateTime, LocalDateTime
{
startRecurrenceProperty.set(startOriginalRecurrence.with(newValue));
} else // LocalDate - use ZonedDateTime at system default ZoneId
{
startRecurrenceProperty.set(ZonedDateTime.of(newValue, DEFAULT_ZONE_ID));
}
startDateTextField.localDateProperty().removeListener(startDateTextListener);
LocalDate newDate = LocalDate.from(startDateTimeTextField.getLocalDateTime());
startDateTextField.setLocalDate(newDate);
startDateTextField.localDateProperty().addListener(startDateTextListener);
}
// Callback for LocalDateTimeTextField that is called when invalid date/time is entered
protected final Callback<Throwable, Void> errorCallback = (throwable) ->
{
Alert alert = new Alert(AlertType.ERROR);
alert.setTitle("Invalid Date or Time");
alert.setContentText("Please enter valid date and time");
alert.showAndWait();
return null;
};
T vComponentEdited;
private String initialCategory;
Temporal startOriginalRecurrence;
/** Contains the actual start recurrence value - Temporal LocalDate or LocalDateTime
* depending on wholeDayCheckBox */
ObjectProperty<Temporal> startRecurrenceProperty;
/**
* Provide necessary data to setup
*
* @param vComponent - component to be edited
* @param startRecurrence - start of selected recurrence
* @param endRecurrence - end of selected recurrence
* @param categories - list of category names
*/
public void setupData(
T vComponent,
Temporal startRecurrence,
Temporal endRecurrence,
List<String> categories)
{
startOriginalRecurrence = startRecurrence;
startRecurrenceProperty = new SimpleObjectProperty<>(startRecurrence);
vComponentEdited = vComponent;
// Disable repeat rules for events with recurrence-id
if (vComponentEdited.getRecurrenceDates() != null)
{ // recurrence recurrences can't add repeat rules (only parent can have repeat rules)
repeatableTab.setDisable(true);
repeatableTab.setTooltip(new Tooltip(resources.getString("repeat.tab.unavailable")));
}
// String bindings
String initialSummary = (vComponent.getSummary() == null || vComponent.getSummary().getValue() == null) ? "" : vComponent.getSummary().getValue();
summaryTextField.setText(initialSummary);
summaryTextField.textProperty().addListener((obs, oldValue, newValue) -> vComponent.setSummary(newValue));
// START DATE/TIME
startDateTimeTextField.setLocale(Locale.getDefault());
startDateTextField.setLocale(Locale.getDefault());
startDateTimeTextField.setParseErrorCallback(errorCallback);
startDateTextField.setParseErrorCallback(errorCallback);
final LocalDateTime start1;
final LocalDate start2 = LocalDate.from(startRecurrence);
if (vComponentEdited.isWholeDay())
{
start1 = LocalDate.from(startRecurrence).atTime(DEFAULT_START_TIME);
} else
{
start1 = TemporalUtilities.toLocalDateTime(startRecurrence);
}
startDateTimeTextField.setLocalDateTime(start1);
startDateTextField.setLocalDate(start2);
startDateTimeTextField.localDateTimeProperty().addListener(startDateTimeTextListener);
startDateTextField.localDateProperty().addListener(startDateTextListener);
// WHOLE DAY
wholeDayCheckBox.setSelected(vComponentEdited.isWholeDay());
handleWholeDayChange(vComponentEdited, wholeDayCheckBox.isSelected());
wholeDayCheckBox.selectedProperty().addListener((observable, oldSelection, newSelection) -> handleWholeDayChange(vComponent, newSelection));
// CATEGORIES
// initialize with empty category, to be removed later if not populated with other value.
if (vComponentEdited.getCategories() == null)
{
vComponentEdited.withCategories("");
categoryTextField.setDisable(true);
categoryTextField.setPromptText("Select a category color box"); // TODO - ADD TO RESOURCE BUNDLE
}
categorySelectionGridPane.categorySelectedProperty().addListener(
(observable, oldSelection, newSelection) ->
{
Integer i = categorySelectionGridPane.getCategorySelected();
String newText = categories.get(i);
categoryTextField.setDisable(false);
categoryTextField.setPromptText("Enter category name"); // TODO - ADD TO RESOURCE BUNDLE
categoryTextField.setText(newText);
});
// store group name changes by each character typed
categoryTextField.textProperty().addListener((observable, oldSelection, newSelection) ->
{
int i = categorySelectionGridPane.getCategorySelected();
if (! categories.get(i).equals(newSelection))
{
// ideally, categories list will be a LinkedList to reduce cost of element removal and addition
categories.remove(i);
categories.add(i, newSelection);
}
categorySelectionGridPane.updateToolTip(i, categories.get(i));
vComponentEdited.getCategories().get(0).setValue(Arrays.asList(newSelection)); // keep Categories to maintain order
});
// verify category is unique
categoryTextField.focusedProperty().addListener((obs, oldValue, newValue) ->
{
if (newValue)
{
// save initial value
initialCategory = categoryTextField.getText();
} else
{
int selectedIndex = categorySelectionGridPane.getCategorySelected();
int otherMatch = -1;
for (int i=0; i<categories.size(); i++)
{
if (i == selectedIndex) continue;
if (categories.get(i).equals(categoryTextField.getText()))
{
otherMatch = ++i;
break;
}
}
if (otherMatch >= 0)
{
invalidCategoryAlert(categoryTextField.getText(), otherMatch);
categoryTextField.setText(initialCategory);
}
}
});
String initialCategory = vComponentEdited.getCategories().get(0).getValue().get(0);
categorySelectionGridPane.setupData(initialCategory, categories);
startDateTimeTextField.localDateTimeProperty().addListener(dateTimeStartListener);
// vComponentEdited.getDateTimeStart().valueProperty().addListener(dateTimeStartListener);
}
/** Synch recurrence dates when DTSTART is modified (can occur when {@link synchStartDatePickerAndComponent#startDatePicker} changes */
ChangeListener<? super Temporal> dateTimeStartListener = (obs, oldValue, newValue) ->
{
// If recurrence before or equal to DTSTART adjust to match DTSTART
// if recurrence is after DTSTART don't adjust it
LocalDate d1 = LocalDate.from(startRecurrenceProperty.get());
LocalDate d2 = LocalDate.from(newValue);
LocalDate d3 = LocalDate.from(oldValue);
if ((! DateTimeUtilities.isAfter(d1, d2)) || d1.equals(d3))
{
Temporal r = newValue.with(startDateTextField.getLocalDate());
TemporalAmount shift = DateTimeUtilities.temporalAmountBetween(r, newValue);
LocalDateTime startNew = startDateTimeTextField.getLocalDateTime().plus(shift);
startDateTimeTextField.setLocalDateTime(startNew);
} else
{
// shiftAmount = DateTimeUtilities.temporalAmountBetween(oldValue, newValue);
}
};
/*
* Change start date/time when whole day is changed
*/
void handleWholeDayChange(T vComponent, Boolean newSelection)
{
startDateTimeTextField.localDateTimeProperty().removeListener(startDateTimeTextListener);
startDateTextField.localDateProperty().removeListener(startDateTextListener);
if (newSelection)
{
timeGridPane.getChildren().remove(startDateTimeTextField);
timeGridPane.add(startDateTextField, 1, 0);
startRecurrenceProperty.set(startDateTextField.getLocalDate());
} else
{
timeGridPane.getChildren().remove(startDateTextField);
timeGridPane.add(startDateTimeTextField, 1, 0);
if (startOriginalRecurrence instanceof LocalDate)
{
startRecurrenceProperty.set(startDateTimeTextField.getLocalDateTime().atZone(DEFAULT_ZONE_ID));
} else if (startOriginalRecurrence instanceof LocalDateTime)
{
startRecurrenceProperty.set(startDateTimeTextField.getLocalDateTime());
} else if (startOriginalRecurrence instanceof ZonedDateTime)
{
ZoneId originalZoneId = ((ZonedDateTime) startOriginalRecurrence).getZone();
startRecurrenceProperty.set(startDateTimeTextField.getLocalDateTime().atZone(originalZoneId));
} else
{
throw new DateTimeException("Unsupported Temporal type:" + startOriginalRecurrence.getClass());
}
}
startDateTextField.localDateProperty().addListener(startDateTextListener);
startDateTimeTextField.localDateTimeProperty().addListener(startDateTimeTextListener);
}
/* If startRecurrence isn't valid due to a RRULE change, changes startRecurrence and
* endRecurrence to closest valid values
*/
void synchRecurrenceDates(Temporal oldValue, Temporal newValue)
{
if (! vComponentEdited.isRecurrence(startRecurrenceProperty.get()))
{
TemporalAmount shift = DateTimeUtilities.temporalAmountBetween(oldValue, newValue);
LocalDateTime startNew = startDateTimeTextField.getLocalDateTime().plus(shift);
startDateTimeTextField.setLocalDateTime(startNew);
}
}
/* Displays an alert notifying that startInstance has changed due to changes in the Repeat tab.
* These changes can include the day of the week is not valid or the start date has shifted.
* The closest valid date is substituted.
*/
// TODO - PUT COMMENTS IN RESOURCES
@Deprecated // possibly remove - just update without alert
protected void startRecurrenceChangedAlert(Temporal t1, Temporal t2)
{
Alert alert = new Alert(AlertType.INFORMATION);
alert.getDialogPane().setId("startInstanceChangedAlert");
alert.getDialogPane().lookupButton(ButtonType.OK).setId("startInstanceChangedAlertOkButton");
alert.setHeaderText("Time not valid due to repeat rule change");
alert.setContentText(Settings.DATE_FORMAT_AGENDA_EXCEPTION.format(t1) + " is no longer valid." + System.lineSeparator()
+ "It has been replaced by " + Settings.DATE_FORMAT_AGENDA_EXCEPTION.format(t2));
ButtonType buttonTypeOk = new ButtonType("OK", ButtonData.CANCEL_CLOSE);
alert.getButtonTypes().setAll(buttonTypeOk);
alert.showAndWait();
}
protected void invalidCategoryAlert(String newString, int otherMatch)
{
Alert alert = new Alert(AlertType.ERROR);
alert.getDialogPane().setId("invalidCategoryAlert");
alert.getDialogPane().lookupButton(ButtonType.OK).setId("invalidCategoryAlertOkButton");
alert.setTitle("Edit Component Error");
alert.setHeaderText("Invalid Category Name.");
alert.setContentText("The Category name must be unique." + System.lineSeparator() + "The name \"" + newString + "\" matches category #" + otherMatch);
ButtonType buttonTypeOk = new ButtonType("OK", ButtonData.CANCEL_CLOSE);
alert.getButtonTypes().setAll(buttonTypeOk);
alert.showAndWait();
}
protected static void loadFxml(URL fxmlFile, Object rootController)
{
FXMLLoader loader = new FXMLLoader(fxmlFile);
loader.setController(rootController);
loader.setRoot(rootController);
loader.setResources(Settings.resources);
try {
loader.load();
}
catch(IOException e) {
e.printStackTrace();
}
}
}