| /******************************************************************************* |
| * Copyright (c) 2006 The Pampered Chef and others. |
| * All rights reserved. This program and the accompanying materials |
| * are made available under the terms of the Eclipse Public License v1.0 |
| * which accompanies this distribution, and is available at |
| * http://www.eclipse.org/legal/epl-v10.html |
| * |
| * Contributors: |
| * The Pampered Chef - initial API and implementation |
| ******************************************************************************/ |
| |
| package org.eclipse.jface.examples.databinding.compositetable.timeeditor; |
| |
| import java.util.ArrayList; |
| import java.util.Calendar; |
| import java.util.Date; |
| import java.util.GregorianCalendar; |
| import java.util.Iterator; |
| import java.util.LinkedList; |
| import java.util.List; |
| |
| /** |
| * Represents the model behind the calendar control. This model manages three |
| * concerns: |
| * 1) Setting/maintaining the visible range of days (startDate, numberOfDays) |
| * 2) Keeping the events for a particular day within the range of visible days |
| * 3) Keeping track of the number of columns required to display the events |
| * in a given day from the set of visible days. |
| * |
| * @since 3.2 |
| */ |
| public class CalendarableModel { |
| |
| private static final int DEFAULT_START_HOUR = 8; |
| |
| private int numberOfDays = -1; |
| private int numberOfDivisionsInHour = -1; |
| private ArrayList[] dayColumns = null; |
| private CalendarableItem[][][] eventLayout = null; // [day][column][row] |
| |
| private int defaultStartHour = DEFAULT_START_HOUR; |
| |
| /** |
| * @param dayOffset |
| * @return the number of columns within the day or -1 if this has not been computed yet. |
| */ |
| public int getNumberOfColumnsWithinDay(int dayOffset) { |
| if (eventLayout == null) { |
| return -1; |
| } |
| if (eventLayout[dayOffset] == null) { |
| return -1; |
| } |
| return eventLayout[dayOffset].length; |
| } |
| |
| /** |
| * Sets the eventLayout for a particular dayOffset |
| * |
| * @param dayOffset |
| * @param eventLayout |
| */ |
| public void setEventLayout(int dayOffset, CalendarableItem[][] eventLayout) { |
| this.eventLayout[dayOffset] = eventLayout; |
| } |
| |
| /** |
| * Gets the eventLayout for a particular dayOffset |
| * |
| * @param dayOffset |
| * @return the eventLayout array for the specified day or null if none has been computed. |
| */ |
| public CalendarableItem[][] getEventLayout(int dayOffset) { |
| return eventLayout[dayOffset]; |
| } |
| |
| /** |
| * @param numberOfDays |
| * @param numberOfDivisionsInHour |
| */ |
| public void setTimeBreakdown(int numberOfDays, int numberOfDivisionsInHour) { |
| if (numberOfDivisionsInHour < 1) { |
| throw new IllegalArgumentException("There must be at least one division in the hour"); |
| } |
| |
| if (numberOfDays < 1) { |
| throw new IllegalArgumentException("There must be at least one day in the editor"); |
| } |
| |
| this.numberOfDays = numberOfDays; |
| this.numberOfDivisionsInHour = numberOfDivisionsInHour; |
| initializeDayArrays(numberOfDays); |
| |
| refresh(); |
| } |
| |
| private void initializeDayArrays(int numberOfDays) { |
| dayColumns = new ArrayList[numberOfDays]; |
| for (int i=0; i < numberOfDays; ++i) { |
| dayColumns[i] = new ArrayList(); |
| } |
| eventLayout = new CalendarableItem[numberOfDays][][]; |
| } |
| |
| /** |
| * @return The number of days to display |
| */ |
| public int getNumberOfDays() { |
| return numberOfDays; |
| } |
| |
| /** |
| * @return Returns the numberOfDivisionsInHour. |
| */ |
| public int getNumberOfDivisionsInHour() { |
| return numberOfDivisionsInHour; |
| } |
| |
| private Date startDate = null; |
| |
| /** |
| * @param startDate The starting date to display |
| * @return The obsolete Calendarable objects |
| */ |
| public List setStartDate(Date startDate) { |
| // If there's no overlap between the old and new date ranges |
| if (this.startDate == null || |
| startDate.after(calculateDate(this.startDate, numberOfDays-1)) || |
| calculateDate(startDate, numberOfDays-1).before(this.startDate)) |
| { |
| this.startDate = startDate; |
| eventLayout = new CalendarableItem[numberOfDays][][]; |
| return refresh(); |
| } |
| |
| // There's an overlap |
| List obsoleteCalendarables = new LinkedList(); |
| int overlap = -1; |
| |
| // If we're scrolling viewport to the left |
| if (startDate.before(this.startDate)) { |
| // Calculate the overlap |
| for (int day=0; day < numberOfDays; ++day) { |
| Date candidate = calculateDate(startDate, day); |
| if (candidate.equals(this.startDate)) |
| overlap = day; |
| } |
| for (int day=numberOfDays-1; day >= 0; --day) { |
| if (numberOfDays - day <= overlap) { |
| // Shift the arrays; track obsolete calendarables |
| for (Iterator invalidated = dayColumns[day].iterator(); invalidated.hasNext();) { |
| obsoleteCalendarables.add(invalidated.next()); |
| } |
| dayColumns[day] = dayColumns[day-overlap]; |
| eventLayout[day] = eventLayout[day-overlap]; |
| } if (day >= overlap) { |
| // Shift the arrays |
| dayColumns[day] = dayColumns[day-overlap]; |
| eventLayout[day] = eventLayout[day-overlap]; |
| } else { |
| // Recalculate new columns |
| dayColumns[day] = new ArrayList(); |
| eventLayout[day] = null; |
| refresh(calculateDate(startDate, day), day, obsoleteCalendarables); |
| } |
| } |
| } else { |
| // We're scrolling the viewport to the right |
| for (int day=0; day < numberOfDays; ++day) { |
| Date candidate = calculateDate(this.startDate, day); |
| if (candidate.equals(startDate)) |
| overlap = day; |
| } |
| for (int day=0; day < numberOfDays; ++day) { |
| if (day < overlap) { |
| // Shift the arrays; track obsolete calendarables |
| for (Iterator invalidated = dayColumns[day].iterator(); invalidated.hasNext();) { |
| obsoleteCalendarables.add(invalidated.next()); |
| } |
| dayColumns[day] = dayColumns[day+overlap]; |
| eventLayout[day] = eventLayout[day+overlap]; |
| } if (day < numberOfDays - overlap) { |
| // Shift the arrays |
| dayColumns[day] = dayColumns[day+overlap]; |
| eventLayout[day] = eventLayout[day+overlap]; |
| } else { |
| // Recalculate new columns |
| dayColumns[day] = new ArrayList(); |
| eventLayout[day] = null; |
| refresh(calculateDate(startDate, day), day, obsoleteCalendarables); |
| } |
| } |
| } |
| this.startDate = startDate; |
| return obsoleteCalendarables; |
| } |
| |
| /** |
| * @return The starting date to display |
| */ |
| public Date getStartDate() { |
| return startDate; |
| } |
| |
| private EventCountProvider eventCountProvider = null; |
| |
| /** |
| * Sets a strategy pattern object that can return the number of events |
| * to display on a particulr day. |
| * |
| * @param eventCountProvider |
| */ |
| public void setEventCountProvider(EventCountProvider eventCountProvider) { |
| this.eventCountProvider = eventCountProvider; |
| refresh(); |
| } |
| |
| private EventContentProvider eventContentProvider = null; |
| |
| /** |
| * Sets a strategy pattern object that can set the data for the actual events for |
| * a particular day. |
| * |
| * @param eventContentProvider |
| */ |
| public void setEventContentProvider(EventContentProvider eventContentProvider) { |
| this.eventContentProvider = eventContentProvider; |
| refresh(); |
| } |
| |
| /** |
| * Refresh everything in the display. |
| */ |
| private List refresh() { |
| if (startDate == null) { |
| return new LinkedList(); |
| } |
| LinkedList result = new LinkedList(); |
| if(!isInitialized()) { |
| return result; |
| } |
| //refresh |
| Date dateToRefresh = null; |
| for (int i = 0; i < dayColumns.length; i++) { |
| dateToRefresh = calculateDate(startDate, i); |
| refresh(dateToRefresh, i, result); |
| } |
| return result; |
| } |
| |
| /** |
| * Returns the date that is the numberOfDaysFromStartDate. |
| * |
| * @param startDate The start date |
| * @param numberOfDaysFromStartDate |
| * @return Date |
| */ |
| public Date calculateDate(Date startDate, int numberOfDaysFromStartDate) { |
| GregorianCalendar gc = new GregorianCalendar(); |
| gc.setTime(startDate); |
| gc.add(Calendar.DATE, numberOfDaysFromStartDate); |
| return gc.getTime(); |
| } |
| |
| /** |
| * Has all data been set for a refresh. |
| * |
| */ |
| private boolean isInitialized() { |
| return |
| null != startDate && |
| numberOfDays > 0 && |
| numberOfDivisionsInHour > 0 && |
| null != eventContentProvider && |
| null != eventCountProvider; |
| } |
| |
| private void refresh(Date date, int column, List invalidatedElements) { |
| if (eventCountProvider == null || eventContentProvider == null) { |
| return; |
| } |
| |
| int numberOfEventsInDay = eventCountProvider.getNumberOfEventsInDay(date); |
| |
| while (dayColumns[column].size() > 0) { |
| invalidatedElements.add(dayColumns[column].remove(0)); |
| } |
| resizeList(date, dayColumns[column], numberOfEventsInDay); |
| |
| CalendarableItem[] tempEvents = (CalendarableItem[]) dayColumns[column] |
| .toArray(new CalendarableItem[numberOfEventsInDay]); |
| |
| eventContentProvider.refresh( |
| date, |
| tempEvents); |
| } |
| |
| private void resizeList(Date date, ArrayList list, int numberOfEventsInDay) { |
| while (list.size() < numberOfEventsInDay) { |
| list.add(new CalendarableItem(date)); |
| } |
| while (list.size() > numberOfEventsInDay) { |
| list.remove(0); |
| } |
| } |
| |
| /** |
| * Refresh the display for the specified Date. If Date isn't being |
| * displayed, this method ignores the request. |
| * |
| * @param date the date to refresh. |
| * @return List any Calendarables that were invalidated |
| */ |
| public List refresh(Date date) { |
| LinkedList invalidatedCalendarables = new LinkedList(); |
| GregorianCalendar dateToRefresh = new GregorianCalendar(); |
| dateToRefresh.setTime(date); |
| for (int offset=0; offset < numberOfDays; ++offset) { |
| Date refreshTarget = calculateDate(startDate, offset); |
| GregorianCalendar target = new GregorianCalendar(); |
| target.setTime(refreshTarget); |
| |
| if (target.get(Calendar.DATE) == dateToRefresh.get(Calendar.DATE) && |
| target.get(Calendar.MONTH) == dateToRefresh.get(Calendar.MONTH) && |
| target.get(Calendar.YEAR) == dateToRefresh.get(Calendar.YEAR)) |
| { |
| refresh(date, offset, invalidatedCalendarables); |
| break; |
| } |
| } |
| return invalidatedCalendarables; |
| } |
| |
| /** |
| * Return the events for a particular day offset. |
| * |
| * @param dayOffset |
| * @return A List of events. |
| */ |
| public List getCalendarableItems(int dayOffset) { |
| return dayColumns[dayOffset]; |
| } |
| |
| /** |
| * Method computeNumberOfAllDayEventRows. |
| * |
| * @return int representing the max number of events in all visible days. |
| */ |
| public int computeNumberOfAllDayEventRows() { |
| int maxAllDayEvents = 0; |
| for (int day = 0; day < dayColumns.length; day++) { |
| ArrayList calendarables = dayColumns[day]; |
| int allDayEventsInCurrentDay = 0; |
| for (Iterator iter = calendarables.iterator(); iter.hasNext();) { |
| CalendarableItem event = (CalendarableItem) iter.next(); |
| if (event.isAllDayEvent()) { |
| allDayEventsInCurrentDay++; |
| } |
| } |
| if (allDayEventsInCurrentDay > maxAllDayEvents) { |
| maxAllDayEvents = allDayEventsInCurrentDay; |
| } |
| } |
| return maxAllDayEvents; |
| } |
| |
| /** |
| * Method computeStartHour. Computes the start hour of the day for all |
| * days that are displayed. If no events are before the defaultStartHour, |
| * the defaultStartHour is returned. If any day in the model has an event |
| * beginning before defaultStartHour, the hour of the earliest event is |
| * used instead. |
| * |
| * @return int The start hour. |
| */ |
| public int computeStartHour() { |
| GregorianCalendar gc = new GregorianCalendar(); |
| |
| int startHour = getDefaultStartHour(); |
| for (int day = 0; day < dayColumns.length; day++) { |
| ArrayList calendarables = dayColumns[day]; |
| for (Iterator iter = calendarables.iterator(); iter.hasNext();) { |
| CalendarableItem event = (CalendarableItem) iter.next(); |
| if (event.isAllDayEvent()) { |
| continue; |
| } |
| gc.setTime(event.getStartTime()); |
| int eventStartHour = gc.get(Calendar.HOUR_OF_DAY); |
| if (eventStartHour < startHour) { |
| startHour = eventStartHour; |
| } |
| } |
| } |
| return startHour; |
| } |
| |
| /** |
| * Method setDefaultStartHour. |
| * |
| * @param defaultStartHour The first hour to be displayed by default. |
| */ |
| public void setDefaultStartHour(int defaultStartHour) { |
| this.defaultStartHour = defaultStartHour; |
| } |
| |
| /** |
| * Method getDefaultStartHour |
| * |
| * @return int representing the first hour to be displayed by default. |
| */ |
| public int getDefaultStartHour() { |
| return defaultStartHour; |
| } |
| |
| |
| /** |
| * Method getDay. Returns the day on which the specified Calendarable appers. |
| * |
| * @param calendarable The calendarable to find |
| * @return The day offset (0-based) |
| * @throws IllegalArgumentException if Calendarable isn't found |
| */ |
| public int getDay(CalendarableItem calendarable) { |
| for (int day = 0; day < dayColumns.length; day++) { |
| for (Iterator calendarableIter = dayColumns[day].iterator(); calendarableIter.hasNext();) { |
| CalendarableItem event = (CalendarableItem) calendarableIter.next(); |
| if (event == calendarable) { |
| return day; |
| } |
| } |
| } |
| throw new IllegalArgumentException("Invalid Calenderable passed"); |
| } |
| |
| /** |
| * FIXME: Test me please |
| * @param row The row starting from the beginning of the day |
| * @return The hour portion of the time that this row represents |
| */ |
| public int computeHourFromRow(int row) { |
| return row / getNumberOfDivisionsInHour() + computeStartHour(); |
| } |
| |
| /** |
| * FIXME: Test me please |
| * @param row The row starting from the beginning of the day |
| * @return The minute portion of the time that this row represents |
| */ |
| public int computeMinuteFromRow(int row) { |
| int numberOfDivisionsInHour = getNumberOfDivisionsInHour(); |
| int minute = (int) ((double) row |
| % numberOfDivisionsInHour |
| / numberOfDivisionsInHour * 60); |
| return minute; |
| } |
| |
| /** |
| * @param day The day to return all day Calendarables for |
| * @return All the all day Calendarables for the specified day, order maintained |
| */ |
| public CalendarableItem[] getAllDayCalendarables(int day) { |
| List allDays = new LinkedList(); |
| for (Iterator calendarablesIter = getCalendarableItems(day).iterator(); calendarablesIter.hasNext();) { |
| CalendarableItem candidate = (CalendarableItem) calendarablesIter.next(); |
| if (candidate.isAllDayEvent()) { |
| allDays.add(candidate); |
| } |
| } |
| return (CalendarableItem[]) allDays.toArray(new CalendarableItem[allDays.size()]); |
| } |
| |
| /** |
| * @param day The day to search |
| * @param forward true if we're going forward; false if we're searching backward |
| * @param selection The currently selected Calendarable or null if none |
| * @return The next Calendarable in the specified direction where result != selection; null if none |
| */ |
| public CalendarableItem findAllDayCalendarable(int day, boolean forward, CalendarableItem selection) { |
| CalendarableItem[] calendarables = getAllDayCalendarables(day); |
| if (forward) { |
| if (calendarables.length < 1) { |
| return null; |
| } |
| if (selection == null) { |
| return null; |
| } else if (selection == calendarables[calendarables.length-1]) { |
| return null; |
| } |
| for (int selected = 0; selected < calendarables.length; selected++) { |
| if (calendarables[selected] == selection) { |
| return calendarables[selected+1]; |
| } |
| } |
| } else { |
| if (calendarables.length < 1) { |
| return null; |
| } |
| if (selection == null) { |
| return calendarables[calendarables.length-1]; |
| } else if (selection == calendarables[0]) { |
| return null; |
| } |
| for (int selected = 0; selected < calendarables.length; selected++) { |
| if (calendarables[selected] == selection) { |
| return calendarables[selected-1]; |
| } |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * @param day The day to search |
| * @param currentRow The first row to search |
| * @param stopPosition The row to stop searching on or -1 to search to the first/last element |
| * @param forward true if we're going forward; false if we're searching backward |
| * @param selection The Calendarable associated with currentRow or null if none |
| * @return The next Calendarable in the specified direction where result != selection; null if none |
| */ |
| public CalendarableItem findTimedCalendarable(int day, int currentRow, int stopPosition, boolean forward, CalendarableItem selection) { |
| CalendarableItem[][] eventLayoutForDay = getEventLayout(day); |
| if (eventLayoutForDay == null) { |
| throw new IllegalArgumentException("Day " + day + " has no event data!!!"); |
| } |
| |
| int startColumn = 0; |
| if (selection != null) { |
| startColumn = findCalendarable(selection, currentRow, eventLayoutForDay); |
| if (startColumn == -1) { |
| throw new IllegalArgumentException("day " + day + ", row " + currentRow + " does not contain the specified Calendarable"); |
| } |
| } |
| |
| int currentColumn = startColumn; |
| if (forward) { |
| if (stopPosition == -1) { |
| stopPosition = eventLayoutForDay[0].length; |
| } |
| for (int row = currentRow; row < stopPosition; row++) { |
| while (true) { |
| CalendarableItem candidate = eventLayoutForDay[currentColumn][row]; |
| if (candidate != null && candidate != selection) { |
| if (selection == null || |
| candidate.getStartTime().after(selection.getStartTime()) || |
| (currentColumn > startColumn && !candidate.getStartTime().before(selection.getStartTime()))) |
| { |
| return candidate; |
| } |
| } |
| ++currentColumn; |
| if (currentColumn >= eventLayoutForDay.length) { |
| currentColumn = 0; |
| break; |
| } |
| } |
| } |
| } else { |
| if (stopPosition == -1) { |
| stopPosition = 0; |
| } |
| for (int row = currentRow; row >= stopPosition; --row) { |
| while (true) { |
| CalendarableItem candidate = eventLayoutForDay[currentColumn][row]; |
| if (candidate != null && candidate != selection && candidate.getUpperLeftPositionInDayRowCoordinates().y == row) { |
| if (selection == null || |
| candidate.getStartTime().before(selection.getStartTime()) || |
| (currentColumn < startColumn && !candidate.getStartTime().after(selection.getStartTime()))) |
| { |
| if (selection == null && currentColumn > 0) { |
| // The candidate could have an earlier start time |
| // than some other column |
| for (int earlierColumn = currentColumn-1; earlierColumn >= 0; --earlierColumn) { |
| CalendarableItem newCandidate = eventLayoutForDay[earlierColumn][row]; |
| if (newCandidate.getStartTime().after(candidate.getStartTime())) { |
| candidate = newCandidate; |
| } |
| } |
| } |
| return candidate; |
| } |
| } |
| --currentColumn; |
| if (currentColumn < 0) { |
| currentColumn = eventLayoutForDay.length-1; |
| break; |
| } |
| } |
| } |
| } |
| return null; |
| } |
| |
| private int findCalendarable(CalendarableItem selection, int currentRow, CalendarableItem[][] eventLayoutForDay) { |
| for (int column = 0; column < eventLayoutForDay.length; column++) { |
| if (eventLayoutForDay[column][currentRow] == selection) { |
| return column; |
| } |
| } |
| return -1; |
| } |
| |
| /** |
| * @param selectedDay |
| * @param selectedRow |
| * @param selection |
| * @param isAllDayEventRow |
| * @return |
| */ |
| public CalendarableItem findNextCalendarable(int selectedDay, int selectedRow, CalendarableItem selection, boolean isAllDayEventRow) { |
| // Search the rest of the selectedDay starting at selectedRow |
| CalendarableItem result = null; |
| if (isAllDayEventRow) { |
| result = findAllDayCalendarable(selectedDay, true, selection); |
| if (result != null) |
| return result; |
| result = findTimedCalendarable(selectedDay, 0, -1, true, null); |
| if (result != null) |
| return result; |
| } else { |
| result = findTimedCalendarable(selectedDay, selectedRow, -1, true, selection); |
| if (result != null) { |
| return result; |
| } |
| } |
| |
| // Search all days other than selectedDay |
| int currentDay = nextDay(selectedDay); |
| while (currentDay != selectedDay) { |
| // Is there an all-day event to select? |
| CalendarableItem[] allDayCalendarables = getAllDayCalendarables(currentDay); |
| if (allDayCalendarables.length > 0) { |
| return allDayCalendarables[0]; |
| } |
| |
| // Nope, search for the first timed event |
| result = findTimedCalendarable(currentDay, 0, -1, true, null); |
| if (result != null) { |
| return result; |
| } |
| |
| currentDay = nextDay(currentDay); |
| } |
| |
| // Search selectedDay from 0 to selectedRow |
| CalendarableItem[] allDayCalendarables = getAllDayCalendarables(selectedDay); |
| if (allDayCalendarables.length > 0) { |
| return allDayCalendarables[0]; |
| } |
| |
| // If we started in an all-day event row, we're done searching. |
| if (isAllDayEventRow) { |
| return null; |
| } |
| |
| // Search the last of the timed-events |
| result = findTimedCalendarable(selectedDay, 0, selectedRow-1, true, null); |
| // result = findTimedCalendarable(selectedDay, 0, selectedRow-1, true, selection); |
| if (result != null) { |
| return result; |
| } |
| |
| // Nothing more to search; give up. |
| return null; |
| } |
| |
| private int nextDay(int selectedDay) { |
| ++selectedDay; |
| if (selectedDay >= numberOfDays) { |
| selectedDay = 0; |
| } |
| return selectedDay; |
| } |
| |
| /** |
| * @param selectedDay |
| * @param selectedRow |
| * @param selection |
| * @param isAllDayEventRow |
| * @return |
| */ |
| public CalendarableItem findPreviousCalendarable(int selectedDay, int selectedRow, CalendarableItem selection, boolean isAllDayEventRow) { |
| CalendarableItem result = null; |
| |
| // Search to the beginning of the current day |
| if (!isAllDayEventRow) { |
| // search timed events to the beginning of the day |
| result = findTimedCalendarable(selectedDay, selectedRow, -1, false, selection); |
| if (result != null) |
| return result; |
| |
| // Search all-day events |
| result = findAllDayCalendarable(selectedDay, false, null); |
| if (result != null) |
| return result; |
| } else { |
| result = findAllDayCalendarable(selectedDay, false, selection); |
| if (result != null) |
| return result; |
| } |
| |
| // Search all days other than selectedDay |
| int currentDay = previousDay(selectedDay); |
| while (currentDay != selectedDay) { |
| // Nope, search for the first timed event |
| result = findTimedCalendarable(currentDay, |
| getEventLayout(selectedDay)[0].length-1, |
| -1, false, null); |
| if (result != null) { |
| return result; |
| } |
| |
| // Is there an all-day event to select? |
| CalendarableItem[] allDayCalendarables = getAllDayCalendarables(currentDay); |
| if (allDayCalendarables.length > 0) { |
| return allDayCalendarables[allDayCalendarables.length-1]; |
| } |
| |
| currentDay = previousDay(currentDay); |
| } |
| |
| // Search from the end of the current day to the current time |
| result = findTimedCalendarable(currentDay, |
| getEventLayout(selectedDay)[0].length-1, |
| selectedRow+1, false, null); |
| if (result != null) { |
| return result; |
| } |
| |
| return null; |
| } |
| |
| private int previousDay(int selectedDay) { |
| --selectedDay; |
| if (selectedDay < 0) { |
| selectedDay = numberOfDays-1; |
| } |
| return selectedDay; |
| } |
| } |
| |
| |