package de.saring.util.gui.javafx.control.calendar;
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.css.PseudoClass;
import javafx.geometry.Point2D;
import javafx.scene.control.Label;
import javafx.scene.control.Tooltip;
import javafx.scene.input.Dragboard;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.TransferMode;
import de.saring.util.data.IdObject;
/**
* Calendar cell implementation which shows a single day cell. It displays the day number
* and the entries of the day below.
*
* @author Stefan Saring
*/
class CalendarDayCell extends AbstractCalendarCell {
private static final PseudoClass PSEUDO_CLASS_SUNDAY = PseudoClass.getPseudoClass("sunday");
private static final PseudoClass PSEUDO_CLASS_TODAY = PseudoClass.getPseudoClass("today");
private static final PseudoClass PSEUDO_CLASS_OUTSIDE_MONTH = PseudoClass.getPseudoClass("outside-month");
private LocalDate date;
private boolean displayedMonth;
private List<CalendarEntryLabel> calendarEntryLabels = new ArrayList<>();
private CalendarEntrySelectionListener calendarEntrySelectionListener;
private CalendarActionListener calendarActionListener;
/**
* Standard c'tor.
*/
public CalendarDayCell() {
setupListeners();
getStyleClass().add("calendar-control-day-cell");
}
/**
* Returns the date of this day cell.
*
* @return date
*/
public LocalDate getDate() {
return date;
}
/**
* Sets the date of this day cell
*
* @param date new date
* @param displayedMonth flag whether this date is inside the currently displayed month
*/
public void setDate(final LocalDate date, final boolean displayedMonth) {
this.date = date;
this.displayedMonth = displayedMonth;
updateDayLabel();
}
/**
* Displays the specified calendar entries inside this day cell.
*
* @param entries list of calendar entries (must not be null)
*/
public void setEntries(final List<CalendarEntry> entries) {
calendarEntryLabels = entries.stream() //
.map(entry -> new CalendarEntryLabel(entry, calendarEntrySelectionListener, calendarActionListener)) //
.collect(Collectors.toList());
updateEntryLabels(calendarEntryLabels);
}
/**
* Sets the listener for the selection status of calendar entries. The new listener is not
* set for already displayed calendar entries.
*
* @param selectionListener listener implementation
*/
public void setCalendarEntrySelectionListener(final CalendarEntrySelectionListener selectionListener) {
this.calendarEntrySelectionListener = selectionListener;
}
/**
* Sets the listener for handling actions on the calendar entries.
*
* @param calendarActionListener listener implementation
*/
public void setCalendarActionListener(final CalendarActionListener calendarActionListener) {
this.calendarActionListener = calendarActionListener;
}
/**
* Removes all calendar entry selections of this day cell, except the specified calendar entry.
*
* @param calendarEntryExcept entry for which the selection must not be removed (can be null)
*/
public void removeSelectionExcept(final CalendarEntry calendarEntryExcept) {
final IdObject entryExcept = calendarEntryExcept == null ? null : calendarEntryExcept.getEntry();
calendarEntryLabels.stream() //
.filter(entryLabel -> entryLabel.selected.get() && //
(entryExcept == null || !entryExcept.equals(entryLabel.entry.getEntry()))) //
.forEach(calendarEntryLabel -> calendarEntryLabel.selected.set(false));
}
/**
* Selects the specified entry, if it is displayed in this day cell.
*
* @param entry entry to select
* @return true when the entry was selected
*/
public boolean selectEntry(final IdObject entry) {
for (CalendarEntryLabel calendarEntryLabel : calendarEntryLabels) {
if (calendarEntryLabel.entry.getEntry().equals(entry)) {
calendarEntryLabel.selected.set(true);
return true;
}
}
return false;
}
/**
* Returns the CalendarEntry at the specified screen position or null when there is no entry.
*
* @param screenX X position on screen
* @param screenY Y position on screen
* @return CalendarEntry or null
*/
public CalendarEntry getEntryAtScreenPosition(final double screenX, final double screenY) {
for (CalendarEntryLabel calendarEntryLabel : calendarEntryLabels) {
final Point2D localPosition = calendarEntryLabel.screenToLocal(screenX, screenY);
if (calendarEntryLabel.getBoundsInLocal().contains(localPosition)) {
return calendarEntryLabel.entry;
}
}
return null;
}
private void setupListeners() {
// setup action listener for double clicks on the day cell (not on entries)
setOnMouseClicked(event -> {
if (calendarActionListener != null && event.getClickCount() > 1) {
calendarActionListener.onCalendarDayAction(date);
}
});
// all day cells support drag&drap of ONE FILE in mode 'copy' ()
setOnDragOver(event -> {
final Dragboard dragboard = event.getDragboard();
if (dragboard.hasFiles() && dragboard.getFiles().size() == 1 && dragboard.getFiles().get(0).isFile()) {
event.acceptTransferModes(TransferMode.COPY);
} else {
event.consume();
}
});
// notify action listener when a file has been dropped on the day cell or on a calendar entry
setOnDragDropped(event -> {
final Dragboard dragboard = event.getDragboard();
boolean success = false;
if (dragboard.hasFiles() && calendarActionListener != null) {
success = true;
final String filePath = dragboard.getFiles().get(0).getAbsolutePath();
final CalendarEntry droppedOnEntry = getEntryAtScreenPosition(event.getScreenX(), event.getScreenY());
if (droppedOnEntry == null) {
calendarActionListener.onDraggedFileDroppedOnCalendarDay(filePath);
} else {
calendarActionListener.onDraggedFileDroppedOnCalendarEntry(droppedOnEntry.getEntry(), filePath);
}
}
event.setDropCompleted(success);
event.consume();
});
}
private void updateDayLabel() {
setNumber(date.getDayOfMonth());
final boolean sunday = date.getDayOfWeek() == DayOfWeek.SUNDAY;
final boolean today = LocalDate.now().equals(date);
getNumberLabel().pseudoClassStateChanged(PSEUDO_CLASS_SUNDAY, sunday);
getNumberLabel().pseudoClassStateChanged(PSEUDO_CLASS_TODAY, today);
getNumberLabel().pseudoClassStateChanged(PSEUDO_CLASS_OUTSIDE_MONTH, !displayedMonth);
}
/**
* Listener interface for notification when the calendar entry selection changes.
*/
interface CalendarEntrySelectionListener {
/**
* This method is called whenever the selection of a calendar entry changes (when
* selected or deselected).
*
* @param calendarEntry entry of selection change
* @param selected true when the entry gets selected
*/
void calendarEntrySelectionChanged(CalendarEntry calendarEntry, boolean selected);
}
/**
* Custom label extension which displays a single entry inside a CalendarDayCell.
* Calendar entries are selectable, the status is provided by the property {@code selected}.
*/
private static class CalendarEntryLabel extends Label {
private static final PseudoClass PSEUDO_CLASS_SELECTED = PseudoClass.getPseudoClass("selected");
private CalendarEntry entry;
private BooleanProperty selected = new SimpleBooleanProperty(false);
public CalendarEntryLabel(final CalendarEntry entry, final CalendarEntrySelectionListener selectionListener,
final CalendarActionListener actionListener) {
this.entry = entry;
setMaxWidth(Double.MAX_VALUE);
this.setText(entry.getText());
if (entry.getToolTipText() != null) {
this.setTooltip(new Tooltip(entry.getToolTipText()));
}
if (entry.getColor() != null) {
this.setTextFill(entry.getColor());
}
// bind the background color to the selection status
getStyleClass().add("calendar-control-entry");
selected.addListener((observable, oldValue, newValue) -> pseudoClassStateChanged( //
PSEUDO_CLASS_SELECTED, newValue));
setupListeners(selectionListener, actionListener);
}
private void setupListeners(final CalendarEntrySelectionListener selectionListener,
final CalendarActionListener actionListener) {
// notify selection listener on changes (if registered)
if (selectionListener != null) {
selected.addListener((observable, oldValue, newValue) -> //
selectionListener.calendarEntrySelectionChanged(entry, newValue));
}
// update selection status when the user clicks on the entry label
addEventHandler(MouseEvent.MOUSE_PRESSED, event -> {
event.consume();
if (!selected.get()) {
selected.set(true);
}
});
// register action listener for double clicks on the entry
if (actionListener != null) {
addEventHandler(MouseEvent.MOUSE_CLICKED, event -> {
event.consume();
if (event.getClickCount() > 1) {
actionListener.onCalendarEntryAction(entry);
}
});
}
}
}
}