All Downloads are FREE. Search and download functionalities are using the official Maven repository.

jfxtras.labs.internal.scene.control.skin.AgendaWeekSkin Maven / Gradle / Ivy

There is a newer version: 9.0-r1
Show newest version
/**
 * Copyright (c) 2011, JFXtras
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *	 * Redistributions of source code must retain the above copyright
 *	   notice, this list of conditions and the following disclaimer.
 *	 * Redistributions in binary form must reproduce the above copyright
 *	   notice, this list of conditions and the following disclaimer in the
 *	   documentation and/or other materials provided with the distribution.
 *	 * Neither the name of the  nor the
 *	   names of its contributors may be used to endorse or promote products
 *	   derived from this software without specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL  BE LIABLE FOR ANY
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
// TODO: refactor the layout code in showMenu
// TODO: manual edit of all properties, location, description
// TODO: why does the "horizontal scrollbar" whitespace appear when viewport > min required? JFX bug?
// TODO: concept of tasks; appointment with only a start date (not whole day). Rendered as a line. 
// TODO: dropping an area event in the header and then back into the day; take the location of the drop into account as the start time (instead of the last start time)
// TODO: drop a wholeday in the day, drag it down so it spans two days, drag it in the header again, drag it back to the day again: height = 0 
// TODO: allow dragging on day spanning events on the not-the-first areas
// TODO: undo feature on all actions (remove, add, ...)
// TODO: should we use an intermediate appointment in the popup and have an ok button which copies all data (vs calling the setters directly)?
// TODO: single day view
// TODO: reminders?
// TODO: callbacks to check if a delete is ok, etc
// TODO: repeating appointments; is that something we want to do or are we letting the provider of the appointments handle that?
package jfxtras.labs.internal.scene.control.skin;

import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.ListChangeListener;
import javafx.event.EventHandler;
import javafx.scene.Cursor;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ScrollBar;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.ScrollPane.ScrollBarPolicy;
import javafx.scene.control.ScrollPaneBuilder;
import javafx.scene.control.TextField;
import javafx.scene.control.Tooltip;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.scene.layout.VBox;
import javafx.scene.shape.Line;
import javafx.scene.shape.Rectangle;
import javafx.scene.text.FontSmoothingType;
import javafx.scene.text.Text;
import javafx.stage.Popup;
import javafx.stage.WindowEvent;
import javafx.util.Duration;
import jfxtras.labs.animation.Timer;
import jfxtras.labs.internal.scene.control.behavior.AgendaBehavior;
import jfxtras.labs.scene.control.Agenda;
import jfxtras.labs.scene.control.Agenda.Appointment;
import jfxtras.labs.scene.control.CalendarTextField;
import jfxtras.labs.util.NodeUtil;

import com.sun.javafx.scene.control.skin.SkinBase;

/**
 * @author Tom Eugelink
 */
public class AgendaWeekSkin extends SkinBase
{
	// ==================================================================================================================
	// CONSTRUCTOR
	
	/**
	 * 
	 */
	public AgendaWeekSkin(Agenda control)
	{
		super(control, new AgendaBehavior(control));
		construct();
	}

	/*
	 * construct the component
	 */
	private void construct()
	{	
		// setup component
		createNodes();

		// react to changes in the locale 
		getSkinnable().localeProperty().addListener(new InvalidationListener() 
		{
			@Override
			public void invalidated(Observable observable)
			{
				refreshLocale();
			} 
		});
		refreshLocale();
		
		// react to changes in the displayed calendar 
		getSkinnable().displayedCalendar().addListener(new InvalidationListener()
		{			
			@Override
			public void invalidated(Observable observable)
			{
				assignCalendarToTheDayPanes();
				setupAppointments();
			}
		});
		assignCalendarToTheDayPanes();
		
		// react to changes in the appointments 
		getSkinnable().appointments().addListener(new ListChangeListener() 
		{
			@Override
			public void onChanged(javafx.collections.ListChangeListener.Change arg0)
			{
				setupAppointments();
			} 
		});
		setupAppointments();
		
		// react to changes in the appointments 
		getSkinnable().selectedAppointments().addListener(new ListChangeListener() 
		{
			@Override
			public void onChanged(javafx.collections.ListChangeListener.Change changes)
			{
				setOrRemoveSelected();
			} 
		});
		setOrRemoveSelected();
	}
	
	/**
	 * set or remove the Selected class from the appointments
	 */
	private void setOrRemoveSelected()
	{
		// update the styleclass
		for (DayPane lDayPane : weekPane.dayPanes)
		{
			for (AbstractAppointmentPane lAppointmentPane : lDayPane.allAbstractAppointmentPanes())
			{
				// remove 
				if ( lAppointmentPane.getStyleClass().contains("Selected") == true
				  && getSkinnable().selectedAppointments().contains(lAppointmentPane.appointment) == false
				   )
				{
					lAppointmentPane.getStyleClass().remove("Selected");
				}
				// add
				if ( lAppointmentPane.getStyleClass().contains("Selected") == false
				  && getSkinnable().selectedAppointments().contains(lAppointmentPane.appointment) == true
				   )
				{
					lAppointmentPane.getStyleClass().add("Selected");
				}
			}
		}		
	}
	
	/**
	 * Assign a calendar to each day, so it knows what it must draw.
	 */
	private void assignCalendarToTheDayPanes()
	{
		// get the first day of week calendar
		Calendar lCalendar = getFirstDayOfWeekCalendar();
		Calendar lStartCalendar = (Calendar)lCalendar.clone();
		
		// assign it to each day pane
		Calendar lEndCalendar = null;
		for (int i = 0; i < 7; i++)
		{
			// set the calendar
			DayPane lDayPane = weekPane.dayPanes.get(i); 
			lDayPane.calendarObjectProperty.set( (Calendar)lCalendar.clone() );
			if (i== 6) lEndCalendar = (Calendar)lCalendar.clone();
			lCalendar.add(Calendar.DATE, 1);
		}		
		
		// place the now line
		nowUpdateRunnable.run(); 
		
		// tell the skin what range is displayed, so it can update the appointments
		if (getSkinnable().getCalendarRangeCallback() != null)
		{
			Agenda.CalendarRange lCalendarRange = new Agenda.CalendarRange(lStartCalendar, lEndCalendar);
			getSkinnable().getCalendarRangeCallback().call(lCalendarRange);
		}
	}
	
	/**
	 * 
	 */
	private void refreshLocale()
	{
		// create the formatter to use
		dayOfWeekDateFormat = new SimpleDateFormat("E", getSkinnable().getLocale());
		dateFormat = (SimpleDateFormat)SimpleDateFormat.getDateInstance(SimpleDateFormat.SHORT, getSkinnable().getLocale());
		
		// force redraw the dayHeaders by reassigning the calendar
		int lCnt = 0;
		for (DayPane lDayPane : weekPane.dayPanes)
		{
			if (lDayPane.calendarObjectProperty.get() != null)
			{
				lDayPane.calendarObjectProperty.set( (Calendar)lDayPane.calendarObjectProperty.get().clone() );
			}

			// set weekend class
			String lWeekendOrWeekday = isWeekdayWeekend(lCnt) ? "weekend" : "weekday";
			lDayPane.getStyleClass().removeAll("weekend", "weekday");
			lDayPane.getStyleClass().add(lWeekendOrWeekday);			
			lDayPane.dayHeaderPane.calendarText.getStyleClass().removeAll("weekend", "weekday");
			lDayPane.dayHeaderPane.calendarText.getStyleClass().add(lWeekendOrWeekday);
			lCnt++;
		}
	}
	private SimpleDateFormat dayOfWeekDateFormat = null;
	private SimpleDateFormat dateFormat = null;
	final static private SimpleDateFormat timeFormat = new SimpleDateFormat("HH:mm");

	/**
	 * Have all days reconstruct the appointments
	 */
	private void setupAppointments()
	{
		calculateSizes();
		for (DayPane lDay : weekPane.dayPanes)
		{
			lDay.setupAppointments();
		}
		calculateSizes(); // TODO: when dropping a wholeday appointment into another day header, the header height is not increased unless this call is present. Figure out why.
		nowUpdateRunnable.run(); // set the history
	}

	
	// ==================================================================================================================
	// DRAW
	
	/**
	 * construct the nodes
	 */
	private void createNodes()
	{
		// we use a borderpane
		borderPane = new BorderPane();
		
		// borderpane center
		weekPane = new WeekPane();
		weekScrollPane = ScrollPaneBuilder.create()
			.content(weekPane)
			.hbarPolicy(ScrollBarPolicy.NEVER)
			.fitToWidth(true)
			.pannable(false) // panning would conflict with creating a new appointment
			.build();
		borderPane.setCenter(weekScrollPane);
		// bind to the scrollpane's viewport
		weekScrollPane.viewportBoundsProperty().addListener(new InvalidationListener()
		{
			@Override
			public void invalidated(Observable viewportBoundsProperty)
			{
				calculateSizes();
				nowUpdateRunnable.run();
			}
		});
		
		// borderpane top: header has to be created after the content, because there is a binding
		weekHeaderPane = new WeekHeaderPane();
		weekHeaderPane.prefWidthProperty().bind(weekPane.widthProperty()); // same width as the weekpane
		weekHeaderPane.prefHeightProperty().bind(headerHeightProperty);
		weekHeaderPane.setTranslateX(1); // correct for the scrollpane
		borderPane.setTop(weekHeaderPane);
		
		// create a transparent pane where dragging an appointment is visualized 
		dragPane = new Pane();
		dragPane.prefWidthProperty().bind(widthProperty()); // the drag pane is the same size as the whole skin
		dragPane.prefHeightProperty().bind(heightProperty());
		// the borderpane is placed in the drag pane and sized to match 
		dragPane.getChildren().add(borderPane);
		borderPane.prefWidthProperty().bind(dragPane.widthProperty());
		borderPane.prefHeightProperty().bind(dragPane.heightProperty());
		
		// add to self
		getStyleClass().add(getClass().getSimpleName()); // always add self as style class, because CSS should relate to the skin not the control		
		getChildren().add(dragPane);
		
		// load the close icon
		closeIconImage = new Image(this.getClass().getResourceAsStream(this.getClass().getSimpleName() + "PopupCloseWindowIcon.png"));
	}
	private Pane dragPane = null;
	private BorderPane borderPane = null;
	private WeekHeaderPane weekHeaderPane = null;
	private ScrollPane weekScrollPane = null;
	private WeekPane weekPane = null;
	private Image closeIconImage = null;

	// ==================================================================================================================
	// PANES
	
	/**
	 * Responsible for rendering the day headers
	 */
	class WeekHeaderPane extends Pane
	{
		/**
		 * 
		 */
		public WeekHeaderPane()
		{
			// 7 days per week
			for (int i = 0; i < 7; i++)
			{
				DayHeaderPane lDayHeader = new DayHeaderPane(weekPane.dayPanes.get(i)); // associate with a day, so we can use its administration. This needs only be done once
				lDayHeader.layoutXProperty().bind(weekPane.dayPanes.get(i).layoutXProperty());			
				lDayHeader.layoutYProperty().set(0);
				lDayHeader.prefWidthProperty().bind(weekPane.dayPanes.get(i).prefWidthProperty());			
				lDayHeader.prefHeightProperty().bind(heightProperty());			
				getChildren().add(lDayHeader);
				dayHeaderPanes.add(lDayHeader);
			}
		}
		final List dayHeaderPanes = new ArrayList();
	}

	/**
	 * Responsible for rendering the day header (whole day appointments).
	 * This class is connected to the daypane and uses its data.
	 */
	class DayHeaderPane extends Pane
	{
		public DayHeaderPane(DayPane dayPane)
		{
			// for debugging setStyle("-fx-border-color:PINK;-fx-border-width:4px;");
			getStyleClass().add("DayHeader");
			
			// link up the day and day header panes
			this.dayPane = dayPane;
			dayPane.dayHeaderPane = this; // two way link
			
			// set label
			calendarText = new Text("?");
			calendarText.getStyleClass().add("Calendar");
			calendarText.setX( padding ); // align left
			calendarText.setY( calendarText.prefHeight(0) );
			Rectangle lClip = new Rectangle(0,0,0,0);
			lClip.widthProperty().bind(widthProperty().subtract(padding));
			lClip.heightProperty().bind(heightProperty());
			calendarText.setClip(lClip);
			getChildren().add(calendarText);
			// react to changes in the calendar by updating the label
			dayPane.calendarObjectProperty.addListener(new InvalidationListener()
			{
				@Override
				public void invalidated(Observable arg0)
				{
					String lLabel = DayHeaderPane.this.dayPane.calendarObjectProperty.get() == null ? "" : dayOfWeekDateFormat.format(DayHeaderPane.this.dayPane.calendarObjectProperty.get().getTime()) + " " + dateFormat.format(DayHeaderPane.this.dayPane.calendarObjectProperty.get().getTime());
					calendarText.setText(lLabel);
				}
			});
			
			// change the layout related to the size
			widthProperty().addListener(new InvalidationListener()
			{
				@Override
				public void invalidated(Observable arg0)
				{
					relayout();
				}
			});
			heightProperty().addListener(new InvalidationListener()
			{
				@Override
				public void invalidated(Observable arg0)
				{
					relayout();
				}
			});
			
			// layout
			relayout();
		}
		DayPane dayPane = null;
		Text calendarText = null;
		
		/**
		 * 
		 */
		public void relayout()
		{
			// create headers
			int lOffset = highestNumberOfWholedayAppointmentsProperty.get() - appointmentHeaderPanes.size(); // to make sure the appointments are renders aligned bottom
			for (AppointmentHeaderPane lAppointmentHeaderPane : appointmentHeaderPanes)
			{
				int lIdx = appointmentHeaderPanes.indexOf(lAppointmentHeaderPane);
				lAppointmentHeaderPane.setLayoutX(lIdx * wholedayAppointmentWidth); // each pane is cascade offset to the right to allow connecting to the wholeday appointment on the day pane 
				lAppointmentHeaderPane.setLayoutY( titleCalendarHeightProperty.get() + ((lIdx + lOffset) * wholedayTitleHeightProperty.get()) ); // each pane is cascaded offset down so the title label is visible 
				lAppointmentHeaderPane.setPrefSize(dayWidthProperty.get() - (lIdx * wholedayAppointmentWidth), (appointmentHeaderPanes.size() - lIdx) * wholedayTitleHeightProperty.get()); // make sure the size matches the cascading
			}
		}
		
		/**
		 * 
		 */
		public void setupAppointments()
		{
			// remove all appointments and create new ones
			getChildren().removeAll(appointmentHeaderPanes);
			appointmentHeaderPanes.clear();
			
			// for all wholeday appointments on the day pane, create a header appointment pane as well
			for (WholedayAppointmentPane lAppointmentPane : dayPane.wholedayAppointmentPanes)  
			{
				AppointmentHeaderPane lAppointmentHeaderPane = new AppointmentHeaderPane(lAppointmentPane.appointment); 
				getChildren().add(lAppointmentHeaderPane);				
				appointmentHeaderPanes.add(lAppointmentHeaderPane);	
			}
			
			// and layout
			relayout();
		}
		final List appointmentHeaderPanes = new ArrayList();		
	}
	
	/**
	 * Responsible for rendering a single whole day appointment on a day header.
	 * 
	 */
	class AppointmentHeaderPane extends AbstractAppointmentPane
	{
		/**
		 * 
		 * @param calendar
		 * @param appointment
		 */
		public AppointmentHeaderPane(Agenda.Appointment appointment)
		{
			super(appointment);
			
			// for debugging setStyle("-fx-border-color:GREEN;-fx-border-width:4px;");
			getStyleClass().add("Appointment");
			getStyleClass().add(appointment.getAppointmentGroup().getStyleClass());

			// add a text node
			Text lSummaryText = new Text(appointment.getSummary());
			lSummaryText.getStyleClass().add("AppointmentLabel");
			lSummaryText.setX( padding );
			lSummaryText.setY( textHeight1MProperty.get() );
			Rectangle lClip = new Rectangle(0,0,0,0);
			lClip.widthProperty().bind(widthProperty().subtract(padding));
			lClip.heightProperty().bind(heightProperty());
			lSummaryText.setClip(lClip);
			getChildren().add(lSummaryText);			
			
			// add the menu header
			getChildren().add(menuIcon);
			
			// historical visualizer
			historicalVisualizer = new HistoricalVisualizer(this);
			getChildren().add(historicalVisualizer);
		}
		final Rectangle historicalVisualizer;
	}
	
	/**
	 * Responsible for rendering the days
	 */
	class WeekPane extends Pane
	{
		final List dayPanes = new ArrayList();

		/**
		 * 
		 */
		public WeekPane()
		{
			getStyleClass().add("WeekPane");
			
			// draw hours
			for (int lHour = 0; lHour < 24; lHour++)
			{
				// hour line
				{
					Line l = new Line(0,10,100,10);
					l.getStyleClass().add("HourLine");
					l.startXProperty().set(0.0);
					l.startYProperty().bind(hourHeighProperty.multiply(lHour));
					l.endXProperty().bind(widthProperty());
					l.endYProperty().bind(l.startYProperty());
					getChildren().add(l);
				}
				// half hour line
				{
					Line l = new Line(0,10,100,10);
					l.getStyleClass().add("HalfHourLine");
					l.startXProperty().bind(timeWidthProperty);
					l.endXProperty().bind(widthProperty());
					l.startYProperty().bind(hourHeighProperty.multiply(lHour + 0.5));
					l.endYProperty().bind(l.startYProperty());
					getChildren().add(l);
				}
				// hour text
				{
					Text t = new Text(lHour + ":00");
					t.xProperty().bind(timeWidthProperty.subtract(t.getBoundsInParent().getWidth()).subtract(timeColumnWhitespace / 2));
					t.yProperty().bind(hourHeighProperty.multiply(lHour));
					t.setTranslateY(t.getBoundsInParent().getHeight()); // move it under the line
					t.getStyleClass().add("HourLabel");
					t.setFontSmoothingType(FontSmoothingType.LCD);
					getChildren().add(t);
				}
			}

			// 7 days per week
			for (int i = 0; i < 7; i++)
			{
				DayPane lDay = new DayPane();
				lDay.layoutXProperty().bind(dayWidthProperty.multiply(i).add(dayFirstColumnXProperty));
				lDay.layoutYProperty().set(0.0);
				lDay.prefWidthProperty().bind(dayWidthProperty);
				lDay.prefHeightProperty().bind(dayHeightProperty);
				getChildren().add(lDay);
				
				// remember
				dayPanes.add(lDay);
			}
		}
	}
	
	
	/**
	 * Responsible for rendering the appointments within a day 
	 */
	class DayPane extends Pane
	{
		// this daypane is representing this calendar (date)
		ObjectProperty calendarObjectProperty = new SimpleObjectProperty(DayPane.this, "calendar");
		
		// know your header
		DayHeaderPane dayHeaderPane = null;
		
		public DayPane()
		{
			// for debugging setStyle("-fx-border-color:PINK;-fx-border-width:4px;");		
			getStyleClass().add("Day");
			
			// the appointments layout is too complex to cleanly calculate using binding, so we'll listen to size changes
			widthProperty().addListener(new InvalidationListener()
			{
				@Override
				public void invalidated(Observable arg0)
				{
					relayout();
				}
			});
			heightProperty().addListener(new InvalidationListener()
			{
				@Override
				public void invalidated(Observable arg0)
				{
					relayout();
				}
			});
			
			// ---------
			
			// start new appointment
			setOnMousePressed(new EventHandler()
			{
				@Override
				public void handle(MouseEvent mouseEvent)
				{
					// if there is no one to handle the result, don't eve bother
					if (getSkinnable().createAppointmentCallbackProperty().get() == null) return;
					
					// show the rectangle
					DayPane.this.setCursor(Cursor.V_RESIZE);
					double lY = mouseEvent.getScreenY() - NodeUtil.screenY(DayPane.this);
					resizeRectangle = new Rectangle(0, lY, dayWidthProperty.get(), 10);
					resizeRectangle.getStyleClass().add("GhostRectangle");
					DayPane.this.getChildren().add(resizeRectangle);
					
					// this event should not be processed by the appointment area
					mouseEvent.consume();
					dragged = false;
					getSkinnable().selectedAppointments().clear();
				}
			});
			// visualize resize
			setOnMouseDragged(new EventHandler()
			{
				@Override
				public void handle(MouseEvent mouseEvent)
				{
					if (resizeRectangle == null) return;
					
					// - calculate the number of pixels from onscreen nodeY (layoutY) to onscreen mouseY					
					double lHeight = mouseEvent.getScreenY() - NodeUtil.screenY(resizeRectangle);
					if (lHeight < 5) lHeight = 5;
					resizeRectangle.setHeight(lHeight);
					
					// no one else
					mouseEvent.consume();
					dragged = true;
				}
			});
			// end resize
			setOnMouseReleased(new EventHandler()
			{
				@Override
				public void handle(MouseEvent mouseEvent)
				{
					if (resizeRectangle == null) return;
					
					// no one else
					mouseEvent.consume();
					
					// reset ui
					DayPane.this.setCursor(Cursor.HAND);
					DayPane.this.getChildren().remove(resizeRectangle);
					
					// must have dragged (otherwise it is considered an "unselect all" action)
					if (dragged == false) return;
					
					// calculate the starttime
					Calendar lStartCalendar = setTimeTo0000((Calendar)DayPane.this.calendarObjectProperty.get().clone());
					lStartCalendar.add(Calendar.MILLISECOND, (int)(resizeRectangle.getY() * durationInMSPerPixelProperty.get()));
					setTimeToNearestMinutes(lStartCalendar, 5);
					
					// calculate the new end date for the appointment (recalculating the duration)
					Calendar lEndCalendar = (Calendar)lStartCalendar.clone();					
					lEndCalendar.add(Calendar.MILLISECOND, (int)(resizeRectangle.getHeight() * durationInMSPerPixelProperty.get()));
					setTimeToNearestMinutes(lEndCalendar, 5);
					
					// clean up
					resizeRectangle = null;					
					
					// ask the control to create a new appointment (null may be returned)
					Agenda.Appointment lAppointment = getSkinnable().createAppointmentCallbackProperty().get().call(new Agenda.CalendarRange(lStartCalendar, lEndCalendar));
					if (lAppointment != null) 
					{
						getSkinnable().appointments().add(lAppointment); // the appointments collection is listened to, so they will automatically be refreshed
					}
				}
			});
		}
		Rectangle resizeRectangle = null;
		boolean dragged = false;
		
		/**
		 * 
		 * @return
		 */
		public List allAbstractAppointmentPanes()
		{
			List lPanes = new ArrayList(regularAppointmentPanes);
			lPanes.addAll(wholedayAppointmentPanes);
			lPanes.addAll(dayHeaderPane.appointmentHeaderPanes);
			return lPanes;
		}
		
		/**
		 * 
		 */
		private void relayout()
		{
			// first add all the whole day appointments
			int lWholedayCnt = 0;
			for (WholedayAppointmentPane lAppointmentPane : wholedayAppointmentPanes)
			{
				lAppointmentPane.setLayoutX(lWholedayCnt * wholedayAppointmentWidth);
				lAppointmentPane.setLayoutY(0);
				lAppointmentPane.setPrefSize(wholedayAppointmentWidth, dayHeightProperty.get());
				lWholedayCnt++;
			}
			
			// then add all regular appointments to the day
			// calculate how much room is remaining for the regular appointments
			double lRemainingWidthForRegularAppointments = dayContentWidthProperty.get() - (lWholedayCnt * wholedayAppointmentWidth);			
			for (RegularAppointmentPane lAppointmentPane : regularAppointmentPanes)
			{
				// the X is determine by offsetting the wholeday appointments and then calculate the X of the track the appointment is placed in (available width / number of tracks) 
				lAppointmentPane.setLayoutX((lWholedayCnt * wholedayAppointmentWidth) + (lRemainingWidthForRegularAppointments / lAppointmentPane.clusterOwner.clusterTracks.size() * lAppointmentPane.clusterTrackIdx));
				
				// the Y is determined by the start time in minutes projected onto the total day height (being 24 hours)
				int lTimeFactor = (lAppointmentPane.start.get(Calendar.HOUR_OF_DAY) * 60) + lAppointmentPane.start.get(Calendar.MINUTE);
				lAppointmentPane.setLayoutY(dayHeightProperty.get() / (24 * 60) * lTimeFactor );
				
				// the width is the remaining width (subtracting the wholeday appointments) divided by the number of tracks in the cluster
				double lW = (dayContentWidthProperty.get() - (wholedayAppointmentPanes.size() * wholedayAppointmentWidth)) * (1.0 / (((double)lAppointmentPane.clusterOwner.clusterTracks.size())));
				// all but the most right appointment get 50% extra width, so they underlap the next track 
				if (lAppointmentPane.clusterTrackIdx < lAppointmentPane.clusterOwner.clusterTracks.size() - 1) lW *= 1.75;
				lAppointmentPane.setPrefWidth(lW);
				
				// the height is determing by the duration projected against the total dayHeight (being 24 hours)
				double lH = (dayHeightProperty.get() / (24 * 60) * (lAppointmentPane.durationInMS / 1000 / 60) );
				// the height has a minimum size, in order to be able to render sensibly
				// if (lH < textHeight1MProperty.get() + padding) lH = textHeight1MProperty.get() + padding; 
				if (lH < 2 * padding) lH = 2 * padding; 
				lAppointmentPane.setPrefHeight(lH);
			}
		}			

		/**
		 * This method prepares a day for being drawn.
		 * The appointments within one day might overlap, this method will create a data structure so it is clear how these overlapping appointments should be drawn.
		 * All appointments in one day are process based on their start time; earliest first, and if there are more with the same start time, longest duration first.
		 * The appointments are then place onto (parallel) tracks; an appointment initially is placed in track 0. 
		 * But if there is already an (partially overlapping) appointment there, then the appointment is moved to track 1. 
		 * Unless there also is an appointment already in that track 1, then the next track is tried, and so forth, until a free track is found.
		 * For example (the letters are not the sequence in which the appointments are processed, they're just for identifying them):
		 * 
		 *  tracks
		 *  0 1 2 3
		 *  -------
		 *  . . . .
		 *  . . . .
		 *  A . . .
		 *  A B C .
		 *  A B C D
		 *  A B . D
		 *  A . . D
		 *  A E . D
		 *  A . . D
		 *  . . . D
		 *  . . . D
		 *  F . . D
		 *  F H . D 
		 *  . . . .
		 *  G . . . 
		 *  . . . .
		 * 
		 * Appointment A was rendered first and put into track 0 and its start time.
		 * Then appointment B was added, initially it was put in track 0, but appointment A already uses the that slot, so B was moved into track 1.
		 * C moved from track 0, conflicting with A, to track 1, conflicting with B, and ended up in track 2. And so forth.
		 * F and H show that even though D overlaps them, they could perfectly be placed in lower tracks.
		 * 
		 * A cluster of appointments always starts with a free standing appointment in track 0, for example A or G, such appointment is called the cluster owner.
		 * When the next appointment is added to the tracks, and finds that it cannot be put in track 0, it will be added as a member to the cluster represented by the appointment in track 0.
		 * Special attention must be paid to an appointment that is placed in track 0, but is linked to a cluster by a earlier appointment in a higher track; such an appointment is not the cluster owner.
		 * In the example above, F is linked through D to the cluster owned by A. So F is not a cluster owner, but a member of the cluster owned by A.
		 * And appointment H through F is also part of the cluster owned by A.  
		 * G finally starts a new cluster.
		 * The cluster owner knows all members and how many tracks there are, each member knows in what track it is and has a direct link to the cluster owner. 
		 *  
		 * When rendering the appointments above, parallel appointments are rendered narrower & indented, so appointments partially overlap and the left side of an appointment is always visible to the user.
		 * In the example above the single appointment G is rendered full width, while for example A, B, C and D are overlapping.
		 * F and H are drawn in the same dimensions as A and B in order to allow D to overlap then.
		 * The size and amount of indentation depends on the number of appointments that are rendered next to each other.
		 * In order to compute its location and size, each appointment needs to know:
		 * - its start and ending time,
		 * - its track number,
		 * - its total number of tracks,
		 * - and naturally the total width and height available to draw the day.
		 * 
		 */
		public void setupAppointments()
		{
			// remember for animation
			final List lOldRegularAppointmentPanes = new ArrayList(regularAppointmentPanes); 
			final List lOldWholedayAppointmentPanes = new ArrayList(wholedayAppointmentPanes); 

			// clear
			regularAppointmentPanes.clear();
			wholedayAppointmentPanes.clear();			
			if (calendarObjectProperty.get() == null) return;
			
			// scan all appointments and filter the ones for this day
			for (Agenda.Appointment lAppointment : getSkinnable().appointments())
			{
				// different panes depending on the appointment time
				if (lAppointment.isWholeDay())
				{
					// if appointment falls on the same date as this day pane
					if (isSameDay(calendarObjectProperty.get(), lAppointment.getStartTime()))
					{
						WholedayAppointmentPane lAppointmentPane = new WholedayAppointmentPane(lAppointment, this);
						wholedayAppointmentPanes.add(lAppointmentPane);
					}
				}
				else
				{
					// appointments may span multiple days, but the appointment pane will clamp the start and end date
					RegularAppointmentPane lAppointmentPane = new RegularAppointmentPane(lAppointment, this);
					
					// check if the appointment falls in the same day as this day pane
					if ( isSameDay(calendarObjectProperty.get(), lAppointmentPane.start) 
					  && isSameDay(calendarObjectProperty.get(), lAppointmentPane.end)
					   )
					{
						regularAppointmentPanes.add(lAppointmentPane);
					}
				}
			}
			
			// sort on start time and then decreasing duration
			Collections.sort(regularAppointmentPanes, new Comparator()
			{
				@Override
				public int compare(AbstractDayAppointmentPane o1, AbstractDayAppointmentPane o2)
				{
					if (o1.startAsString.equals(o2.startAsString) == false)
					{
						return o1.startAsString.compareTo(o2.startAsString);
					}
					return o1.durationInMS > o2.durationInMS ? -1 : 1;
				}
			});
			
			// start placing appointments in the tracks
			RegularAppointmentPane lClusterOwner = null;
			for (RegularAppointmentPane lAppointmentPane : regularAppointmentPanes)
			{
				// if there is no cluster owner
				if (lClusterOwner == null)
				{
					// than the current becomes an owner
					// only create a minimal cluster, because it will be setup fully in the code below
					lClusterOwner = lAppointmentPane;
					lClusterOwner.clusterTracks = new ArrayList>();
				}
				
				// in which track should it be added
				int lTrackNr = determineTrackWhereAppointmentCanBeAdded(lClusterOwner.clusterTracks, lAppointmentPane);
				// if it can be added to track 0, then we have a "situation". Track 0 could mean
				// - we must start a new cluster
				// - the appointment is still linked to the running cluster by means of a linking appointment in the higher tracks
				if (lTrackNr == 0)
				{
					// So let's see if there is a linking appointment higher up
					boolean lOverlaps = false;
					for (int i = 1; i < lClusterOwner.clusterTracks.size() && lOverlaps == false; i++)
					{
						lOverlaps = checkIfTheAppointmentOverlapsAnAppointmentAlreadyInThisTrack(lClusterOwner.clusterTracks, i, lAppointmentPane);
					}
					
					// if it does not overlap, we start a new cluster
					if (lOverlaps == false)
					{
						lClusterOwner = lAppointmentPane;
						lClusterOwner.clusterMembers = new ArrayList(); 
						lClusterOwner.clusterTracks = new ArrayList>();
						lClusterOwner.clusterTracks.add(new ArrayList());
					}
				}
				
				// add it to the track (and setup all other cluster data)
				lClusterOwner.clusterMembers.add(lAppointmentPane);
				lClusterOwner.clusterTracks.get(lTrackNr).add(lAppointmentPane);
				lAppointmentPane.clusterOwner = lClusterOwner;
				lAppointmentPane.clusterTrackIdx = lTrackNr;				
				// for debug  System.out.println("----"); for (int i = 0; i < lClusterOwner.clusterTracks.size(); i++) { System.out.println(i + ": " + lClusterOwner.clusterTracks.get(i) ); } System.out.println("----");
			}
			
			// laying out the appointments is fairly complex, so we use listeners and a relayout method instead of binding
			relayout();

			// and swap the appointments; old ones out, new ones in
			// TODO: animation? we could move the old appointments to the equivalent positions on the drag pane, then animate them to their new positions, remove thge old, and insert the new ones.
			// however, this needs to be cross-days, so it cannot be done here (this is only one day), but after the complete setupAppointments()
			getChildren().removeAll(lOldRegularAppointmentPanes);
			getChildren().removeAll(lOldWholedayAppointmentPanes);
			getChildren().addAll(wholedayAppointmentPanes);
			getChildren().addAll(regularAppointmentPanes);

			// we're done, now have the header updated
			dayHeaderPane.setupAppointments();
		}
		final List regularAppointmentPanes = new ArrayList(); 
		final List wholedayAppointmentPanes = new ArrayList(); 
	}
	
	/**
	 * Responsible for rendering a whoelday appointment on a single day.
	 * 
	 */
	class WholedayAppointmentPane extends AbstractDayAppointmentPane
	{
		/**
		 * 
		 * @param calendar
		 * @param appointment
		 */
		public WholedayAppointmentPane(Agenda.Appointment appointment, DayPane dayPane)
		{
			super(appointment, dayPane);
			isDraggable = false;
			
			// start
			this.start = setTimeTo0000( (Calendar)appointment.getStartTime().clone() );
			this.end = setTimeTo2359( (Calendar)appointment.getStartTime().clone() );
			durationInMS = this.end.getTimeInMillis() - this.start.getTimeInMillis();
			
			// strings
			this.startAsString = timeFormat.format(this.start.getTime());
			this.endAsString = timeFormat.format(this.end.getTime());
			
			// history visualizer
			historicalVisualizer = new HistoricalVisualizer(this);
			getChildren().add(historicalVisualizer);
		}

		/**
		 * 
		 */
		public String toString()
		{
			return super.toString()
				 + ";" + startAsString + " wholeday"
				 + ";" + appointment.getSummary()
				 ;
		}
	}

	/**
	 * Responsible for rendering a regular appointment on a day.
	 * Appointments may span multiple days, each day gets its own appointment pane.
	 * 
	 */
	class RegularAppointmentPane extends AbstractDayAppointmentPane
	{
		// for the role of cluster owner
		List clusterMembers = null; 
		List> clusterTracks = null;
		
		// for the role of cluster member
		RegularAppointmentPane clusterOwner = null;
		int clusterTrackIdx = -1;

		/**
		 * 
		 * @param dayPaneCalendar
		 * @param appointment
		 */
		public RegularAppointmentPane(Agenda.Appointment appointment, DayPane dayPane)
		{
			super(appointment, dayPane);
			
			// start
			Calendar lDayStartCalendar = setTimeTo0000( (Calendar)dayPane.calendarObjectProperty.get().clone() );
			this.start = (appointment.getStartTime().before(lDayStartCalendar) ? lDayStartCalendar : (Calendar)appointment.getStartTime().clone());			
			// end
			Calendar lDayEndCalendar = setTimeTo2359( (Calendar)dayPane.calendarObjectProperty.get().clone() );
			this.end = (appointment.getEndTime().after(lDayEndCalendar) ? lDayEndCalendar : (Calendar)appointment.getEndTime().clone());
			// duration
			durationInMS = this.end.getTimeInMillis() - this.start.getTimeInMillis();
			
			// may span multiple days; mark which one this is 
			isFirstAreaOfAppointment = this.start.equals(appointment.getStartTime()); 
			isLastAreaOfAppointment = this.end.equals(appointment.getEndTime()); 
		
			// strings
			this.startAsString = timeFormat.format(this.start.getTime());
			this.endAsString = timeFormat.format(this.end.getTime());
			
			// add the duration as text
			Text lTimeText = new Text(startAsString + "-" + endAsString);
			{
				lTimeText.getStyleClass().add("AppointmentTimeLabel");
				lTimeText.setX( padding );
				lTimeText.setY(lTimeText.prefHeight(0));
				Rectangle lClip = new Rectangle(0,0,0,0);
				lClip.widthProperty().bind(widthProperty().subtract(padding));
				lClip.heightProperty().bind(heightProperty());
				lTimeText.setClip(lClip);
				getChildren().add(lTimeText);
			}
			// add summary
			Text lSummaryText = new Text(appointment.getSummary());
			{
				lSummaryText.getStyleClass().add("AppointmentLabel");
				lSummaryText.setX( padding );
				lSummaryText.setY( lTimeText.getY() + textHeight1MProperty.get());
				lSummaryText.wrappingWidthProperty().bind(widthProperty().subtract(padding));
				Rectangle lClip = new Rectangle(0,0,0,0);
				lClip.widthProperty().bind(widthProperty());
				lClip.heightProperty().bind(heightProperty().subtract(padding));
				lSummaryText.setClip(lClip);
				getChildren().add(lSummaryText);			
			}
			
			// duration dragger
			if (isLastAreaOfAppointment == true)
			{
				durationDragger = new DurationDragger(this);
				getChildren().add(durationDragger);
			}
			
			// add the menu header
			if (appointment.isWholeDay() == false) 
			{
				getChildren().add(menuIcon);
			}
			
			// history visualizer
			historicalVisualizer = new HistoricalVisualizer(this);
			getChildren().add(historicalVisualizer);
		}
		
		/**
		 * 
		 */
		public String toString()
		{
			return super.toString()
				 + ";" + startAsString + "-" + endAsString
				 + ";" + durationInMS + "ms"
				 + ";" + appointment.getSummary()
				 ;
		}
	}

			
	// ==================================================================================================================
	// HISTORICAL
	
	/**
	 * For whiting-out an appointment pane
	 */
	class HistoricalVisualizer extends Rectangle
	{
		public HistoricalVisualizer(Pane pane)
		{
			// it 100% overlays the pane it is bound to
			setMouseTransparent(true);
			xProperty().set(0);
			yProperty().set(0);
			widthProperty().bind(pane.prefWidthProperty());
			heightProperty().bind(pane.prefHeightProperty());
			setVisible(false);
			getStyleClass().add("History");			
		}
	}
	
	
	// ==================================================================================================================
	// MENU
	
	/**
	 * for the popup menu
	 */
	class MenuIcon extends Rectangle
	{
		public MenuIcon(final AbstractAppointmentPane abstractAppointmentPane)
		{
			// layout
			setX(padding);
			setY(padding);
			setWidth(6);
			setHeight(3);
			
			// style
			getStyleClass().add("MenuIcon");
			
			// play with the mouse pointer to show something can be done here
			setOnMouseEntered(new EventHandler()
			{
				@Override
				public void handle(MouseEvent mouseEvent)
				{
					if (!mouseEvent.isPrimaryButtonDown())
					{						
						MenuIcon.this.setCursor(Cursor.HAND);
						
						// no one else
						mouseEvent.consume();
					}
				}
			});
			setOnMouseExited(new EventHandler()
			{
				@Override
				public void handle(MouseEvent mouseEvent)
				{
					if (!mouseEvent.isPrimaryButtonDown())
					{
						MenuIcon.this.setCursor(Cursor.DEFAULT);
						
						// no one else
						mouseEvent.consume();
					}
				}
			});
			setOnMousePressed(new EventHandler() // these just need to be captured, so they are not processed by the underlying pane
			{
				@Override
				public void handle(MouseEvent mouseEvent)
				{
					mouseEvent.consume();
				}
			});
			setOnMouseReleased(new EventHandler() // these just need to be captured, so they are not processed by the underlying pane
			{
				@Override
				public void handle(MouseEvent mouseEvent)
				{
					mouseEvent.consume();
				}
			});
			setOnMouseClicked(new EventHandler()
			{
				@Override
				public void handle(MouseEvent mouseEvent)
				{
					mouseEvent.consume();
					showMenu(mouseEvent, abstractAppointmentPane);
				}
			});
		}
	}
	
	
	// ==================================================================================================================
	// DURATION
	
	/**
	 * 
	 */
	class DurationDragger extends Rectangle
	{
		public DurationDragger(AbstractDayAppointmentPane appointmentPane)
		{
			// remember
			this.appointmentPane = appointmentPane;
			
			// bind
			xProperty().bind(appointmentPane.widthProperty().multiply(0.25));
			yProperty().bind(appointmentPane.heightProperty().subtract(5));
			widthProperty().bind(appointmentPane.widthProperty().multiply(0.5));
			setHeight(3);
			
			// styling
			getStyleClass().add("DurationDragger");

			// play with the mouse pointer to show something can be done here
			setOnMouseEntered(new EventHandler()
			{
				@Override
				public void handle(MouseEvent mouseEvent)
				{
					if (!mouseEvent.isPrimaryButtonDown())
					{						
						DurationDragger.this.setCursor(Cursor.HAND);
						
						// no one else
						mouseEvent.consume();
					}
				}
			});
			setOnMouseExited(new EventHandler()
			{
				@Override
				public void handle(MouseEvent mouseEvent)
				{
					if (!mouseEvent.isPrimaryButtonDown())
					{
						DurationDragger.this.setCursor(Cursor.DEFAULT);
						
						// no one else
						mouseEvent.consume();
					}
				}
			});
			// start resize
			setOnMousePressed(new EventHandler()
			{
				@Override
				public void handle(MouseEvent mouseEvent)
				{
					// // record a delta distance for the drag and drop operation.
					// dragDelta.x = stage.getX() - mouseEvent.getScreenX();
					// dragDelta.y = stage.getY() - mouseEvent.getScreenY();
					DurationDragger.this.setCursor(Cursor.V_RESIZE);
					resizeRectangle = new Rectangle(DurationDragger.this.appointmentPane.getLayoutX(), DurationDragger.this.appointmentPane.getLayoutY(), DurationDragger.this.appointmentPane.getWidth(), DurationDragger.this.appointmentPane.getHeight());
					resizeRectangle.getStyleClass().add("GhostRectangle");
					DurationDragger.this.appointmentPane.dayPane.getChildren().add(resizeRectangle);
					
					// this event should not be processed by the appointment area
					mouseEvent.consume();
				}
			});
			// visualize resize
			setOnMouseDragged(new EventHandler()
			{
				@Override
				public void handle(MouseEvent mouseEvent)
				{
					// - calculate the number of pixels from onscreen nodeY (layoutY) to onscreen mouseY					
					double lNodeScreenY = NodeUtil.screenY(DurationDragger.this.appointmentPane);
					double lMouseY = mouseEvent.getScreenY();
					double lHeight = lMouseY - lNodeScreenY;
					if (lHeight < 5) lHeight = 5;
					resizeRectangle.setHeight(lHeight);
					
					// no one else
					mouseEvent.consume();
				}
			});
			// end resize
			setOnMouseReleased(new EventHandler()
			{
				@Override
				public void handle(MouseEvent mouseEvent)
				{					
					// - calculate the new end date for the appointment (recalculating the duration)
					int ms = (int)(resizeRectangle.getHeight() * durationInMSPerPixelProperty.get());
					Calendar lCalendar = (Calendar)DurationDragger.this.appointmentPane.appointment.getStartTime().clone();					
					lCalendar.add(Calendar.MILLISECOND, ms);
					
					// align to X minutes accuracy
					setTimeToNearestMinutes(lCalendar, 5);
					
					// set the new enddate
					DurationDragger.this.appointmentPane.appointment.setEndTime(lCalendar);
					
					// redo whole week
					setupAppointments();
									
					// reset ui
					DurationDragger.this.setCursor(Cursor.HAND);
					DurationDragger.this.appointmentPane.dayPane.getChildren().remove(resizeRectangle);
					resizeRectangle = null;					
					
					// no one else
					mouseEvent.consume();
				}
			});
		}
		final AbstractDayAppointmentPane appointmentPane;
		Rectangle resizeRectangle;
	}
	
	
	// ==================================================================================================================
	// GENERIC APPOINTMENT AREA

	/**
	 * This class has the shared parts of day (non header) appointment panes
	 *
	 */
	abstract class AbstractDayAppointmentPane extends AbstractAppointmentPane
	{
		public AbstractDayAppointmentPane(Agenda.Appointment appointment, DayPane dayPane)
		{
			super(appointment);
			this.dayPane = dayPane;
			
			// for debugging setStyle("-fx-border-color:BLUE;-fx-border-width:4px;");
			getStyleClass().add("Appointment");
			getStyleClass().add(appointment.getAppointmentGroup().getStyleClass());	
		}
		final DayPane dayPane;
		
		Rectangle historicalVisualizer = null;
		Calendar start = null;
		String startAsString = null;
		Calendar end = null;
		String endAsString = null;
		long durationInMS = 0;
		DurationDragger durationDragger = null;
	}
	
	/**
	 * This class handles shared logic for all (both day and header) appointment panes, like dragging and focusable. 
	 */
	abstract class AbstractAppointmentPane extends Pane
	{
		Agenda.Appointment appointment = null;
		boolean isFirstAreaOfAppointment = true;
		boolean isLastAreaOfAppointment = true;
		boolean isDraggable = true;

		/**
		 * 
		 */
		public AbstractAppointmentPane(Agenda.Appointment appointment)
		{
			// remember
			this.appointment = appointment;

			// tooltip
			Tooltip.install(this, new Tooltip(appointment.getSummary()));
			
			// setup menu arrow
			menuIcon = new MenuIcon(this);

			// ------------
			// dragging
			
			// start drag
			setOnMousePressed(new EventHandler()
			{
				@Override
				public void handle(MouseEvent mouseEvent)
				{					
					// no one else
					mouseEvent.consume();
					if (mouseEvent.isPrimaryButtonDown() == false) return;
					if (isDraggable == false) return;

					// no drag yet
					dragEventHasOccurred = mouseEvent.isPrimaryButtonDown() ? false : true; // if not primary mouse, then just assume drag from the start 
					if (isFirstAreaOfAppointment == false) return; // TODO: temporarily

					// place the rectangle
					AbstractAppointmentPane.this.setCursor(Cursor.MOVE);
					double lX = NodeUtil.screenX(AbstractAppointmentPane.this) - NodeUtil.screenX(dragPane);
					double lY = NodeUtil.screenY(AbstractAppointmentPane.this) - NodeUtil.screenY(dragPane);
					dragRectangle = new Rectangle(lX, lY, AbstractAppointmentPane.this.getWidth(), (AbstractAppointmentPane.this.appointment.isWholeDay() ? titleCalendarHeightProperty.get() : AbstractAppointmentPane.this.getHeight()) );
					dragRectangle.getStyleClass().add("GhostRectangle");
					dragPane.getChildren().add(dragRectangle);
					
					// remember
					startX = mouseEvent.getScreenX();
					startY = mouseEvent.getScreenY();
				}
			});
			// visualize dragging
			setOnMouseDragged(new EventHandler()
			{
				@Override
				public void handle(MouseEvent mouseEvent)
				{
					// no one else
					mouseEvent.consume();
					if (mouseEvent.isPrimaryButtonDown() == false) return;

					// no dragged
					dragEventHasOccurred = true;
					if (dragRectangle == null) return;
					
					double lDeltaX = mouseEvent.getScreenX() - startX;
					double lDeltaY = mouseEvent.getScreenY() - startY;
					double lX = NodeUtil.screenX(AbstractAppointmentPane.this) - NodeUtil.screenX(dragPane) + lDeltaX;
					double lY = NodeUtil.screenY(AbstractAppointmentPane.this) - NodeUtil.screenY(dragPane) + lDeltaY;
					dragRectangle.setX(lX);
					dragRectangle.setY(lY);
					
					// no one else
					mouseEvent.consume();
				}
			});
			// end drag
			setOnMouseReleased(new EventHandler()
			{
				@Override
				public void handle(MouseEvent mouseEvent)
				{
					// no one else
					mouseEvent.consume();

					// reset ui
					boolean lDragRectangleWasVisible = (dragRectangle != null);
					AbstractAppointmentPane.this.setCursor(Cursor.HAND);
					dragPane.getChildren().remove(dragRectangle);
					dragRectangle = null;					
					
					// -----
					// select
					
					// if have not dragged (even if the drag rectangle was shown), then we're selecting
					if (dragEventHasOccurred == false)
					{
						// if not shift pressed, clear the selection
						if (mouseEvent.isShiftDown() == false)
						{
							getSkinnable().selectedAppointments().clear();
						}
						// add to selection if not already added
						if (getSkinnable().selectedAppointments().contains(AbstractAppointmentPane.this.appointment) == false)
						{
							getSkinnable().selectedAppointments().add(AbstractAppointmentPane.this.appointment);
						}
						return;
					}
					
					// ------------
					// dragging
					
					if (lDragRectangleWasVisible == false) return;
					
					// find out where it was dropped
					for (DayPane lDayPane : weekPane.dayPanes)
					{
						double lDayX = NodeUtil.screenX(lDayPane); 
						double lDayY = NodeUtil.screenY(lDayPane); 
						if ( lDayX <= mouseEvent.getScreenX() && mouseEvent.getScreenX() < lDayX + lDayPane.getWidth()
						  && lDayY <= mouseEvent.getScreenY() && mouseEvent.getScreenY() < lDayY + lDayPane.getHeight()
						   )
						{
							// get the appointment that needs handling
							Appointment lAppointment = AbstractAppointmentPane.this.appointment;
							Calendar lDroppedOnCalendar = lDayPane.calendarObjectProperty.get();
		
							// is wholeday now, will become partial
							if (lAppointment.isWholeDay())
							{
								// calculate new start
								Calendar lStartCalendar = copyYMD( lDroppedOnCalendar, (Calendar)lAppointment.getStartTime().clone() );
								// and end times
								Calendar lEndCalendar = lAppointment.getEndTime() == null ? setTimeTo2359( (Calendar)lDroppedOnCalendar.clone() ) : copyYMD( lDroppedOnCalendar, (Calendar)lAppointment.getEndTime().clone() );
								
								// set the new enddate
								lAppointment.setStartTime(lStartCalendar);
								lAppointment.setEndTime(lEndCalendar);
								
								// no longer whole day
								lAppointment.setWholeDay(false);
							}
							else
							{
								// duration
								long lDurationInMS = lAppointment.getEndTime().getTimeInMillis() - lAppointment.getStartTime().getTimeInMillis();
								
								// calculate new start
								Calendar lStartCalendar = copyYMD(lDroppedOnCalendar, (Calendar)lAppointment.getStartTime().clone());
	
								// also add the delta Y minutes
								int lDeltaDurationInMS = (int)((mouseEvent.getScreenY() - startY) * durationInMSPerPixelProperty.get());
								lStartCalendar.add(Calendar.MILLISECOND, lDeltaDurationInMS);
								setTimeToNearestMinutes(lStartCalendar, 5);
								while (isSameDay(lStartCalendar, lDroppedOnCalendar) == false && lStartCalendar.before(lDroppedOnCalendar)) { lStartCalendar.add(Calendar.MINUTE, 1);  }// the delta may have pushed it out of today 
								while (isSameDay(lStartCalendar, lDroppedOnCalendar) == false && lStartCalendar.after(lDroppedOnCalendar)) { lStartCalendar.add(Calendar.MINUTE, -1);  }// the delta may have pushed it out of today
								
								// calculate
								Calendar lEndCalendar = (Calendar)lStartCalendar.clone();
								lEndCalendar.add(Calendar.MILLISECOND, (int)lDurationInMS);
								
								// set the new enddate
								lAppointment.setStartTime(lStartCalendar);
								lAppointment.setEndTime(lEndCalendar);
							}
						}
					}
					
					// find out where it was dropped
					for (DayHeaderPane lDayHeaderPane : weekHeaderPane.dayHeaderPanes)
					{
						double lDayX = NodeUtil.screenX(lDayHeaderPane); 
						double lDayY = NodeUtil.screenY(lDayHeaderPane); 
						if ( lDayX <= mouseEvent.getScreenX() && mouseEvent.getScreenX() < lDayX + lDayHeaderPane.getWidth()
						  && lDayY <= mouseEvent.getScreenY() && mouseEvent.getScreenY() < lDayY + lDayHeaderPane.getHeight()
						   )
						{
							// get the appointment that needs handling
							Appointment lAppointment = AbstractAppointmentPane.this.appointment;
							
							// calculate new start
							Calendar lStartCalendar = copyYMD(lDayHeaderPane.dayPane.calendarObjectProperty.get(), (Calendar)lAppointment.getStartTime().clone() );
							
							// set the new start date
							lAppointment.setStartTime(lStartCalendar);
							
							// enddate can be ignored
							
							// now a whole day (just in case it wasn't)
							lAppointment.setWholeDay(true);
						}
					}
					
					// redo whole week
					setupAppointments();					
				}
			});
		}
		Rectangle dragRectangle;
		double startX = 0;
		double startY = 0;
		boolean dragEventHasOccurred = false;
		Rectangle menuIcon = null;
		
//		public String describe()
//		{
//			// strings
//			StringBuilder lStringBuilder = new StringBuilder();
//			lStringBuilder.append( dateFormat.format(this.appointment.getStartTime().getTime()) );
//			if (this.appointment.isWholeDay() == false)
//			{
//				lStringBuilder.append( " " );
//				lStringBuilder.append( timeFormat.format(this.appointment.getStartTime().getTime()) );
//				lStringBuilder.append( " - " );
//				if (isSameDay(this.appointment.getStartTime(), this.appointment.getEndTime()) == false)
//				{
//					lStringBuilder.append( dateFormat.format(this.appointment.getEndTime().getTime()) );
//					lStringBuilder.append( " " );
//				}
//				lStringBuilder.append( timeFormat.format(this.appointment.getEndTime().getTime()) );
//			}
//			return lStringBuilder.toString();
//		}
	}
	AbstractAppointmentPane focused = null;

	
	// ==================================================================================================================
	// NOW
	
	final Rectangle nowLine = new Rectangle(0,0,0,0);
	
	/**
	 * This is implemented as a runnable so it can be called from a timer, but also directly
	 */
	Runnable nowUpdateRunnable = new Runnable()
	{
		{
			nowLine.getStyleClass().add("Now");
			nowLine.setHeight(3);
		}
		
		@Override
		public void run()
		{
			//  get now
			Calendar lNow = Calendar.getInstance();
			
			// see if we are displaying now (this has to do with the fact that now may slide in or out of the view)
			// check all days
			boolean lFound = false;
			for (DayPane lDayPane : weekPane.dayPanes)
			{
				// if the calendar of the day is the same day as now
				if (isSameDay(lDayPane.calendarObjectProperty.get(), lNow) == false)
				{
					// not today
					lDayPane.getStyleClass().remove("today");					
				}
				else
				{
					// today
					if (lDayPane.getStyleClass().contains("today") == false) 
					{
						lDayPane.getStyleClass().add("today");
					}
					lFound = true;
					
					// add if not present
					if (weekPane.getChildren().contains(nowLine) == false)
					{
						weekPane.getChildren().add(nowLine);
						nowLine.xProperty().bind(lDayPane.layoutXProperty());
					}

					// place it
					int lOffsetY = (lNow.get(Calendar.HOUR_OF_DAY) * 60) + lNow.get(Calendar.MINUTE);
					nowLine.setY(dayHeightProperty.get() / (24 * 60) * lOffsetY );
					if (nowLine.widthProperty().isBound() == false) nowLine.widthProperty().bind(dayWidthProperty);	

				}
				
				// display history
				for (RegularAppointmentPane lAppointmentPane : lDayPane.regularAppointmentPanes)
				{
					lAppointmentPane.historicalVisualizer.setVisible( lAppointmentPane.start.before(lNow));
				}
				for (WholedayAppointmentPane lAppointmentPane : lDayPane.wholedayAppointmentPanes)
				{
					lAppointmentPane.historicalVisualizer.setVisible( lAppointmentPane.start.before(lNow));
				}
			}
			
			// if cannot be placed, remove
			if (lFound == false)
			{
				weekPane.getChildren().remove(nowLine);
			}
			
			// also for headers
			for (DayHeaderPane lDayHeaderPane : weekHeaderPane.dayHeaderPanes)
			{
				for (AppointmentHeaderPane lAppointmentHeaderPane : lDayHeaderPane.appointmentHeaderPanes)
				{
					lAppointmentHeaderPane.historicalVisualizer.setVisible(lAppointmentHeaderPane.appointment.getStartTime().before(lNow));
				}
			}
		}
	};
	
	/**
	 * This timer takes care of updating NOW
	 */
	Timer nowTimer = new Timer(nowUpdateRunnable)
		.withCycleDuration(new Duration(60 * 1000)) // every minute
		.withDelay(new Duration( (60 - Calendar.getInstance().get(Calendar.SECOND)) * 1000)) // trigger exactly on each new minute
		.start();  

	
	// ==================================================================================================================
	// POPUP

	/*
	 * 
	 */
	private void showMenu(MouseEvent evt, final AbstractAppointmentPane abstractAppointmentPane)
	{
		// create popup
		final Popup lPopup = new Popup();
		lPopup.setAutoFix(true);
		lPopup.setAutoHide(true);
		lPopup.setHideOnEscape(true);
		lPopup.setOnHidden(new EventHandler()
		{
			@Override
			public void handle(WindowEvent arg0)
			{
				setupAppointments();
			}
		});

		BorderPane lBorderPane = new BorderPane();
		lBorderPane.getStyleClass().add(this.getClass().getSimpleName() + "_popup");
		lPopup.getContent().add(lBorderPane);
		
		// close icon
		ImageView lImageView = new ImageView(closeIconImage);
		lImageView.setPickOnBounds(true);
		lImageView.setOnMouseClicked(new EventHandler()
		{
			@Override public void handle(MouseEvent evt)
			{
				lPopup.hide(); 
			}
		});
		lBorderPane.setRight(lImageView);

		// initial layout
		VBox lMenuVBox = new VBox(padding);
		lBorderPane.setCenter(lMenuVBox);

		// time
		lMenuVBox.getChildren().add(new Text("Time:"));
		// start
		final CalendarTextField lStartCalendarTextField = new CalendarTextField().withShowTime(true);
		lStartCalendarTextField.setLocale(getSkinnable().getLocale());
		lStartCalendarTextField.setValue(abstractAppointmentPane.appointment.getStartTime());
		lMenuVBox.getChildren().add(lStartCalendarTextField);
		// end
		final CalendarTextField lEndCalendarTextField = new CalendarTextField().withShowTime(true);
		lEndCalendarTextField.setLocale(getSkinnable().getLocale());
		lEndCalendarTextField.setValue(abstractAppointmentPane.appointment.getEndTime());
		lMenuVBox.getChildren().add(lEndCalendarTextField);
		lEndCalendarTextField.valueProperty().addListener(new ChangeListener()
		{
			@Override
			public void changed(ObservableValue arg0, Calendar oldValue, Calendar newValue)
			{
				abstractAppointmentPane.appointment.setEndTime(newValue);
				// refresh is done upon popup close
			}
		});
		lEndCalendarTextField.setVisible(abstractAppointmentPane.appointment.getEndTime() != null);
		// wholeday
		final CheckBox lWholedayCheckBox = new CheckBox("Wholeday");
		lWholedayCheckBox.selectedProperty().set(abstractAppointmentPane.appointment.isWholeDay());
		lMenuVBox.getChildren().add(lWholedayCheckBox);
		lWholedayCheckBox.selectedProperty().addListener(new ChangeListener()
		{
			@Override
			public void changed(ObservableValue arg0, Boolean oldValue, Boolean newValue)
			{
				abstractAppointmentPane.appointment.setWholeDay(newValue);
				if (newValue == true) 
				{
					abstractAppointmentPane.appointment.setEndTime(null);
				}
				else
				{
					Calendar lEndTime = (Calendar)abstractAppointmentPane.appointment.getStartTime().clone();
					lEndTime.add(Calendar.MINUTE, 30);
					abstractAppointmentPane.appointment.setEndTime(lEndTime);
					lEndCalendarTextField.setValue(abstractAppointmentPane.appointment.getEndTime());
				}
				lEndCalendarTextField.setVisible(abstractAppointmentPane.appointment.getEndTime() != null);
				// refresh is done upon popup close
			}
		});
		// event handling
		lStartCalendarTextField.valueProperty().addListener(new ChangeListener()
		{
			@Override
			public void changed(ObservableValue arg0, Calendar oldValue, Calendar newValue)
			{
				// enddate
				if (abstractAppointmentPane.appointment.isWholeDay())
				{
					abstractAppointmentPane.appointment.setStartTime(newValue);
				}
				else
				{
					// calculate duration
					long lDurationInMS = abstractAppointmentPane.appointment.getEndTime().getTimeInMillis() - abstractAppointmentPane.appointment.getStartTime().getTimeInMillis();
					
					// set
					abstractAppointmentPane.appointment.setStartTime(newValue);
					
					// end date
					Calendar lEndCalendar = (Calendar)abstractAppointmentPane.appointment.getStartTime().clone();
					lEndCalendar.add(Calendar.MILLISECOND, (int)lDurationInMS);
					abstractAppointmentPane.appointment.setEndTime(lEndCalendar);
					
					// update field
					lEndCalendarTextField.setValue(abstractAppointmentPane.appointment.getEndTime());
					
					// refresh is done upon popup close
				}
			}
		});
		
		// summary
		lMenuVBox.getChildren().add(new Text("Summary:"));
		TextField lSummaryTextField = new TextField();
		lSummaryTextField.setText(abstractAppointmentPane.appointment.getSummary());
		lSummaryTextField.textProperty().addListener(new ChangeListener()
		{
			@Override
			public void changed(ObservableValue arg0, String oldValue, String newValue)
			{
				abstractAppointmentPane.appointment.setSummary(newValue);
				// refresh is done upon popup close
			}
		});
		lMenuVBox.getChildren().add(lSummaryTextField);
		
		// location
		lMenuVBox.getChildren().add(new Text("Location:"));
		TextField lLocationTextField = new TextField();
		lLocationTextField.setText( abstractAppointmentPane.appointment.getLocation() == null ? "" : abstractAppointmentPane.appointment.getLocation());
		lLocationTextField.textProperty().addListener(new ChangeListener()
		{
			@Override
			public void changed(ObservableValue arg0, String oldValue, String newValue)
			{
				abstractAppointmentPane.appointment.setLocation(newValue);
				// refresh is done upon popup close
			}
		});
		lMenuVBox.getChildren().add(lLocationTextField);
		
		// actions
		lMenuVBox.getChildren().add(new Text("Actions:"));
		HBox lHBox = new HBox();
		lMenuVBox.getChildren().add(lHBox);
		// delete
		{
			ImageButton lImageButton = new ImageButton( new Image(this.getClass().getResourceAsStream("jqueryMobileBlack16x16/delete.png")) );
			lImageButton.setOnMouseClicked(new EventHandler()
			{
				@Override public void handle(MouseEvent evt)
				{
					lPopup.hide();
					getSkinnable().appointments().remove(abstractAppointmentPane.appointment);
					// refresh is done via the collection events
				}
			});
			Tooltip.install(lImageButton, new Tooltip("Delete"));
			lHBox.getChildren().add(lImageButton);
		}
		
		// construct a area of appointment groups
		lMenuVBox.getChildren().add(new Text("Group:"));
		GridPane lAppointmentGroupGridPane = new GridPane();
		lMenuVBox.getChildren().add(lAppointmentGroupGridPane);
		lAppointmentGroupGridPane.getStyleClass().add("AppointmentGroups");
		lAppointmentGroupGridPane.setHgap(2);
		lAppointmentGroupGridPane.setVgap(2);
		int lCnt = 0;
		for (Agenda.AppointmentGroup lAppointmentGroup : getSkinnable().appointmentGroups())
		{
			// create the appointment group
			final Pane lPane = new Pane();			
			lPane.setPrefSize(15, 15);
			lPane.getStyleClass().addAll("AppointmentGroup", lAppointmentGroup.getStyleClass());
			lAppointmentGroupGridPane.add(lPane, lCnt % 10, lCnt / 10 );
			lCnt++;
			
			// tooltip
			Tooltip.install(lPane, new Tooltip(lAppointmentGroup.getDescription()));
			 
			// mouse reactions
			lPane.setOnMouseEntered(new EventHandler()
			{
				@Override
				public void handle(MouseEvent mouseEvent)
				{
					if (!mouseEvent.isPrimaryButtonDown())
					{						
						mouseEvent.consume();
						lPane.setCursor(Cursor.HAND);
					}
				}
			});
			lPane.setOnMouseExited(new EventHandler()
			{
				@Override
				public void handle(MouseEvent mouseEvent)
				{
					if (!mouseEvent.isPrimaryButtonDown())
					{
						mouseEvent.consume();
						lPane.setCursor(Cursor.DEFAULT);
					}
				}
			});
			final Agenda.AppointmentGroup lAppointmentGroupFinal = lAppointmentGroup; 
			lPane.setOnMouseClicked(new EventHandler()
			{
				@Override
				public void handle(MouseEvent mouseEvent)
				{
					mouseEvent.consume();
					
					// assign appointment group
					abstractAppointmentPane.appointment.setAppointmentGroup(lAppointmentGroupFinal);
					
					// refresh is done upon popup close
					lPopup.hide();
				}
			});
		}
		
		// show it just below the menu icon
		lPopup.show(abstractAppointmentPane, NodeUtil.screenX(abstractAppointmentPane), NodeUtil.screenY(abstractAppointmentPane.menuIcon) + abstractAppointmentPane.menuIcon.getHeight());
	}

	// ==================================================================================================================
	// SUPPORT

	/**
	 * check if a certain weekday name is a certain day-of-the-week
	 */
	protected boolean isWeekday(int idx, int weekdaynr)
	{
		// setup the dayLabels
		// Calendar.SUNDAY = 1 and Calendar.SATURDAY = 7
		Calendar lCalendar = new java.util.GregorianCalendar(2009, 6, 4 + getSkinnable().getDisplayedCalendar().getFirstDayOfWeek()); // july 5th 2009 is a Sunday
		lCalendar.add(java.util.Calendar.DATE, idx);
		int lDayOfWeek = lCalendar.get(java.util.Calendar.DAY_OF_WEEK);

		// check
		return (lDayOfWeek == weekdaynr);
	}

	/**
	 * check if a certain weekday name is a certain day-of-the-week
	 */
	protected boolean isWeekdayWeekend(int idx) 
	{
		return (isWeekday(idx, java.util.Calendar.SATURDAY) || isWeekday(idx, java.util.Calendar.SUNDAY));
	}
	

	/**
	 * 
	 */
	private void calculateSizes()
	{
		// generic
		double lScrollbarSize = new ScrollBar().getWidth();
		textHeight1MProperty.set( new Text("X").getBoundsInParent().getHeight() );
		
		// header
		highestNumberOfWholedayAppointmentsProperty.set(0);
		for (DayPane lDay : weekPane.dayPanes)
		{
			if (lDay.wholedayAppointmentPanes.size() > highestNumberOfWholedayAppointmentsProperty.get())
			{
				highestNumberOfWholedayAppointmentsProperty.set( lDay.wholedayAppointmentPanes.size() );
			}
		}
		titleCalendarHeightProperty.set( 1.5 * textHeight1MProperty.get() ); 
		wholedayTitleHeightProperty.set( textHeight1MProperty.get() + 5 ); // not sure why the 5 is needed
		headerHeightProperty.set( titleCalendarHeightProperty.get() + (highestNumberOfWholedayAppointmentsProperty.get() * wholedayTitleHeightProperty.get()) );

		// time column
		timeWidthProperty.set( new Text("88:88").getBoundsInParent().getWidth() + timeColumnWhitespace );
		
		// day columns
		dayFirstColumnXProperty.set( timeWidthProperty.get() );
		if (weekScrollPane.viewportBoundsProperty().get() != null) 
		{
			dayWidthProperty.set( (weekScrollPane.viewportBoundsProperty().get().getWidth() - timeWidthProperty.get()) / 7 ); // 7 days per week
		}
		dayContentWidthProperty.set( dayWidthProperty.get() - 10 ); // the 10 is a margin at the right so that there is always room to start a new appointment
		
		// hour height
		hourHeighProperty.set( (2 * textHeight1MProperty.get()) + 10 ); // 10 is padding
		if (weekScrollPane.viewportBoundsProperty().get() != null && (weekScrollPane.viewportBoundsProperty().get().getHeight() - lScrollbarSize) > hourHeighProperty.get() * 24)
		{
			// if there is more room than absolutely required, let the height grow with the available room
			hourHeighProperty.set( (weekScrollPane.viewportBoundsProperty().get().getHeight() - lScrollbarSize) / 24 );
		}
		dayHeightProperty.set(hourHeighProperty.get() * 24);
		durationInMSPerPixelProperty.set( (24 * 60 * 60 * 1000) / dayHeightProperty.get() );
	}
	private final double padding = 3;
	private final double timeColumnWhitespace = 10.0;
	private final double wholedayAppointmentWidth = 5;
	private final IntegerProperty highestNumberOfWholedayAppointmentsProperty = new SimpleIntegerProperty(0);
	private final DoubleProperty textHeight1MProperty = new SimpleDoubleProperty(0);
	private final DoubleProperty titleCalendarHeightProperty = new SimpleDoubleProperty(0);
	private final DoubleProperty headerHeightProperty = new SimpleDoubleProperty(0);
	private final DoubleProperty wholedayTitleHeightProperty = new SimpleDoubleProperty(0);
	private final DoubleProperty timeWidthProperty = new SimpleDoubleProperty(0); 
	private final DoubleProperty dayFirstColumnXProperty = new SimpleDoubleProperty(0); 
	private final DoubleProperty dayWidthProperty = new SimpleDoubleProperty(0); 
	private final DoubleProperty dayContentWidthProperty = new SimpleDoubleProperty(0); 
	private final DoubleProperty dayHeightProperty = new SimpleDoubleProperty(0);  
	private final DoubleProperty durationInMSPerPixelProperty = new SimpleDoubleProperty(0);
	private final DoubleProperty hourHeighProperty = new SimpleDoubleProperty(0); 
	
	
	/**
	 * get the calendar for the first day of the week
	 */
	protected Calendar getFirstDayOfWeekCalendar()
	{
		// result
		Calendar lLocalCalendar = Calendar.getInstance(getSkinnable().getLocale());
		int lFirstDayOfWeek = lLocalCalendar.getFirstDayOfWeek();
		
		// get the displayed calendar
		Calendar lDisplayedCalendar = getSkinnable().getDisplayedCalendar();
		if (lDisplayedCalendar == null) return null;
		
		// work towards the first day of week calendar
		Calendar lFirstDayOfWeekCalendar = (Calendar)lDisplayedCalendar.clone();
		
		// if not on the first day of the week, correct with the appropriate amount
		lFirstDayOfWeekCalendar.add(Calendar.DATE, lFirstDayOfWeek - lFirstDayOfWeekCalendar.get(Calendar.DAY_OF_WEEK));
		
		// make sure we are in the same week
		while ( lFirstDayOfWeekCalendar.get(Calendar.YEAR) > lDisplayedCalendar.get(Calendar.YEAR)
			 || (lFirstDayOfWeekCalendar.get(Calendar.YEAR) == lDisplayedCalendar.get(Calendar.YEAR) && lFirstDayOfWeekCalendar.get(Calendar.WEEK_OF_YEAR) > lDisplayedCalendar.get(Calendar.WEEK_OF_YEAR))
			  )
		{
			lFirstDayOfWeekCalendar.add(Calendar.DATE, -7);
		}
		while ( lFirstDayOfWeekCalendar.get(Calendar.YEAR) < lDisplayedCalendar.get(Calendar.YEAR)
		     || (lFirstDayOfWeekCalendar.get(Calendar.YEAR) == lDisplayedCalendar.get(Calendar.YEAR) && lFirstDayOfWeekCalendar.get(Calendar.WEEK_OF_YEAR) < lDisplayedCalendar.get(Calendar.WEEK_OF_YEAR))
		      )
		{
			lFirstDayOfWeekCalendar.add(Calendar.DATE, 7);
		}
		
		// done
		return lFirstDayOfWeekCalendar;
	}

	/**
	 * 
	 * @param c1
	 * @param c2
	 * @return
	 */
	private boolean isSameDay(Calendar c1, Calendar c2)
	{
		return c1.get(Calendar.YEAR) == c2.get(Calendar.YEAR)
			&& c1.get(Calendar.MONTH) == c2.get(Calendar.MONTH)
			&& c1.get(Calendar.DATE) == c2.get(Calendar.DATE)
			 ; 
	}

	/**
	 * 
	 * @param tracks
	 * @param appointmentPane
	 * @return
	 */
	private int determineTrackWhereAppointmentCanBeAdded(List> tracks, RegularAppointmentPane appointmentPane)
	{
		int lTrackNr = 0;
		while (true)
		{
			// make sure there is a arraylist for this track
			if (lTrackNr == tracks.size()) tracks.add(new ArrayList());
			
			// scan all existing appointments in this track and see if there is an overlap
			if (checkIfTheAppointmentOverlapsAnAppointmentAlreadyInThisTrack(tracks, lTrackNr, appointmentPane) == false)
			{
				// no overlap, it can be added here
				return lTrackNr;
			}

			// overlap, try next track
			lTrackNr++;
		}
	}
	
	/**
	 * 
	 * @param tracks
	 * @param tracknr
	 * @param appointmentPane
	 * @return
	 */
	private boolean checkIfTheAppointmentOverlapsAnAppointmentAlreadyInThisTrack(List> tracks, int tracknr, RegularAppointmentPane appointmentPane)
	{
		// get the track
		List lTrack = tracks.get(tracknr);
		
		// scan all existing appointments in this track
		for (RegularAppointmentPane lAppointmentPane : lTrack)
		{
			// There is an overlap:
			// if the start time of the already placed appointment is before or equals the new appointment's end time 
			// and the end time of the already placed appointment is after or equals the new appointment's start time
			// ...PPPPPPPPP...
			// .NNNN.......... -> Ps <= Ne & Pe >= Ns -> overlap
			// .....NNNNN..... -> Ps <= Ne & Pe >= Ns -> overlap
			// ..........NNN.. -> Ps <= Ne & Pe >= Ns -> overlap
			// .NNNNNNNNNNNNN. -> Ps <= Ne & Pe >= Ns -> overlap
			// .N............. -> false	& Pe >= Ns -> no overlap
			// .............N. -> Ps <= Ne & false	-> no overlap
			if ( (lAppointmentPane.start.equals(appointmentPane.start) || lAppointmentPane.start.before(appointmentPane.end)) 
			  && (lAppointmentPane.end.equals(appointmentPane.start) || lAppointmentPane.end.after(appointmentPane.start))
			   )
			{
				// overlap
				return true;
			}
		}
		
		// no overlap
		return false;
	}
	
	/**
	 * 
	 * @param c
	 * @return
	 */
	private Calendar setTimeTo0000(Calendar c)
	{
		// start
		c.set(Calendar.HOUR_OF_DAY, 0);
		c.set(Calendar.MINUTE, 0);
		c.set(Calendar.SECOND, 0);
		c.set(Calendar.MILLISECOND, 0);
		return c;
	}
	
	/**
	 * 
	 * @param c
	 * @return
	 */
	private Calendar setTimeTo2359(Calendar c)
	{
		c.set(Calendar.HOUR_OF_DAY, 23);
		c.set(Calendar.MINUTE, 59);
		c.set(Calendar.SECOND, 59);
		c.set(Calendar.MILLISECOND, 999);
		return c;
	}
	
	/**
	 * 
	 * @param c
	 * @param minutes
	 * @return
	 */
	private Calendar setTimeToNearestMinutes(Calendar c, int minutes)
	{
		// align to X minutes accuracy
		c.set(Calendar.MILLISECOND, 0);
		c.set(Calendar.SECOND, 0);
		int lMinutes = c.get(Calendar.MINUTE) % minutes;
		if (lMinutes < (minutes/2)) c.add(Calendar.MINUTE, -1 * lMinutes);
		else c.add(Calendar.MINUTE, minutes - lMinutes);
		return c;
	}
	
	/**
	 * 
	 * @param from
	 * @param to
	 * @return
	 */
	private Calendar copyYMD(Calendar from, Calendar to)
	{
		to.set(Calendar.YEAR, from.get(Calendar.YEAR));
		to.set(Calendar.MONTH, from.get(Calendar.MONTH));
		to.set(Calendar.DATE, from.get(Calendar.DATE));
		return to;
	}
	
	class ImageButton extends ImageView
	{
		public ImageButton(Image i)
		{
			super(i);
			setPickOnBounds(true);
			setOnMouseEntered(new EventHandler()
			{
				@Override
				public void handle(MouseEvent mouseEvent)
				{
					if (!mouseEvent.isPrimaryButtonDown())
					{						
						ImageButton.this.setCursor(Cursor.HAND);
					}
				}
			});
			setOnMouseExited(new EventHandler()
			{
				@Override
				public void handle(MouseEvent mouseEvent)
				{
					if (!mouseEvent.isPrimaryButtonDown())
					{
						ImageButton.this.setCursor(Cursor.DEFAULT);
					}
				}
			});
		}
	}
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy