summaryrefslogtreecommitdiffstats
path: root/compatibility-client/src
diff options
context:
space:
mode:
authorArtur Signell <artur@vaadin.com>2016-08-18 22:10:47 +0300
committerArtur Signell <artur@vaadin.com>2016-08-20 00:12:18 +0300
commitfe3dca081a64af892a7f4c0416ecc643aec3ec5a (patch)
tree1901fb377336d3c5a772335322d9c434a4a75e24 /compatibility-client/src
parent65370e12a0605926cb80e205c2b0e74fefe83e5b (diff)
downloadvaadin-framework-fe3dca081a64af892a7f4c0416ecc643aec3ec5a.tar.gz
vaadin-framework-fe3dca081a64af892a7f4c0416ecc643aec3ec5a.zip
Move remaining selects and container implementations to compatibility package
Because of dependencies also moves Calendar, ColorPicker, SQLContainer, container filters Change-Id: I0594cb24f20486ebbca4be578827fea7cdf92108
Diffstat (limited to 'compatibility-client/src')
-rw-r--r--compatibility-client/src/main/java/com/vaadin/client/ui/VCalendar.java1504
-rw-r--r--compatibility-client/src/main/java/com/vaadin/client/ui/VFilterSelect.java2772
-rw-r--r--compatibility-client/src/main/java/com/vaadin/client/ui/VTree.java2267
-rw-r--r--compatibility-client/src/main/java/com/vaadin/client/ui/calendar/CalendarConnector.java713
-rw-r--r--compatibility-client/src/main/java/com/vaadin/client/ui/calendar/VCalendarAction.java138
-rw-r--r--compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/CalendarDay.java61
-rw-r--r--compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/CalendarEvent.java320
-rw-r--r--compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/DateCell.java850
-rw-r--r--compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/DateCellContainer.java117
-rw-r--r--compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/DateCellDayEvent.java664
-rw-r--r--compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/DateCellGroup.java59
-rw-r--r--compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/DateUtil.java70
-rw-r--r--compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/DayToolbar.java179
-rw-r--r--compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/FocusableComplexPanel.java122
-rw-r--r--compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/FocusableGrid.java134
-rw-r--r--compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/FocusableHTML.java124
-rw-r--r--compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/HasTooltipKey.java33
-rw-r--r--compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/MonthEventLabel.java173
-rw-r--r--compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/MonthGrid.java215
-rw-r--r--compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/SimpleDayCell.java736
-rw-r--r--compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/SimpleDayToolbar.java97
-rw-r--r--compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/SimpleWeekToolbar.java109
-rw-r--r--compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/WeekGrid.java692
-rw-r--r--compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/WeekGridMinuteTimeRange.java62
-rw-r--r--compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/WeekLabel.java51
-rw-r--r--compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/WeeklyLongEvents.java189
-rw-r--r--compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/WeeklyLongEventsDateCell.java67
-rw-r--r--compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/dd/CalendarDropHandler.java65
-rw-r--r--compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/dd/CalendarMonthDropHandler.java171
-rw-r--r--compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/dd/CalendarWeekDropHandler.java187
-rw-r--r--compatibility-client/src/main/java/com/vaadin/client/ui/colorpicker/AbstractColorPickerConnector.java113
-rw-r--r--compatibility-client/src/main/java/com/vaadin/client/ui/colorpicker/ColorPickerAreaConnector.java67
-rw-r--r--compatibility-client/src/main/java/com/vaadin/client/ui/colorpicker/ColorPickerConnector.java69
-rw-r--r--compatibility-client/src/main/java/com/vaadin/client/ui/colorpicker/ColorPickerGradientConnector.java85
-rw-r--r--compatibility-client/src/main/java/com/vaadin/client/ui/colorpicker/ColorPickerGridConnector.java94
-rw-r--r--compatibility-client/src/main/java/com/vaadin/client/ui/colorpicker/VColorPickerGradient.java211
-rw-r--r--compatibility-client/src/main/java/com/vaadin/client/ui/colorpicker/VColorPickerGrid.java147
-rw-r--r--compatibility-client/src/main/java/com/vaadin/client/ui/combobox/ComboBoxConnector.java378
-rw-r--r--compatibility-client/src/main/java/com/vaadin/client/ui/dd/VIsOverId.java54
-rw-r--r--compatibility-client/src/main/java/com/vaadin/client/ui/dd/VItemIdIs.java50
-rw-r--r--compatibility-client/src/main/java/com/vaadin/client/ui/tree/TreeConnector.java392
-rw-r--r--compatibility-client/src/main/java/com/vaadin/client/ui/tree/VTargetInSubtree.java59
-rw-r--r--compatibility-client/src/main/java/com/vaadin/client/ui/tree/VTreeLazyInitItemIdentifiers.java26
43 files changed, 14686 insertions, 0 deletions
diff --git a/compatibility-client/src/main/java/com/vaadin/client/ui/VCalendar.java b/compatibility-client/src/main/java/com/vaadin/client/ui/VCalendar.java
new file mode 100644
index 0000000000..8414d7c99c
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/client/ui/VCalendar.java
@@ -0,0 +1,1504 @@
+/*
+ * Copyright 2000-2016 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.client.ui;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.Date;
+import java.util.List;
+
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.event.dom.client.ContextMenuEvent;
+import com.google.gwt.event.dom.client.ContextMenuHandler;
+import com.google.gwt.i18n.client.DateTimeFormat;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.DockPanel;
+import com.google.gwt.user.client.ui.ScrollPanel;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.client.ui.calendar.schedule.CalendarDay;
+import com.vaadin.client.ui.calendar.schedule.CalendarEvent;
+import com.vaadin.client.ui.calendar.schedule.DayToolbar;
+import com.vaadin.client.ui.calendar.schedule.MonthGrid;
+import com.vaadin.client.ui.calendar.schedule.SimpleDayCell;
+import com.vaadin.client.ui.calendar.schedule.SimpleDayToolbar;
+import com.vaadin.client.ui.calendar.schedule.SimpleWeekToolbar;
+import com.vaadin.client.ui.calendar.schedule.WeekGrid;
+import com.vaadin.client.ui.calendar.schedule.WeeklyLongEvents;
+import com.vaadin.client.ui.calendar.schedule.dd.CalendarDropHandler;
+import com.vaadin.client.ui.dd.VHasDropHandler;
+import com.vaadin.shared.ui.calendar.DateConstants;
+
+/**
+ * Client side implementation for Calendar
+ *
+ * @since 7.1
+ * @author Vaadin Ltd.
+ */
+public class VCalendar extends Composite implements VHasDropHandler {
+
+ public static final String ATTR_FIRSTDAYOFWEEK = "firstDay";
+ public static final String ATTR_LASTDAYOFWEEK = "lastDay";
+ public static final String ATTR_FIRSTHOUROFDAY = "firstHour";
+ public static final String ATTR_LASTHOUROFDAY = "lastHour";
+
+ // private boolean hideWeekends;
+ private String[] monthNames;
+ private String[] dayNames;
+ private boolean format;
+ private final DockPanel outer = new DockPanel();
+ private int rows;
+
+ private boolean rangeSelectAllowed = true;
+ private boolean rangeMoveAllowed = true;
+ private boolean eventResizeAllowed = true;
+ private boolean eventMoveAllowed = true;
+
+ private final SimpleDayToolbar nameToolbar = new SimpleDayToolbar();
+
+ private final DayToolbar dayToolbar = new DayToolbar(this);
+ private final SimpleWeekToolbar weekToolbar;
+ private WeeklyLongEvents weeklyLongEvents;
+ private MonthGrid monthGrid;
+ private WeekGrid weekGrid;
+ private int intWidth = 0;
+ private int intHeight = 0;
+
+ protected final DateTimeFormat dateformat_datetime = DateTimeFormat
+ .getFormat("yyyy-MM-dd HH:mm:ss");
+ protected final DateTimeFormat dateformat_date = DateTimeFormat
+ .getFormat("yyyy-MM-dd");
+ protected final DateTimeFormat time12format_date = DateTimeFormat
+ .getFormat("h:mm a");
+ protected final DateTimeFormat time24format_date = DateTimeFormat
+ .getFormat("HH:mm");
+
+ private boolean readOnly = false;
+ private boolean disabled = false;
+
+ private boolean isHeightUndefined = false;
+
+ private boolean isWidthUndefined = false;
+ private int firstDay;
+ private int lastDay;
+ private int firstHour;
+ private int lastHour;
+
+ private CalendarDropHandler dropHandler;
+
+ /**
+ * Listener interface for listening to event click events
+ */
+ public interface DateClickListener {
+ /**
+ * Triggered when a date was clicked
+ *
+ * @param date
+ * The date and time that was clicked
+ */
+ void dateClick(String date);
+ }
+
+ /**
+ * Listener interface for listening to week number click events
+ */
+ public interface WeekClickListener {
+ /**
+ * Called when a week number was selected.
+ *
+ * @param event
+ * The format of the vent string is "<year>w<week>"
+ */
+ void weekClick(String event);
+ }
+
+ /**
+ * Listener interface for listening to forward events
+ */
+ public interface ForwardListener {
+
+ /**
+ * Called when the calendar should move one view forward
+ */
+ void forward();
+ }
+
+ /**
+ * Listener interface for listening to backward events
+ */
+ public interface BackwardListener {
+
+ /**
+ * Called when the calendar should move one view backward
+ */
+ void backward();
+ }
+
+ /**
+ * Listener interface for listening to selection events
+ */
+ public interface RangeSelectListener {
+
+ /**
+ * Called when a user selected a new event by highlighting an area of
+ * the calendar.
+ *
+ * FIXME Fix the value nonsense.
+ *
+ * @param value
+ * The format of the value string is
+ * "<year>:<start-minutes>:<end-minutes>" if called from the
+ * {@link SimpleWeekToolbar} and "<yyyy-MM-dd>TO<yyyy-MM-dd>"
+ * if called from {@link MonthGrid}
+ */
+ void rangeSelected(String value);
+ }
+
+ /**
+ * Listener interface for listening to click events
+ */
+ public interface EventClickListener {
+ /**
+ * Called when an event was clicked
+ *
+ * @param event
+ * The event that was clicked
+ */
+ void eventClick(CalendarEvent event);
+ }
+
+ /**
+ * Listener interface for listening to event moved events. Occurs when a
+ * user drags an event to a new position
+ */
+ public interface EventMovedListener {
+ /**
+ * Triggered when an event was dragged to a new position and the start
+ * and end dates was changed
+ *
+ * @param event
+ * The event that was moved
+ */
+ void eventMoved(CalendarEvent event);
+ }
+
+ /**
+ * Listener interface for when an event gets resized (its start or end date
+ * changes)
+ */
+ public interface EventResizeListener {
+ /**
+ * Triggers when the time limits for the event was changed.
+ *
+ * @param event
+ * The event that was changed. The new time limits have been
+ * updated in the event before calling this method
+ */
+ void eventResized(CalendarEvent event);
+ }
+
+ /**
+ * Listener interface for listening to scroll events.
+ */
+ public interface ScrollListener {
+ /**
+ * Triggered when the calendar is scrolled
+ *
+ * @param scrollPosition
+ * The scroll position in pixels as returned by
+ * {@link ScrollPanel#getScrollPosition()}
+ */
+ void scroll(int scrollPosition);
+ }
+
+ /**
+ * Listener interface for listening to mouse events.
+ */
+ public interface MouseEventListener {
+ /**
+ * Triggered when a user wants an context menu
+ *
+ * @param event
+ * The context menu event
+ *
+ * @param widget
+ * The widget that the context menu should be added to
+ */
+ void contextMenu(ContextMenuEvent event, Widget widget);
+ }
+
+ /**
+ * Default constructor
+ */
+ public VCalendar() {
+ weekToolbar = new SimpleWeekToolbar(this);
+ initWidget(outer);
+ setStylePrimaryName("v-calendar");
+ blockSelect(getElement());
+ }
+
+ /**
+ * Hack for IE to not select text when dragging.
+ *
+ * @param e
+ * The element to apply the hack on
+ */
+ private native void blockSelect(Element e)
+ /*-{
+ e.onselectstart = function() {
+ return false;
+ }
+
+ e.ondragstart = function() {
+ return false;
+ }
+ }-*/;
+
+ private void updateEventsToWeekGrid(CalendarEvent[] events) {
+ List<CalendarEvent> allDayLong = new ArrayList<CalendarEvent>();
+ List<CalendarEvent> belowDayLong = new ArrayList<CalendarEvent>();
+
+ for (CalendarEvent e : events) {
+ if (e.isAllDay()) {
+ // Event is set on one "allDay" event or more than one.
+ allDayLong.add(e);
+
+ } else {
+ // Event is set only on one day.
+ belowDayLong.add(e);
+ }
+ }
+
+ weeklyLongEvents.addEvents(allDayLong);
+
+ for (CalendarEvent e : belowDayLong) {
+ weekGrid.addEvent(e);
+ }
+ }
+
+ /**
+ * Adds events to the month grid
+ *
+ * @param events
+ * The events to add
+ * @param drawImmediately
+ * Should the grid be rendered immediately. (currently not in
+ * use)
+ *
+ */
+ public void updateEventsToMonthGrid(Collection<CalendarEvent> events,
+ boolean drawImmediately) {
+ for (CalendarEvent e : sortEventsByDuration(events)) {
+ // FIXME Why is drawImmediately not used ?????
+ addEventToMonthGrid(e, false);
+ }
+ }
+
+ private void addEventToMonthGrid(CalendarEvent e,
+ boolean renderImmediately) {
+ Date when = e.getStart();
+ Date to = e.getEnd();
+ boolean eventAdded = false;
+ boolean inProgress = false; // Event adding has started
+ boolean eventMoving = false;
+ List<SimpleDayCell> dayCells = new ArrayList<SimpleDayCell>();
+ List<SimpleDayCell> timeCells = new ArrayList<SimpleDayCell>();
+ for (int row = 0; row < monthGrid.getRowCount(); row++) {
+ if (eventAdded) {
+ break;
+ }
+ for (int cell = 0; cell < monthGrid.getCellCount(row); cell++) {
+ SimpleDayCell sdc = (SimpleDayCell) monthGrid.getWidget(row,
+ cell);
+ if (isEventInDay(when, to, sdc.getDate())
+ && isEventInDayWithTime(when, to, sdc.getDate(),
+ e.getEndTime(), e.isAllDay())) {
+ if (!eventMoving) {
+ eventMoving = sdc.getMoveEvent() != null;
+ }
+ long d = e.getRangeInMilliseconds();
+ if ((d > 0 && d <= DateConstants.DAYINMILLIS)
+ && !e.isAllDay()) {
+ timeCells.add(sdc);
+ } else {
+ dayCells.add(sdc);
+ }
+ inProgress = true;
+ continue;
+ } else if (inProgress) {
+ eventAdded = true;
+ inProgress = false;
+ break;
+ }
+ }
+ }
+
+ updateEventSlotIndex(e, dayCells);
+ updateEventSlotIndex(e, timeCells);
+
+ for (SimpleDayCell sdc : dayCells) {
+ sdc.addCalendarEvent(e);
+ }
+ for (SimpleDayCell sdc : timeCells) {
+ sdc.addCalendarEvent(e);
+ }
+
+ if (renderImmediately) {
+ reDrawAllMonthEvents(!eventMoving);
+ }
+ }
+
+ /*
+ * We must also handle the special case when the event lasts exactly for 24
+ * hours, thus spanning two days e.g. from 1.1.2001 00:00 to 2.1.2001 00:00.
+ * That special case still should span one day when rendered.
+ */
+ @SuppressWarnings("deprecation")
+ // Date methods are not deprecated in GWT
+ private boolean isEventInDayWithTime(Date from, Date to, Date date,
+ Date endTime, boolean isAllDay) {
+ return (isAllDay || !(to.getDay() == date.getDay()
+ && from.getDay() != to.getDay() && isMidnight(endTime)));
+ }
+
+ private void updateEventSlotIndex(CalendarEvent e,
+ List<SimpleDayCell> cells) {
+ if (cells.isEmpty()) {
+ return;
+ }
+
+ if (e.getSlotIndex() == -1) {
+ // Update slot index
+ int newSlot = -1;
+ for (SimpleDayCell sdc : cells) {
+ int slot = sdc.getEventCount();
+ if (slot > newSlot) {
+ newSlot = slot;
+ }
+ }
+ newSlot++;
+
+ for (int i = 0; i < newSlot; i++) {
+ // check for empty slot
+ if (isSlotEmpty(e, i, cells)) {
+ newSlot = i;
+ break;
+ }
+ }
+ e.setSlotIndex(newSlot);
+ }
+ }
+
+ private void reDrawAllMonthEvents(boolean clearCells) {
+ for (int row = 0; row < monthGrid.getRowCount(); row++) {
+ for (int cell = 0; cell < monthGrid.getCellCount(row); cell++) {
+ SimpleDayCell sdc = (SimpleDayCell) monthGrid.getWidget(row,
+ cell);
+ sdc.reDraw(clearCells);
+ }
+ }
+ }
+
+ private boolean isSlotEmpty(CalendarEvent addedEvent, int slotIndex,
+ List<SimpleDayCell> cells) {
+ for (SimpleDayCell sdc : cells) {
+ CalendarEvent e = sdc.getCalendarEvent(slotIndex);
+ if (e != null && !e.equals(addedEvent)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Remove a month event from the view
+ *
+ * @param target
+ * The event to remove
+ *
+ * @param repaintImmediately
+ * Should we repaint after the event was removed?
+ */
+ public void removeMonthEvent(CalendarEvent target,
+ boolean repaintImmediately) {
+ if (target != null && target.getSlotIndex() >= 0) {
+ // Remove event
+ for (int row = 0; row < monthGrid.getRowCount(); row++) {
+ for (int cell = 0; cell < monthGrid.getCellCount(row); cell++) {
+ SimpleDayCell sdc = (SimpleDayCell) monthGrid.getWidget(row,
+ cell);
+ if (sdc == null) {
+ return;
+ }
+ sdc.removeEvent(target, repaintImmediately);
+ }
+ }
+ }
+ }
+
+ /**
+ * Updates an event in the month grid
+ *
+ * @param changedEvent
+ * The event that has changed
+ */
+ public void updateEventToMonthGrid(CalendarEvent changedEvent) {
+ removeMonthEvent(changedEvent, true);
+ changedEvent.setSlotIndex(-1);
+ addEventToMonthGrid(changedEvent, true);
+ }
+
+ /**
+ * Sort the event by how long they are
+ *
+ * @param events
+ * The events to sort
+ * @return An array where the events has been sorted
+ */
+ public CalendarEvent[] sortEventsByDuration(
+ Collection<CalendarEvent> events) {
+ CalendarEvent[] sorted = events
+ .toArray(new CalendarEvent[events.size()]);
+ Arrays.sort(sorted, getEventComparator());
+ return sorted;
+ }
+
+ /*
+ * Check if the given event occurs at the given date.
+ */
+ private boolean isEventInDay(Date eventWhen, Date eventTo, Date gridDate) {
+ if (eventWhen.compareTo(gridDate) <= 0
+ && eventTo.compareTo(gridDate) >= 0) {
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Re-render the week grid
+ *
+ * @param daysCount
+ * The amount of days to include in the week
+ * @param days
+ * The days
+ * @param today
+ * Todays date
+ * @param realDayNames
+ * The names of the dates
+ */
+ @SuppressWarnings("deprecation")
+ public void updateWeekGrid(int daysCount, List<CalendarDay> days,
+ Date today, String[] realDayNames) {
+ weekGrid.setFirstHour(getFirstHourOfTheDay());
+ weekGrid.setLastHour(getLastHourOfTheDay());
+ weekGrid.getTimeBar().updateTimeBar(is24HFormat());
+
+ dayToolbar.clear();
+ dayToolbar.addBackButton();
+ dayToolbar.setVerticalSized(isHeightUndefined);
+ dayToolbar.setHorizontalSized(isWidthUndefined);
+ weekGrid.clearDates();
+ weekGrid.setDisabled(isDisabledOrReadOnly());
+
+ for (CalendarDay day : days) {
+ String date = day.getDate();
+ String localized_date_format = day.getLocalizedDateFormat();
+ Date d = dateformat_date.parse(date);
+ int dayOfWeek = day.getDayOfWeek();
+ if (dayOfWeek < getFirstDayNumber()
+ || dayOfWeek > getLastDayNumber()) {
+ continue;
+ }
+ boolean isToday = false;
+ int dayOfMonth = d.getDate();
+ if (today.getDate() == dayOfMonth && today.getYear() == d.getYear()
+ && today.getMonth() == d.getMonth()) {
+ isToday = true;
+ }
+ dayToolbar.add(realDayNames[dayOfWeek - 1], date,
+ localized_date_format, isToday ? "today" : null);
+ weeklyLongEvents.addDate(d);
+ weekGrid.addDate(d);
+ if (isToday) {
+ weekGrid.setToday(d, today);
+ }
+ }
+ dayToolbar.addNextButton();
+ }
+
+ /**
+ * Updates the events in the Month view
+ *
+ * @param daysCount
+ * How many days there are
+ * @param daysUidl
+ *
+ * @param today
+ * Todays date
+ */
+ @SuppressWarnings("deprecation")
+ public void updateMonthGrid(int daysCount, List<CalendarDay> days,
+ Date today) {
+ int columns = getLastDayNumber() - getFirstDayNumber() + 1;
+ rows = (int) Math.ceil(daysCount / (double) 7);
+
+ monthGrid = new MonthGrid(this, rows, columns);
+ monthGrid.setEnabled(!isDisabledOrReadOnly());
+ weekToolbar.removeAllRows();
+ int pos = 0;
+ boolean monthNameDrawn = true;
+ boolean firstDayFound = false;
+ boolean lastDayFound = false;
+
+ for (CalendarDay day : days) {
+ String date = day.getDate();
+ Date d = dateformat_date.parse(date);
+ int dayOfWeek = day.getDayOfWeek();
+ int week = day.getWeek();
+
+ int dayOfMonth = d.getDate();
+
+ // reset at start of each month
+ if (dayOfMonth == 1) {
+ monthNameDrawn = false;
+ if (firstDayFound) {
+ lastDayFound = true;
+ }
+ firstDayFound = true;
+ }
+
+ if (dayOfWeek < getFirstDayNumber()
+ || dayOfWeek > getLastDayNumber()) {
+ continue;
+ }
+ int y = (pos / columns);
+ int x = pos - (y * columns);
+ if (x == 0 && daysCount > 7) {
+ // Add week to weekToolbar for navigation
+ weekToolbar.addWeek(week, day.getYearOfWeek());
+ }
+ final SimpleDayCell cell = new SimpleDayCell(this, y, x);
+ cell.setMonthGrid(monthGrid);
+ cell.setDate(d);
+ cell.addDomHandler(new ContextMenuHandler() {
+ @Override
+ public void onContextMenu(ContextMenuEvent event) {
+ if (mouseEventListener != null) {
+ event.preventDefault();
+ event.stopPropagation();
+ mouseEventListener.contextMenu(event, cell);
+ }
+ }
+ }, ContextMenuEvent.getType());
+
+ if (!firstDayFound) {
+ cell.addStyleDependentName("prev-month");
+ } else if (lastDayFound) {
+ cell.addStyleDependentName("next-month");
+ }
+
+ if (dayOfMonth >= 1 && !monthNameDrawn) {
+ cell.setMonthNameVisible(true);
+ monthNameDrawn = true;
+ }
+
+ if (today.getDate() == dayOfMonth && today.getYear() == d.getYear()
+ && today.getMonth() == d.getMonth()) {
+ cell.setToday(true);
+
+ }
+ monthGrid.setWidget(y, x, cell);
+ pos++;
+ }
+ }
+
+ public void setSizeForChildren(int newWidth, int newHeight) {
+ intWidth = newWidth;
+ intHeight = newHeight;
+ isWidthUndefined = intWidth == -1;
+ dayToolbar.setVerticalSized(isHeightUndefined);
+ dayToolbar.setHorizontalSized(isWidthUndefined);
+ recalculateWidths();
+ recalculateHeights();
+ }
+
+ /**
+ * Recalculates the heights of the sub-components in the calendar
+ */
+ protected void recalculateHeights() {
+ if (monthGrid != null) {
+
+ if (intHeight == -1) {
+ monthGrid.addStyleDependentName("sizedheight");
+ } else {
+ monthGrid.removeStyleDependentName("sizedheight");
+ }
+
+ monthGrid.updateCellSizes(intWidth - weekToolbar.getOffsetWidth(),
+ intHeight - nameToolbar.getOffsetHeight());
+ weekToolbar.setHeightPX((intHeight == -1) ? intHeight
+ : intHeight - nameToolbar.getOffsetHeight());
+
+ } else if (weekGrid != null) {
+ weekGrid.setHeightPX((intHeight == -1) ? intHeight
+ : intHeight - weeklyLongEvents.getOffsetHeight()
+ - dayToolbar.getOffsetHeight());
+ }
+ }
+
+ /**
+ * Recalculates the widths of the sub-components in the calendar
+ */
+ protected void recalculateWidths() {
+ if (!isWidthUndefined) {
+ nameToolbar.setWidthPX(intWidth);
+ dayToolbar.setWidthPX(intWidth);
+
+ if (monthGrid != null) {
+ monthGrid.updateCellSizes(
+ intWidth - weekToolbar.getOffsetWidth(),
+ intHeight - nameToolbar.getOffsetHeight());
+ } else if (weekGrid != null) {
+ weekGrid.setWidthPX(intWidth);
+ weeklyLongEvents.setWidthPX(weekGrid.getInternalWidth());
+ }
+ } else {
+ dayToolbar.setWidthPX(intWidth);
+ nameToolbar.setWidthPX(intWidth);
+
+ if (monthGrid != null) {
+ if (intWidth == -1) {
+ monthGrid.addStyleDependentName("sizedwidth");
+
+ } else {
+ monthGrid.removeStyleDependentName("sizedwidth");
+ }
+ } else if (weekGrid != null) {
+ weekGrid.setWidthPX(intWidth);
+ weeklyLongEvents.setWidthPX(weekGrid.getInternalWidth());
+ }
+ }
+ }
+
+ /**
+ * Get the date format used to format dates only (excludes time)
+ *
+ * @return
+ */
+ public DateTimeFormat getDateFormat() {
+ return dateformat_date;
+ }
+
+ /**
+ * Get the time format used to format time only (excludes date)
+ *
+ * @return
+ */
+ public DateTimeFormat getTimeFormat() {
+ if (is24HFormat()) {
+ return time24format_date;
+ }
+ return time12format_date;
+ }
+
+ /**
+ * Get the date and time format to format the dates (includes both date and
+ * time)
+ *
+ * @return
+ */
+ public DateTimeFormat getDateTimeFormat() {
+ return dateformat_datetime;
+ }
+
+ /**
+ * Is the calendar either disabled or readonly
+ *
+ * @return
+ */
+ public boolean isDisabledOrReadOnly() {
+ return disabled || readOnly;
+ }
+
+ /**
+ * Is the component disabled
+ */
+ public boolean isDisabled() {
+ return disabled;
+ }
+
+ /**
+ * Is the component disabled
+ *
+ * @param disabled
+ * True if disabled
+ */
+ public void setDisabled(boolean disabled) {
+ this.disabled = disabled;
+ }
+
+ /**
+ * Is the component read-only
+ */
+ public boolean isReadOnly() {
+ return readOnly;
+ }
+
+ /**
+ * Is the component read-only
+ *
+ * @param readOnly
+ * True if component is readonly
+ */
+ public void setReadOnly(boolean readOnly) {
+ this.readOnly = readOnly;
+ }
+
+ /**
+ * Get the month grid component
+ *
+ * @return
+ */
+ public MonthGrid getMonthGrid() {
+ return monthGrid;
+ }
+
+ /**
+ * Get he week grid component
+ *
+ * @return
+ */
+ public WeekGrid getWeekGrid() {
+ return weekGrid;
+ }
+
+ /**
+ * Calculates correct size for all cells (size / amount of cells ) and
+ * distributes any overflow over all the cells.
+ *
+ * @param totalSize
+ * the total amount of size reserved for all cells
+ * @param numberOfCells
+ * the number of cells
+ * @param sizeModifier
+ * a modifier which is applied to all cells before distributing
+ * the overflow
+ * @return an integer array that contains the correct size for each cell
+ */
+ public static int[] distributeSize(int totalSize, int numberOfCells,
+ int sizeModifier) {
+ int[] cellSizes = new int[numberOfCells];
+ int startingSize = totalSize / numberOfCells;
+ int cellSizeOverFlow = totalSize % numberOfCells;
+
+ for (int i = 0; i < numberOfCells; i++) {
+ cellSizes[i] = startingSize + sizeModifier;
+ }
+
+ // distribute size overflow amongst all slots
+ int j = 0;
+ while (cellSizeOverFlow > 0) {
+ cellSizes[j]++;
+ cellSizeOverFlow--;
+ j++;
+ if (j >= numberOfCells) {
+ j = 0;
+ }
+ }
+
+ // cellSizes[numberOfCells - 1] += cellSizeOverFlow;
+
+ return cellSizes;
+ }
+
+ /**
+ * Returns a comparator which can compare calendar events.
+ *
+ * @return
+ */
+ public static Comparator<CalendarEvent> getEventComparator() {
+ return new Comparator<CalendarEvent>() {
+
+ @Override
+ public int compare(CalendarEvent o1, CalendarEvent o2) {
+ if (o1.isAllDay() != o2.isAllDay()) {
+ if (o2.isAllDay()) {
+ return 1;
+ }
+ return -1;
+ }
+
+ Long d1 = o1.getRangeInMilliseconds();
+ Long d2 = o2.getRangeInMilliseconds();
+ int r = 0;
+ if (!d1.equals(0L) && !d2.equals(0L)) {
+ r = d2.compareTo(d1);
+ return (r == 0)
+ ? ((Integer) o2.getIndex()).compareTo(o1.getIndex())
+ : r;
+ }
+
+ if (d2.equals(0L) && d1.equals(0L)) {
+ return ((Integer) o2.getIndex()).compareTo(o1.getIndex());
+ } else if (d2.equals(0L) && d1 >= DateConstants.DAYINMILLIS) {
+ return -1;
+ } else if (d2.equals(0L) && d1 < DateConstants.DAYINMILLIS) {
+ return 1;
+ } else if (d1.equals(0L) && d2 >= DateConstants.DAYINMILLIS) {
+ return 1;
+ } else if (d1.equals(0L) && d2 < DateConstants.DAYINMILLIS) {
+ return -1;
+ }
+ r = d2.compareTo(d1);
+ return (r == 0)
+ ? ((Integer) o2.getIndex()).compareTo(o1.getIndex())
+ : r;
+ }
+ };
+ }
+
+ /**
+ * Is the date at midnight
+ *
+ * @param date
+ * The date to check
+ *
+ * @return
+ */
+ @SuppressWarnings("deprecation")
+ public static boolean isMidnight(Date date) {
+ return (date.getHours() == 0 && date.getMinutes() == 0
+ && date.getSeconds() == 0);
+ }
+
+ /**
+ * Are the dates equal (uses second resolution)
+ *
+ * @param date1
+ * The first the to compare
+ * @param date2
+ * The second date to compare
+ * @return
+ */
+ @SuppressWarnings("deprecation")
+ public static boolean areDatesEqualToSecond(Date date1, Date date2) {
+ return date1.getYear() == date2.getYear()
+ && date1.getMonth() == date2.getMonth()
+ && date1.getDay() == date2.getDay()
+ && date1.getHours() == date2.getHours()
+ && date1.getSeconds() == date2.getSeconds();
+ }
+
+ /**
+ * Is the calendar event zero seconds long and is occurring at midnight
+ *
+ * @param event
+ * The event to check
+ * @return
+ */
+ public static boolean isZeroLengthMidnightEvent(CalendarEvent event) {
+ return areDatesEqualToSecond(event.getStartTime(), event.getEndTime())
+ && isMidnight(event.getEndTime());
+ }
+
+ /**
+ * Should the 24h time format be used
+ *
+ * @param format
+ * True if the 24h format should be used else the 12h format is
+ * used
+ */
+ public void set24HFormat(boolean format) {
+ this.format = format;
+ }
+
+ /**
+ * Is the 24h time format used
+ */
+ public boolean is24HFormat() {
+ return format;
+ }
+
+ /**
+ * Set the names of the week days
+ *
+ * @param names
+ * The names of the days (Monday, Thursday,...)
+ */
+ public void setDayNames(String[] names) {
+ assert (names.length == 7);
+ dayNames = names;
+ }
+
+ /**
+ * Get the names of the week days
+ */
+ public String[] getDayNames() {
+ return dayNames;
+ }
+
+ /**
+ * Set the names of the months
+ *
+ * @param names
+ * The names of the months (January, February,...)
+ */
+ public void setMonthNames(String[] names) {
+ assert (names.length == 12);
+ monthNames = names;
+ }
+
+ /**
+ * Get the month names
+ */
+ public String[] getMonthNames() {
+ return monthNames;
+ }
+
+ /**
+ * Set the number when a week starts
+ *
+ * @param dayNumber
+ * The number of the day
+ */
+ public void setFirstDayNumber(int dayNumber) {
+ assert (dayNumber >= 1 && dayNumber <= 7);
+ firstDay = dayNumber;
+ }
+
+ /**
+ * Get the number when a week starts
+ */
+ public int getFirstDayNumber() {
+ return firstDay;
+ }
+
+ /**
+ * Set the number when a week ends
+ *
+ * @param dayNumber
+ * The number of the day
+ */
+ public void setLastDayNumber(int dayNumber) {
+ assert (dayNumber >= 1 && dayNumber <= 7);
+ lastDay = dayNumber;
+ }
+
+ /**
+ * Get the number when a week ends
+ */
+ public int getLastDayNumber() {
+ return lastDay;
+ }
+
+ /**
+ * Set the number when a week starts
+ *
+ * @param dayNumber
+ * The number of the day
+ */
+ public void setFirstHourOfTheDay(int hour) {
+ assert (hour >= 0 && hour <= 23);
+ firstHour = hour;
+ }
+
+ /**
+ * Get the number when a week starts
+ */
+ public int getFirstHourOfTheDay() {
+ return firstHour;
+ }
+
+ /**
+ * Set the number when a week ends
+ *
+ * @param dayNumber
+ * The number of the day
+ */
+ public void setLastHourOfTheDay(int hour) {
+ assert (hour >= 0 && hour <= 23);
+ lastHour = hour;
+ }
+
+ /**
+ * Get the number when a week ends
+ */
+ public int getLastHourOfTheDay() {
+ return lastHour;
+ }
+
+ /**
+ * Re-renders the whole week view
+ *
+ * @param scroll
+ * The amount of pixels to scroll the week view
+ * @param today
+ * Todays date
+ * @param daysInMonth
+ * How many days are there in the month
+ * @param firstDayOfWeek
+ * The first day of the week
+ * @param events
+ * The events to render
+ */
+ public void updateWeekView(int scroll, Date today, int daysInMonth,
+ int firstDayOfWeek, Collection<CalendarEvent> events,
+ List<CalendarDay> days) {
+
+ while (outer.getWidgetCount() > 0) {
+ outer.remove(0);
+ }
+
+ monthGrid = null;
+ String[] realDayNames = new String[getDayNames().length];
+ int j = 0;
+
+ if (firstDayOfWeek == 2) {
+ for (int i = 1; i < getDayNames().length; i++) {
+ realDayNames[j++] = getDayNames()[i];
+ }
+ realDayNames[j] = getDayNames()[0];
+ } else {
+ for (int i = 0; i < getDayNames().length; i++) {
+ realDayNames[j++] = getDayNames()[i];
+ }
+
+ }
+
+ weeklyLongEvents = new WeeklyLongEvents(this);
+ if (weekGrid == null) {
+ weekGrid = new WeekGrid(this, is24HFormat());
+ }
+ updateWeekGrid(daysInMonth, days, today, realDayNames);
+ updateEventsToWeekGrid(sortEventsByDuration(events));
+ outer.add(dayToolbar, DockPanel.NORTH);
+ outer.add(weeklyLongEvents, DockPanel.NORTH);
+ outer.add(weekGrid, DockPanel.SOUTH);
+ weekGrid.setVerticalScrollPosition(scroll);
+ }
+
+ /**
+ * Re-renders the whole month view
+ *
+ * @param firstDayOfWeek
+ * The first day of the week
+ * @param today
+ * Todays date
+ * @param daysInMonth
+ * Amount of days in the month
+ * @param events
+ * The events to render
+ * @param days
+ * The day information
+ */
+ public void updateMonthView(int firstDayOfWeek, Date today, int daysInMonth,
+ Collection<CalendarEvent> events, List<CalendarDay> days) {
+
+ // Remove all week numbers from bar
+ while (outer.getWidgetCount() > 0) {
+ outer.remove(0);
+ }
+
+ int firstDay = getFirstDayNumber();
+ int lastDay = getLastDayNumber();
+ int daysPerWeek = lastDay - firstDay + 1;
+ int j = 0;
+
+ String[] dayNames = getDayNames();
+ String[] realDayNames = new String[daysPerWeek];
+
+ if (firstDayOfWeek == 2) {
+ for (int i = firstDay; i < lastDay + 1; i++) {
+ if (i == 7) {
+ realDayNames[j++] = dayNames[0];
+ } else {
+ realDayNames[j++] = dayNames[i];
+ }
+ }
+ } else {
+ for (int i = firstDay - 1; i < lastDay; i++) {
+ realDayNames[j++] = dayNames[i];
+ }
+ }
+
+ nameToolbar.setDayNames(realDayNames);
+
+ weeklyLongEvents = null;
+ weekGrid = null;
+
+ updateMonthGrid(daysInMonth, days, today);
+
+ outer.add(nameToolbar, DockPanel.NORTH);
+ outer.add(weekToolbar, DockPanel.WEST);
+ weekToolbar.updateCellHeights();
+ outer.add(monthGrid, DockPanel.CENTER);
+
+ updateEventsToMonthGrid(events, false);
+ }
+
+ private DateClickListener dateClickListener;
+
+ /**
+ * Sets the listener for listening to event clicks
+ *
+ * @param listener
+ * The listener to use
+ */
+ public void setListener(DateClickListener listener) {
+ dateClickListener = listener;
+ }
+
+ /**
+ * Gets the listener for listening to event clicks
+ *
+ * @return
+ */
+ public DateClickListener getDateClickListener() {
+ return dateClickListener;
+ }
+
+ private ForwardListener forwardListener;
+
+ /**
+ * Set the listener which listens to forward events from the calendar
+ *
+ * @param listener
+ * The listener to use
+ */
+ public void setListener(ForwardListener listener) {
+ forwardListener = listener;
+ }
+
+ /**
+ * Get the listener which listens to forward events from the calendar
+ *
+ * @return
+ */
+ public ForwardListener getForwardListener() {
+ return forwardListener;
+ }
+
+ private BackwardListener backwardListener;
+
+ /**
+ * Set the listener which listens to backward events from the calendar
+ *
+ * @param listener
+ * The listener to use
+ */
+ public void setListener(BackwardListener listener) {
+ backwardListener = listener;
+ }
+
+ /**
+ * Set the listener which listens to backward events from the calendar
+ *
+ * @return
+ */
+ public BackwardListener getBackwardListener() {
+ return backwardListener;
+ }
+
+ private WeekClickListener weekClickListener;
+
+ /**
+ * Set the listener that listens to user clicking on the week numbers
+ *
+ * @param listener
+ * The listener to use
+ */
+ public void setListener(WeekClickListener listener) {
+ weekClickListener = listener;
+ }
+
+ /**
+ * Get the listener that listens to user clicking on the week numbers
+ *
+ * @return
+ */
+ public WeekClickListener getWeekClickListener() {
+ return weekClickListener;
+ }
+
+ private RangeSelectListener rangeSelectListener;
+
+ /**
+ * Set the listener that listens to the user highlighting a region in the
+ * calendar
+ *
+ * @param listener
+ * The listener to use
+ */
+ public void setListener(RangeSelectListener listener) {
+ rangeSelectListener = listener;
+ }
+
+ /**
+ * Get the listener that listens to the user highlighting a region in the
+ * calendar
+ *
+ * @return
+ */
+ public RangeSelectListener getRangeSelectListener() {
+ return rangeSelectListener;
+ }
+
+ private EventClickListener eventClickListener;
+
+ /**
+ * Get the listener that listens to the user clicking on the events
+ */
+ public EventClickListener getEventClickListener() {
+ return eventClickListener;
+ }
+
+ /**
+ * Set the listener that listens to the user clicking on the events
+ *
+ * @param listener
+ * The listener to use
+ */
+ public void setListener(EventClickListener listener) {
+ eventClickListener = listener;
+ }
+
+ private EventMovedListener eventMovedListener;
+
+ /**
+ * Get the listener that listens to when event is dragged to a new location
+ *
+ * @return
+ */
+ public EventMovedListener getEventMovedListener() {
+ return eventMovedListener;
+ }
+
+ /**
+ * Set the listener that listens to when event is dragged to a new location
+ *
+ * @param eventMovedListener
+ * The listener to use
+ */
+ public void setListener(EventMovedListener eventMovedListener) {
+ this.eventMovedListener = eventMovedListener;
+ }
+
+ private ScrollListener scrollListener;
+
+ /**
+ * Get the listener that listens to when the calendar widget is scrolled
+ *
+ * @return
+ */
+ public ScrollListener getScrollListener() {
+ return scrollListener;
+ }
+
+ /**
+ * Set the listener that listens to when the calendar widget is scrolled
+ *
+ * @param scrollListener
+ * The listener to use
+ */
+ public void setListener(ScrollListener scrollListener) {
+ this.scrollListener = scrollListener;
+ }
+
+ private EventResizeListener eventResizeListener;
+
+ /**
+ * Get the listener that listens to when an events time limits are being
+ * adjusted
+ *
+ * @return
+ */
+ public EventResizeListener getEventResizeListener() {
+ return eventResizeListener;
+ }
+
+ /**
+ * Set the listener that listens to when an events time limits are being
+ * adjusted
+ *
+ * @param eventResizeListener
+ * The listener to use
+ */
+ public void setListener(EventResizeListener eventResizeListener) {
+ this.eventResizeListener = eventResizeListener;
+ }
+
+ private MouseEventListener mouseEventListener;
+ private boolean forwardNavigationEnabled = true;
+ private boolean backwardNavigationEnabled = true;
+ private boolean eventCaptionAsHtml = false;
+
+ /**
+ * Get the listener that listen to mouse events
+ *
+ * @return
+ */
+ public MouseEventListener getMouseEventListener() {
+ return mouseEventListener;
+ }
+
+ /**
+ * Set the listener that listen to mouse events
+ *
+ * @param mouseEventListener
+ * The listener to use
+ */
+ public void setListener(MouseEventListener mouseEventListener) {
+ this.mouseEventListener = mouseEventListener;
+ }
+
+ /**
+ * Is selecting a range allowed?
+ */
+ public boolean isRangeSelectAllowed() {
+ return rangeSelectAllowed;
+ }
+
+ /**
+ * Set selecting a range allowed
+ *
+ * @param rangeSelectAllowed
+ * Should selecting a range be allowed
+ */
+ public void setRangeSelectAllowed(boolean rangeSelectAllowed) {
+ this.rangeSelectAllowed = rangeSelectAllowed;
+ }
+
+ /**
+ * Is moving a range allowed
+ *
+ * @return
+ */
+ public boolean isRangeMoveAllowed() {
+ return rangeMoveAllowed;
+ }
+
+ /**
+ * Is moving a range allowed
+ *
+ * @param rangeMoveAllowed
+ * Is it allowed
+ */
+ public void setRangeMoveAllowed(boolean rangeMoveAllowed) {
+ this.rangeMoveAllowed = rangeMoveAllowed;
+ }
+
+ /**
+ * Is resizing an event allowed
+ */
+ public boolean isEventResizeAllowed() {
+ return eventResizeAllowed;
+ }
+
+ /**
+ * Is resizing an event allowed
+ *
+ * @param eventResizeAllowed
+ * True if allowed false if not
+ */
+ public void setEventResizeAllowed(boolean eventResizeAllowed) {
+ this.eventResizeAllowed = eventResizeAllowed;
+ }
+
+ /**
+ * Is moving an event allowed
+ */
+ public boolean isEventMoveAllowed() {
+ return eventMoveAllowed;
+ }
+
+ /**
+ * Is moving an event allowed
+ *
+ * @param eventMoveAllowed
+ * True if moving is allowed, false if not
+ */
+ public void setEventMoveAllowed(boolean eventMoveAllowed) {
+ this.eventMoveAllowed = eventMoveAllowed;
+ }
+
+ public boolean isBackwardNavigationEnabled() {
+ return backwardNavigationEnabled;
+ }
+
+ public void setBackwardNavigationEnabled(boolean enabled) {
+ backwardNavigationEnabled = enabled;
+ }
+
+ public boolean isForwardNavigationEnabled() {
+ return forwardNavigationEnabled;
+ }
+
+ public void setForwardNavigationEnabled(boolean enabled) {
+ forwardNavigationEnabled = enabled;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.client.ui.dd.VHasDropHandler#getDropHandler()
+ */
+ @Override
+ public CalendarDropHandler getDropHandler() {
+ return dropHandler;
+ }
+
+ /**
+ * Set the drop handler
+ *
+ * @param dropHandler
+ * The drophandler to use
+ */
+ public void setDropHandler(CalendarDropHandler dropHandler) {
+ this.dropHandler = dropHandler;
+ }
+
+ /**
+ * Sets whether the event captions are rendered as HTML.
+ * <p>
+ * If set to true, the captions are rendered in the browser as HTML and the
+ * developer is responsible for ensuring no harmful HTML is used. If set to
+ * false, the caption is rendered in the browser as plain text.
+ * <p>
+ * The default is false, i.e. to render that caption as plain text.
+ *
+ * @param captionAsHtml
+ * true if the captions are rendered as HTML, false if rendered
+ * as plain text
+ */
+ public void setEventCaptionAsHtml(boolean eventCaptionAsHtml) {
+ this.eventCaptionAsHtml = eventCaptionAsHtml;
+ }
+
+ /**
+ * Checks whether event captions are rendered as HTML
+ * <p>
+ * The default is false, i.e. to render that caption as plain text.
+ *
+ * @return true if the captions are rendered as HTML, false if rendered as
+ * plain text
+ */
+ public boolean isEventCaptionAsHtml() {
+ return eventCaptionAsHtml;
+ }
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/client/ui/VFilterSelect.java b/compatibility-client/src/main/java/com/vaadin/client/ui/VFilterSelect.java
new file mode 100644
index 0000000000..44cbcf28f6
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/client/ui/VFilterSelect.java
@@ -0,0 +1,2772 @@
+/*
+ * Copyright 2000-2016 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.vaadin.client.ui;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+
+import com.google.gwt.aria.client.Roles;
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.core.client.Scheduler;
+import com.google.gwt.core.client.Scheduler.ScheduledCommand;
+import com.google.gwt.dom.client.Document;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.dom.client.NativeEvent;
+import com.google.gwt.dom.client.Style;
+import com.google.gwt.dom.client.Style.Display;
+import com.google.gwt.dom.client.Style.Unit;
+import com.google.gwt.dom.client.Style.Visibility;
+import com.google.gwt.event.dom.client.BlurEvent;
+import com.google.gwt.event.dom.client.BlurHandler;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.event.dom.client.FocusEvent;
+import com.google.gwt.event.dom.client.FocusHandler;
+import com.google.gwt.event.dom.client.KeyCodes;
+import com.google.gwt.event.dom.client.KeyDownEvent;
+import com.google.gwt.event.dom.client.KeyDownHandler;
+import com.google.gwt.event.dom.client.KeyUpEvent;
+import com.google.gwt.event.dom.client.KeyUpHandler;
+import com.google.gwt.event.dom.client.LoadEvent;
+import com.google.gwt.event.dom.client.LoadHandler;
+import com.google.gwt.event.logical.shared.CloseEvent;
+import com.google.gwt.event.logical.shared.CloseHandler;
+import com.google.gwt.i18n.client.HasDirection.Direction;
+import com.google.gwt.user.client.Command;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.Event;
+import com.google.gwt.user.client.Timer;
+import com.google.gwt.user.client.Window;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.HTML;
+import com.google.gwt.user.client.ui.PopupPanel;
+import com.google.gwt.user.client.ui.PopupPanel.PositionCallback;
+import com.google.gwt.user.client.ui.SuggestOracle.Suggestion;
+import com.google.gwt.user.client.ui.TextBox;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.client.ApplicationConnection;
+import com.vaadin.client.BrowserInfo;
+import com.vaadin.client.ComputedStyle;
+import com.vaadin.client.DeferredWorker;
+import com.vaadin.client.Focusable;
+import com.vaadin.client.VConsole;
+import com.vaadin.client.WidgetUtil;
+import com.vaadin.client.ui.aria.AriaHelper;
+import com.vaadin.client.ui.aria.HandlesAriaCaption;
+import com.vaadin.client.ui.aria.HandlesAriaInvalid;
+import com.vaadin.client.ui.aria.HandlesAriaRequired;
+import com.vaadin.client.ui.combobox.ComboBoxConnector;
+import com.vaadin.client.ui.menubar.MenuBar;
+import com.vaadin.client.ui.menubar.MenuItem;
+import com.vaadin.shared.AbstractComponentState;
+import com.vaadin.shared.ui.ComponentStateUtil;
+import com.vaadin.shared.ui.combobox.FilteringMode;
+import com.vaadin.shared.util.SharedUtil;
+
+/**
+ * Client side implementation of the Select component.
+ *
+ * TODO needs major refactoring (to be extensible etc)
+ */
+@SuppressWarnings("deprecation")
+public class VFilterSelect extends Composite
+ implements Field, KeyDownHandler, KeyUpHandler, ClickHandler,
+ FocusHandler, BlurHandler, Focusable, SubPartAware, HandlesAriaCaption,
+ HandlesAriaInvalid, HandlesAriaRequired, DeferredWorker {
+
+ /**
+ * Represents a suggestion in the suggestion popup box
+ */
+ public class FilterSelectSuggestion implements Suggestion, Command {
+
+ private final String key;
+ private final String caption;
+ private String untranslatedIconUri;
+ private String style;
+
+ /**
+ * Constructor
+ *
+ * @param key
+ * item key, empty string for a special null item not in
+ * container
+ * @param caption
+ * item caption
+ * @param style
+ * item style name, can be empty string
+ * @param untranslatedIconUri
+ * icon URI or null
+ */
+ public FilterSelectSuggestion(String key, String caption, String style,
+ String untranslatedIconUri) {
+ this.key = key;
+ this.caption = caption;
+ this.style = style;
+ this.untranslatedIconUri = untranslatedIconUri;
+ }
+
+ /**
+ * Gets the visible row in the popup as a HTML string. The string
+ * contains an image tag with the rows icon (if an icon has been
+ * specified) and the caption of the item
+ */
+
+ @Override
+ public String getDisplayString() {
+ final StringBuffer sb = new StringBuffer();
+ ApplicationConnection client = connector.getConnection();
+ final Icon icon = client
+ .getIcon(client.translateVaadinUri(untranslatedIconUri));
+ if (icon != null) {
+ sb.append(icon.getElement().getString());
+ }
+ String content;
+ if ("".equals(caption)) {
+ // Ensure that empty options use the same height as other
+ // options and are not collapsed (#7506)
+ content = "&nbsp;";
+ } else {
+ content = WidgetUtil.escapeHTML(caption);
+ }
+ sb.append("<span>" + content + "</span>");
+ return sb.toString();
+ }
+
+ /**
+ * Get a string that represents this item. This is used in the text box.
+ */
+
+ @Override
+ public String getReplacementString() {
+ return caption;
+ }
+
+ /**
+ * Get the option key which represents the item on the server side.
+ *
+ * @return The key of the item
+ */
+ public String getOptionKey() {
+ return key;
+ }
+
+ /**
+ * Get the URI of the icon. Used when constructing the displayed option.
+ *
+ * @return
+ */
+ public String getIconUri() {
+ ApplicationConnection client = connector.getConnection();
+ return client.translateVaadinUri(untranslatedIconUri);
+ }
+
+ /**
+ * Gets the style set for this suggestion item. Styles are typically set
+ * by a server-side {@link com.vaadin.ui.ComboBox.ItemStyleGenerator}.
+ * The returned style is prefixed by <code>v-filterselect-item-</code>.
+ *
+ * @since 7.5.6
+ * @return the style name to use, or <code>null</code> to not apply any
+ * custom style.
+ */
+ public String getStyle() {
+ return style;
+ }
+
+ /**
+ * Executes a selection of this item.
+ */
+
+ @Override
+ public void execute() {
+ onSuggestionSelected(this);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof FilterSelectSuggestion)) {
+ return false;
+ }
+ FilterSelectSuggestion other = (FilterSelectSuggestion) obj;
+ if ((key == null && other.key != null)
+ || (key != null && !key.equals(other.key))) {
+ return false;
+ }
+ if ((caption == null && other.caption != null)
+ || (caption != null && !caption.equals(other.caption))) {
+ return false;
+ }
+ if (!SharedUtil.equals(untranslatedIconUri,
+ other.untranslatedIconUri)) {
+ return false;
+ }
+ if (!SharedUtil.equals(style, other.style)) {
+ return false;
+ }
+ return true;
+ }
+ }
+
+ /** An inner class that handles all logic related to mouse wheel. */
+ private class MouseWheeler extends JsniMousewheelHandler {
+
+ public MouseWheeler() {
+ super(VFilterSelect.this);
+ }
+
+ @Override
+ protected native JavaScriptObject createMousewheelListenerFunction(
+ Widget widget)
+ /*-{
+ return $entry(function(e) {
+ var deltaX = e.deltaX ? e.deltaX : -0.5*e.wheelDeltaX;
+ var deltaY = e.deltaY ? e.deltaY : -0.5*e.wheelDeltaY;
+
+ // IE8 has only delta y
+ if (isNaN(deltaY)) {
+ deltaY = -0.5*e.wheelDelta;
+ }
+
+ @com.vaadin.client.ui.VFilterSelect.JsniUtil::moveScrollFromEvent(*)(widget, deltaX, deltaY, e, e.deltaMode);
+ });
+ }-*/;
+
+ }
+
+ /**
+ * A utility class that contains utility methods that are usually called
+ * from JSNI.
+ * <p>
+ * The methods are moved in this class to minimize the amount of JSNI code
+ * as much as feasible.
+ */
+ static class JsniUtil {
+ private static final int DOM_DELTA_PIXEL = 0;
+ private static final int DOM_DELTA_LINE = 1;
+ private static final int DOM_DELTA_PAGE = 2;
+
+ // Rough estimation of item height
+ private static final int SCROLL_UNIT_PX = 25;
+
+ private static double deltaSum = 0;
+
+ public static void moveScrollFromEvent(final Widget widget,
+ final double deltaX, final double deltaY,
+ final NativeEvent event, final int deltaMode) {
+
+ if (!Double.isNaN(deltaY)) {
+ VFilterSelect filterSelect = (VFilterSelect) widget;
+
+ switch (deltaMode) {
+ case DOM_DELTA_LINE:
+ if (deltaY >= 0) {
+ filterSelect.suggestionPopup.selectNextItem();
+ } else {
+ filterSelect.suggestionPopup.selectPrevItem();
+ }
+ break;
+ case DOM_DELTA_PAGE:
+ if (deltaY >= 0) {
+ filterSelect.selectNextPage();
+ } else {
+ filterSelect.selectPrevPage();
+ }
+ break;
+ case DOM_DELTA_PIXEL:
+ default:
+ // Accumulate dampened deltas
+ deltaSum += Math.pow(Math.abs(deltaY), 0.7)
+ * Math.signum(deltaY);
+
+ // "Scroll" if change exceeds item height
+ while (Math.abs(deltaSum) >= SCROLL_UNIT_PX) {
+ if (!filterSelect.dataReceivedHandler
+ .isWaitingForFilteringResponse()) {
+ // Move selection if page flip is not in progress
+ if (deltaSum < 0) {
+ filterSelect.suggestionPopup.selectPrevItem();
+ } else {
+ filterSelect.suggestionPopup.selectNextItem();
+ }
+ }
+ deltaSum -= SCROLL_UNIT_PX * Math.signum(deltaSum);
+ }
+ break;
+ }
+ }
+ }
+ }
+
+ /**
+ * Represents the popup box with the selection options. Wraps a suggestion
+ * menu.
+ */
+ public class SuggestionPopup extends VOverlay
+ implements PositionCallback, CloseHandler<PopupPanel> {
+
+ private static final int Z_INDEX = 30000;
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public final SuggestionMenu menu;
+
+ private final Element up = DOM.createDiv();
+ private final Element down = DOM.createDiv();
+ private final Element status = DOM.createDiv();
+
+ private boolean isPagingEnabled = true;
+
+ private long lastAutoClosed;
+
+ private int popupOuterPadding = -1;
+
+ private int topPosition;
+
+ private final MouseWheeler mouseWheeler = new MouseWheeler();
+
+ /**
+ * Default constructor
+ */
+ SuggestionPopup() {
+ super(true, false);
+ debug("VFS.SP: constructor()");
+ setOwner(VFilterSelect.this);
+ menu = new SuggestionMenu();
+ setWidget(menu);
+
+ getElement().getStyle().setZIndex(Z_INDEX);
+
+ final Element root = getContainerElement();
+
+ up.setInnerHTML("<span>Prev</span>");
+ DOM.sinkEvents(up, Event.ONCLICK);
+
+ down.setInnerHTML("<span>Next</span>");
+ DOM.sinkEvents(down, Event.ONCLICK);
+
+ root.insertFirst(up);
+ root.appendChild(down);
+ root.appendChild(status);
+
+ DOM.sinkEvents(root, Event.ONMOUSEDOWN | Event.ONMOUSEWHEEL);
+ addCloseHandler(this);
+
+ Roles.getListRole().set(getElement());
+
+ setPreviewingAllNativeEvents(true);
+ }
+
+ @Override
+ protected void onLoad() {
+ super.onLoad();
+
+ // Register mousewheel listener on paged select
+ if (pageLength > 0) {
+ mouseWheeler.attachMousewheelListener(getElement());
+ }
+ }
+
+ @Override
+ protected void onUnload() {
+ mouseWheeler.detachMousewheelListener(getElement());
+ super.onUnload();
+ }
+
+ /**
+ * Shows the popup where the user can see the filtered options
+ *
+ * @param currentSuggestions
+ * The filtered suggestions
+ * @param currentPage
+ * The current page number
+ * @param totalSuggestions
+ * The total amount of suggestions
+ */
+ public void showSuggestions(
+ final Collection<FilterSelectSuggestion> currentSuggestions,
+ final int currentPage, final int totalSuggestions) {
+
+ debug("VFS.SP: showSuggestions(" + currentSuggestions + ", "
+ + currentPage + ", " + totalSuggestions + ")");
+
+ /*
+ * We need to defer the opening of the popup so that the parent DOM
+ * has stabilized so we can calculate an absolute top and left
+ * correctly. This issue manifests when a Combobox is placed in
+ * another popupView which also needs to calculate the absoluteTop()
+ * to position itself. #9768
+ *
+ * After deferring the showSuggestions method, a problem with
+ * navigating in the combo box occurs. Because of that the method
+ * navigateItemAfterPageChange in ComboBoxConnector class, which
+ * navigates to the exact item after page was changed also was
+ * marked as deferred. #11333
+ */
+ final SuggestionPopup popup = this;
+ Scheduler.get().scheduleDeferred(new ScheduledCommand() {
+ @Override
+ public void execute() {
+ // Add TT anchor point
+ getElement().setId("VAADIN_COMBOBOX_OPTIONLIST");
+
+ menu.setSuggestions(currentSuggestions);
+ final int x = VFilterSelect.this.getAbsoluteLeft();
+
+ topPosition = tb.getAbsoluteTop();
+ topPosition += tb.getOffsetHeight();
+
+ setPopupPosition(x, topPosition);
+
+ int nullOffset = (nullSelectionAllowed
+ && "".equals(lastFilter) ? 1 : 0);
+ boolean firstPage = (currentPage == 0);
+ final int first = currentPage * pageLength + 1
+ - (firstPage ? 0 : nullOffset);
+ final int last = first + currentSuggestions.size() - 1
+ - (firstPage && "".equals(lastFilter) ? nullOffset
+ : 0);
+ final int matches = totalSuggestions - nullOffset;
+ if (last > 0) {
+ // nullsel not counted, as requested by user
+ status.setInnerText((matches == 0 ? 0 : first) + "-"
+ + last + "/" + matches);
+ } else {
+ status.setInnerText("");
+ }
+ // We don't need to show arrows or statusbar if there is
+ // only one page
+ if (totalSuggestions <= pageLength || pageLength == 0) {
+ setPagingEnabled(false);
+ } else {
+ setPagingEnabled(true);
+ }
+ setPrevButtonActive(first > 1);
+ setNextButtonActive(last < matches);
+
+ // clear previously fixed width
+ menu.setWidth("");
+ menu.getElement().getFirstChildElement().getStyle()
+ .clearWidth();
+
+ setPopupPositionAndShow(popup);
+ }
+ });
+ }
+
+ /**
+ * Should the next page button be visible to the user?
+ *
+ * @param active
+ */
+ private void setNextButtonActive(boolean active) {
+ if (enableDebug) {
+ debug("VFS.SP: setNextButtonActive(" + active + ")");
+ }
+ if (active) {
+ DOM.sinkEvents(down, Event.ONCLICK);
+ down.setClassName(
+ VFilterSelect.this.getStylePrimaryName() + "-nextpage");
+ } else {
+ DOM.sinkEvents(down, 0);
+ down.setClassName(VFilterSelect.this.getStylePrimaryName()
+ + "-nextpage-off");
+ }
+ }
+
+ /**
+ * Should the previous page button be visible to the user
+ *
+ * @param active
+ */
+ private void setPrevButtonActive(boolean active) {
+ if (enableDebug) {
+ debug("VFS.SP: setPrevButtonActive(" + active + ")");
+ }
+
+ if (active) {
+ DOM.sinkEvents(up, Event.ONCLICK);
+ up.setClassName(
+ VFilterSelect.this.getStylePrimaryName() + "-prevpage");
+ } else {
+ DOM.sinkEvents(up, 0);
+ up.setClassName(VFilterSelect.this.getStylePrimaryName()
+ + "-prevpage-off");
+ }
+
+ }
+
+ /**
+ * Selects the next item in the filtered selections
+ */
+ public void selectNextItem() {
+ debug("VFS.SP: selectNextItem()");
+
+ final int index = menu.getSelectedIndex() + 1;
+ if (menu.getItems().size() > index) {
+ selectItem(menu.getItems().get(index));
+
+ } else {
+ selectNextPage();
+ }
+ }
+
+ /**
+ * Selects the previous item in the filtered selections
+ */
+ public void selectPrevItem() {
+ debug("VFS.SP: selectPrevItem()");
+
+ final int index = menu.getSelectedIndex() - 1;
+ if (index > -1) {
+ selectItem(menu.getItems().get(index));
+
+ } else if (index == -1) {
+ selectPrevPage();
+
+ } else {
+ if (!menu.getItems().isEmpty()) {
+ selectLastItem();
+ }
+ }
+ }
+
+ /**
+ * Select the first item of the suggestions list popup.
+ *
+ * @since 7.2.6
+ */
+ public void selectFirstItem() {
+ debug("VFS.SP: selectFirstItem()");
+ selectItem(menu.getFirstItem());
+ }
+
+ /**
+ * Select the last item of the suggestions list popup.
+ *
+ * @since 7.2.6
+ */
+ public void selectLastItem() {
+ debug("VFS.SP: selectLastItem()");
+ selectItem(menu.getLastItem());
+ }
+
+ /*
+ * Sets the selected item in the popup menu.
+ */
+ private void selectItem(final MenuItem newSelectedItem) {
+ menu.selectItem(newSelectedItem);
+
+ // Set the icon.
+ FilterSelectSuggestion suggestion = (FilterSelectSuggestion) newSelectedItem
+ .getCommand();
+ setSelectedItemIcon(suggestion.getIconUri());
+
+ // Set the text.
+ setText(suggestion.getReplacementString());
+
+ }
+
+ /*
+ * Using a timer to scroll up or down the pages so when we receive lots
+ * of consecutive mouse wheel events the pages does not flicker.
+ */
+ private LazyPageScroller lazyPageScroller = new LazyPageScroller();
+
+ private class LazyPageScroller extends Timer {
+ private int pagesToScroll = 0;
+
+ @Override
+ public void run() {
+ debug("VFS.SP.LPS: run()");
+ if (pagesToScroll != 0) {
+ if (!dataReceivedHandler.isWaitingForFilteringResponse()) {
+ /*
+ * Avoid scrolling while we are waiting for a response
+ * because otherwise the waiting flag will be reset in
+ * the first response and the second response will be
+ * ignored, causing an empty popup...
+ *
+ * As long as the scrolling delay is suitable
+ * double/triple clicks will work by scrolling two or
+ * three pages at a time and this should not be a
+ * problem.
+ */
+ filterOptions(currentPage + pagesToScroll, lastFilter);
+ }
+ pagesToScroll = 0;
+ }
+ }
+
+ public void scrollUp() {
+ debug("VFS.SP.LPS: scrollUp()");
+ if (pageLength > 0 && currentPage + pagesToScroll > 0) {
+ pagesToScroll--;
+ cancel();
+ schedule(200);
+ }
+ }
+
+ public void scrollDown() {
+ debug("VFS.SP.LPS: scrollDown()");
+ if (pageLength > 0
+ && totalMatches > (currentPage + pagesToScroll + 1)
+ * pageLength) {
+ pagesToScroll++;
+ cancel();
+ schedule(200);
+ }
+ }
+ }
+
+ private void scroll(double deltaY) {
+ boolean scrollActive = menu.isScrollActive();
+
+ debug("VFS.SP: scroll() scrollActive: " + scrollActive);
+
+ if (!scrollActive) {
+ if (deltaY > 0d) {
+ lazyPageScroller.scrollDown();
+ } else {
+ lazyPageScroller.scrollUp();
+ }
+ }
+ }
+
+ @Override
+ public void onBrowserEvent(Event event) {
+ debug("VFS.SP: onBrowserEvent()");
+
+ if (event.getTypeInt() == Event.ONCLICK) {
+ final Element target = DOM.eventGetTarget(event);
+ if (target == up || target == DOM.getChild(up, 0)) {
+ lazyPageScroller.scrollUp();
+ } else if (target == down || target == DOM.getChild(down, 0)) {
+ lazyPageScroller.scrollDown();
+ }
+
+ }
+
+ /*
+ * Prevent the keyboard focus from leaving the textfield by
+ * preventing the default behaviour of the browser. Fixes #4285.
+ */
+ handleMouseDownEvent(event);
+ }
+
+ /**
+ * Should paging be enabled. If paging is enabled then only a certain
+ * amount of items are visible at a time and a scrollbar or buttons are
+ * visible to change page. If paging is turned of then all options are
+ * rendered into the popup menu.
+ *
+ * @param paging
+ * Should the paging be turned on?
+ */
+ public void setPagingEnabled(boolean paging) {
+ debug("VFS.SP: setPagingEnabled(" + paging + ")");
+ if (isPagingEnabled == paging) {
+ return;
+ }
+ if (paging) {
+ down.getStyle().clearDisplay();
+ up.getStyle().clearDisplay();
+ status.getStyle().clearDisplay();
+ } else {
+ down.getStyle().setDisplay(Display.NONE);
+ up.getStyle().setDisplay(Display.NONE);
+ status.getStyle().setDisplay(Display.NONE);
+ }
+ isPagingEnabled = paging;
+ }
+
+ @Override
+ public void setPosition(int offsetWidth, int offsetHeight) {
+ debug("VFS.SP: setPosition(" + offsetWidth + ", " + offsetHeight
+ + ")");
+
+ int top = topPosition;
+ int left = getPopupLeft();
+
+ // reset menu size and retrieve its "natural" size
+ menu.setHeight("");
+ if (currentPage > 0 && !hasNextPage()) {
+ // fix height to avoid height change when getting to last page
+ menu.fixHeightTo(pageLength);
+ }
+
+ final int desiredHeight = offsetHeight = getOffsetHeight();
+ final int desiredWidth = getMainWidth();
+
+ debug("VFS.SP: desired[" + desiredWidth + ", " + desiredHeight
+ + "]");
+
+ Element menuFirstChild = menu.getElement().getFirstChildElement();
+ int naturalMenuWidth;
+ if (BrowserInfo.get().isIE()
+ && BrowserInfo.get().getBrowserMajorVersion() < 10) {
+ // On IE 8 & 9 visibility is set to hidden and measuring
+ // elements while they are hidden yields incorrect results
+ String before = menu.getElement().getParentElement().getStyle()
+ .getVisibility();
+ menu.getElement().getParentElement().getStyle()
+ .setVisibility(Visibility.VISIBLE);
+ naturalMenuWidth = WidgetUtil.getRequiredWidth(menuFirstChild);
+ menu.getElement().getParentElement().getStyle()
+ .setProperty("visibility", before);
+ } else {
+ naturalMenuWidth = WidgetUtil.getRequiredWidth(menuFirstChild);
+ }
+
+ if (popupOuterPadding == -1) {
+ popupOuterPadding = WidgetUtil
+ .measureHorizontalPaddingAndBorder(menu.getElement(), 2)
+ + WidgetUtil.measureHorizontalPaddingAndBorder(
+ suggestionPopup.getElement(), 0);
+ }
+
+ updateMenuWidth(desiredWidth, naturalMenuWidth);
+
+ if (BrowserInfo.get().isIE()
+ && BrowserInfo.get().getBrowserMajorVersion() < 11) {
+ // Must take margin,border,padding manually into account for
+ // menu element as we measure the element child and set width to
+ // the element parent
+
+ double naturalMenuOuterWidth;
+ if (BrowserInfo.get().getBrowserMajorVersion() < 10) {
+ // On IE 8 & 9 visibility is set to hidden and measuring
+ // elements while they are hidden yields incorrect results
+ String before = menu.getElement().getParentElement()
+ .getStyle().getVisibility();
+ menu.getElement().getParentElement().getStyle()
+ .setVisibility(Visibility.VISIBLE);
+ naturalMenuOuterWidth = WidgetUtil
+ .getRequiredWidthDouble(menuFirstChild)
+ + getMarginBorderPaddingWidth(menu.getElement());
+ menu.getElement().getParentElement().getStyle()
+ .setProperty("visibility", before);
+ } else {
+ naturalMenuOuterWidth = WidgetUtil
+ .getRequiredWidthDouble(menuFirstChild)
+ + getMarginBorderPaddingWidth(menu.getElement());
+ }
+
+ /*
+ * IE requires us to specify the width for the container
+ * element. Otherwise it will be 100% wide
+ */
+ double rootWidth = Math.max(desiredWidth - popupOuterPadding,
+ naturalMenuOuterWidth);
+ getContainerElement().getStyle().setWidth(rootWidth, Unit.PX);
+ }
+
+ final int textInputHeight = VFilterSelect.this.getOffsetHeight();
+ final int textInputTopOnPage = tb.getAbsoluteTop();
+ final int viewportOffset = Document.get().getScrollTop();
+ final int textInputTopInViewport = textInputTopOnPage
+ - viewportOffset;
+ final int textInputBottomInViewport = textInputTopInViewport
+ + textInputHeight;
+
+ final int spaceAboveInViewport = textInputTopInViewport;
+ final int spaceBelowInViewport = Window.getClientHeight()
+ - textInputBottomInViewport;
+
+ if (spaceBelowInViewport < offsetHeight
+ && spaceBelowInViewport < spaceAboveInViewport) {
+ // popup on top of input instead
+ if (offsetHeight > spaceAboveInViewport) {
+ // Shrink popup height to fit above
+ offsetHeight = spaceAboveInViewport;
+ }
+ top = textInputTopOnPage - offsetHeight;
+ } else {
+ // Show below, position calculated in showSuggestions for some
+ // strange reason
+ top = topPosition;
+ offsetHeight = Math.min(offsetHeight, spaceBelowInViewport);
+ }
+
+ // fetch real width (mac FF bugs here due GWT popups overflow:auto )
+ offsetWidth = menuFirstChild.getOffsetWidth();
+
+ if (offsetHeight < desiredHeight) {
+ int menuHeight = offsetHeight;
+ if (isPagingEnabled) {
+ menuHeight -= up.getOffsetHeight() + down.getOffsetHeight()
+ + status.getOffsetHeight();
+ } else {
+ final ComputedStyle s = new ComputedStyle(
+ menu.getElement());
+ menuHeight -= s.getIntProperty("marginBottom")
+ + s.getIntProperty("marginTop");
+ }
+
+ // If the available page height is really tiny then this will be
+ // negative and an exception will be thrown on setHeight.
+ int menuElementHeight = menu.getItemOffsetHeight();
+ if (menuHeight < menuElementHeight) {
+ menuHeight = menuElementHeight;
+ }
+
+ menu.setHeight(menuHeight + "px");
+
+ if (suggestionPopupWidth == null) {
+ final int naturalMenuWidthPlusScrollBar = naturalMenuWidth
+ + WidgetUtil.getNativeScrollbarSize();
+ if (offsetWidth < naturalMenuWidthPlusScrollBar) {
+ menu.setWidth(naturalMenuWidthPlusScrollBar + "px");
+ }
+ }
+ }
+
+ if (offsetWidth + left > Window.getClientWidth()) {
+ left = VFilterSelect.this.getAbsoluteLeft()
+ + VFilterSelect.this.getOffsetWidth() - offsetWidth;
+ if (left < 0) {
+ left = 0;
+ menu.setWidth(Window.getClientWidth() + "px");
+
+ }
+ }
+
+ setPopupPosition(left, top);
+ menu.scrollSelectionIntoView();
+ }
+
+ /**
+ * Adds in-line CSS rules to the DOM according to the
+ * suggestionPopupWidth field
+ *
+ * @param desiredWidth
+ * @param naturalMenuWidth
+ */
+ private void updateMenuWidth(final int desiredWidth,
+ int naturalMenuWidth) {
+ /**
+ * Three different width modes for the suggestion pop-up:
+ *
+ * 1. Legacy "null"-mode: width is determined by the longest item
+ * caption for each page while still maintaining minimum width of
+ * (desiredWidth - popupOuterPadding)
+ *
+ * 2. relative to the component itself
+ *
+ * 3. fixed width
+ */
+ String width = "auto";
+ if (suggestionPopupWidth == null) {
+ if (naturalMenuWidth < desiredWidth) {
+ naturalMenuWidth = desiredWidth - popupOuterPadding;
+ width = (desiredWidth - popupOuterPadding) + "px";
+ }
+ } else if (isrelativeUnits(suggestionPopupWidth)) {
+ float mainComponentWidth = desiredWidth - popupOuterPadding;
+ // convert percentage value to fraction
+ int widthInPx = Math.round(
+ mainComponentWidth * asFraction(suggestionPopupWidth));
+ width = widthInPx + "px";
+ } else {
+ // use as fixed width CSS definition
+ width = WidgetUtil.escapeAttribute(suggestionPopupWidth);
+ }
+ menu.setWidth(width);
+ }
+
+ /**
+ * Returns the percentage value as a fraction, e.g. 42% -> 0.42
+ *
+ * @param percentage
+ */
+ private float asFraction(String percentage) {
+ String trimmed = percentage.trim();
+ String withoutPercentSign = trimmed.substring(0,
+ trimmed.length() - 1);
+ float asFraction = Float.parseFloat(withoutPercentSign) / 100;
+ return asFraction;
+ }
+
+ /**
+ * @since 7.7
+ * @param suggestionPopupWidth
+ * @return
+ */
+ private boolean isrelativeUnits(String suggestionPopupWidth) {
+ return suggestionPopupWidth.trim().endsWith("%");
+ }
+
+ /**
+ * Was the popup just closed?
+ *
+ * @return true if popup was just closed
+ */
+ public boolean isJustClosed() {
+ debug("VFS.SP: justClosed()");
+ final long now = (new Date()).getTime();
+ return (lastAutoClosed > 0 && (now - lastAutoClosed) < 200);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.google.gwt.event.logical.shared.CloseHandler#onClose(com.google
+ * .gwt.event.logical.shared.CloseEvent)
+ */
+
+ @Override
+ public void onClose(CloseEvent<PopupPanel> event) {
+ if (enableDebug) {
+ debug("VFS.SP: onClose(" + event.isAutoClosed() + ")");
+ }
+ if (event.isAutoClosed()) {
+ lastAutoClosed = (new Date()).getTime();
+ }
+ }
+
+ /**
+ * Updates style names in suggestion popup to help theme building.
+ *
+ * @param componentState
+ * shared state of the combo box
+ */
+ public void updateStyleNames(AbstractComponentState componentState) {
+ debug("VFS.SP: updateStyleNames()");
+ setStyleName(
+ VFilterSelect.this.getStylePrimaryName() + "-suggestpopup");
+ menu.setStyleName(
+ VFilterSelect.this.getStylePrimaryName() + "-suggestmenu");
+ status.setClassName(
+ VFilterSelect.this.getStylePrimaryName() + "-status");
+ if (ComponentStateUtil.hasStyles(componentState)) {
+ for (String style : componentState.styles) {
+ if (!"".equals(style)) {
+ addStyleDependentName(style);
+ }
+ }
+ }
+ }
+
+ }
+
+ /**
+ * The menu where the suggestions are rendered
+ */
+ public class SuggestionMenu extends MenuBar
+ implements SubPartAware, LoadHandler {
+
+ private VLazyExecutor delayedImageLoadExecutioner = new VLazyExecutor(
+ 100, new ScheduledCommand() {
+
+ @Override
+ public void execute() {
+ debug("VFS.SM: delayedImageLoadExecutioner()");
+ if (suggestionPopup.isVisible()
+ && suggestionPopup.isAttached()) {
+ setWidth("");
+ getElement().getFirstChildElement().getStyle()
+ .clearWidth();
+ suggestionPopup
+ .setPopupPositionAndShow(suggestionPopup);
+ }
+
+ }
+ });
+
+ /**
+ * Default constructor
+ */
+ SuggestionMenu() {
+ super(true);
+ debug("VFS.SM: constructor()");
+ addDomHandler(this, LoadEvent.getType());
+
+ setScrollEnabled(true);
+ }
+
+ /**
+ * Fixes menus height to use same space as full page would use. Needed
+ * to avoid height changes when quickly "scrolling" to last page.
+ */
+ public void fixHeightTo(int pageItemsCount) {
+ setHeight(getPreferredHeight(pageItemsCount));
+ }
+
+ /*
+ * Gets the preferred height of the menu including pageItemsCount items.
+ */
+ String getPreferredHeight(int pageItemsCount) {
+ if (currentSuggestions.size() > 0) {
+ final int pixels = (getPreferredHeight()
+ / currentSuggestions.size()) * pageItemsCount;
+ return pixels + "px";
+ } else {
+ return "";
+ }
+ }
+
+ /**
+ * Sets the suggestions rendered in the menu
+ *
+ * @param suggestions
+ * The suggestions to be rendered in the menu
+ */
+ public void setSuggestions(
+ Collection<FilterSelectSuggestion> suggestions) {
+ if (enableDebug) {
+ debug("VFS.SM: setSuggestions(" + suggestions + ")");
+ }
+
+ clearItems();
+ final Iterator<FilterSelectSuggestion> it = suggestions.iterator();
+ boolean isFirstIteration = true;
+ while (it.hasNext()) {
+ final FilterSelectSuggestion s = it.next();
+ final MenuItem mi = new MenuItem(s.getDisplayString(), true, s);
+ String style = s.getStyle();
+ if (style != null) {
+ mi.addStyleName("v-filterselect-item-" + style);
+ }
+ Roles.getListitemRole().set(mi.getElement());
+
+ WidgetUtil.sinkOnloadForImages(mi.getElement());
+
+ this.addItem(mi);
+
+ // By default, first item on the list is always highlighted,
+ // unless adding new items is allowed.
+ if (isFirstIteration && !allowNewItem) {
+ selectItem(mi);
+ }
+
+ // If the filter matches the current selection, highlight that
+ // instead of the first item.
+ if (tb.getText().equals(s.getReplacementString())
+ && s == currentSuggestion) {
+ selectItem(mi);
+ }
+
+ isFirstIteration = false;
+ }
+ }
+
+ /**
+ * Send the current selection to the server. Triggered when a selection
+ * is made with the ENTER key.
+ */
+ public void doSelectedItemAction() {
+ debug("VFS.SM: doSelectedItemAction()");
+ // do not send a value change event if null was and stays selected
+ final String enteredItemValue = tb.getText();
+ if (nullSelectionAllowed && "".equals(enteredItemValue)
+ && selectedOptionKey != null
+ && !"".equals(selectedOptionKey)) {
+ if (nullSelectItem) {
+ reset();
+ return;
+ }
+ // null is not visible on pages != 0, and not visible when
+ // filtering: handle separately
+ connector.requestFirstPage();
+
+ suggestionPopup.hide();
+ return;
+ }
+
+ dataReceivedHandler.doPostFilterWhenReady();
+ }
+
+ /**
+ * Triggered after a selection has been made.
+ */
+ public void doPostFilterSelectedItemAction() {
+ debug("VFS.SM: doPostFilterSelectedItemAction()");
+ final MenuItem item = getSelectedItem();
+ final String enteredItemValue = tb.getText();
+
+ // check for exact match in menu
+ int p = getItems().size();
+ if (p > 0) {
+ for (int i = 0; i < p; i++) {
+ final MenuItem potentialExactMatch = getItems().get(i);
+ if (potentialExactMatch.getText()
+ .equals(enteredItemValue)) {
+ selectItem(potentialExactMatch);
+ // do not send a value change event if null was and
+ // stays selected
+ if (!"".equals(enteredItemValue)
+ || (selectedOptionKey != null
+ && !"".equals(selectedOptionKey))) {
+ doItemAction(potentialExactMatch, true);
+ }
+ suggestionPopup.hide();
+ return;
+ }
+ }
+ }
+ if (allowNewItem) {
+
+ if (!prompting && !enteredItemValue.equals(lastNewItemString)) {
+ /*
+ * Store last sent new item string to avoid double sends
+ */
+ lastNewItemString = enteredItemValue;
+ connector.sendNewItem(enteredItemValue);
+ }
+ } else if (item != null && !"".equals(lastFilter)
+ && (filteringmode == FilteringMode.CONTAINS
+ ? item.getText().toLowerCase()
+ .contains(lastFilter.toLowerCase())
+ : item.getText().toLowerCase()
+ .startsWith(lastFilter.toLowerCase()))) {
+ doItemAction(item, true);
+ } else {
+ // currentSuggestion has key="" for nullselection
+ if (currentSuggestion != null
+ && !currentSuggestion.key.equals("")) {
+ // An item (not null) selected
+ String text = currentSuggestion.getReplacementString();
+ setText(text);
+ selectedOptionKey = currentSuggestion.key;
+ } else {
+ // Null selected
+ setText("");
+ selectedOptionKey = null;
+ }
+ }
+ suggestionPopup.hide();
+ }
+
+ private static final String SUBPART_PREFIX = "item";
+
+ @Override
+ public com.google.gwt.user.client.Element getSubPartElement(
+ String subPart) {
+ int index = Integer
+ .parseInt(subPart.substring(SUBPART_PREFIX.length()));
+
+ MenuItem item = getItems().get(index);
+
+ return item.getElement();
+ }
+
+ @Override
+ public String getSubPartName(
+ com.google.gwt.user.client.Element subElement) {
+ if (!getElement().isOrHasChild(subElement)) {
+ return null;
+ }
+
+ Element menuItemRoot = subElement;
+ while (menuItemRoot != null
+ && !menuItemRoot.getTagName().equalsIgnoreCase("td")) {
+ menuItemRoot = menuItemRoot.getParentElement().cast();
+ }
+ // "menuItemRoot" is now the root of the menu item
+
+ final int itemCount = getItems().size();
+ for (int i = 0; i < itemCount; i++) {
+ if (getItems().get(i).getElement() == menuItemRoot) {
+ String name = SUBPART_PREFIX + i;
+ return name;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public void onLoad(LoadEvent event) {
+ debug("VFS.SM: onLoad()");
+ // Handle icon onload events to ensure shadow is resized
+ // correctly
+ delayedImageLoadExecutioner.trigger();
+
+ }
+
+ /**
+ * @deprecated use {@link SuggestionPopup#selectFirstItem()} instead.
+ */
+ @Deprecated
+ public void selectFirstItem() {
+ debug("VFS.SM: selectFirstItem()");
+ MenuItem firstItem = getItems().get(0);
+ selectItem(firstItem);
+ }
+
+ /**
+ * @deprecated use {@link SuggestionPopup#selectLastItem()} instead.
+ */
+ @Deprecated
+ public void selectLastItem() {
+ debug("VFS.SM: selectLastItem()");
+ List<MenuItem> items = getItems();
+ MenuItem lastItem = items.get(items.size() - 1);
+ selectItem(lastItem);
+ }
+
+ /*
+ * Gets the height of one menu item.
+ */
+ int getItemOffsetHeight() {
+ List<MenuItem> items = getItems();
+ return items != null && items.size() > 0
+ ? items.get(0).getOffsetHeight() : 0;
+ }
+
+ /*
+ * Gets the width of one menu item.
+ */
+ int getItemOffsetWidth() {
+ List<MenuItem> items = getItems();
+ return items != null && items.size() > 0
+ ? items.get(0).getOffsetWidth() : 0;
+ }
+
+ /**
+ * Returns true if the scroll is active on the menu element or if the
+ * menu currently displays the last page with less items then the
+ * maximum visibility (in which case the scroll is not active, but the
+ * scroll is active for any other page in general).
+ *
+ * @since 7.2.6
+ */
+ @Override
+ public boolean isScrollActive() {
+ String height = getElement().getStyle().getHeight();
+ String preferredHeight = getPreferredHeight(pageLength);
+
+ return !(height == null || height.length() == 0
+ || height.equals(preferredHeight));
+ }
+
+ }
+
+ /**
+ * TextBox variant used as input element for filter selects, which prevents
+ * selecting text when disabled.
+ *
+ * @since 7.1.5
+ */
+ public class FilterSelectTextBox extends TextBox {
+
+ /**
+ * Creates a new filter select text box.
+ *
+ * @since 7.6.4
+ */
+ public FilterSelectTextBox() {
+ /*-
+ * Stop the browser from showing its own suggestion popup.
+ *
+ * Using an invalid value instead of "off" as suggested by
+ * https://developer.mozilla.org/en-US/docs/Web/Security/Securing_your_site/Turning_off_form_autocompletion
+ *
+ * Leaving the non-standard Safari options autocapitalize and
+ * autocorrect untouched since those do not interfere in the same
+ * way, and they might be useful in a combo box where new items are
+ * allowed.
+ */
+ getElement().setAttribute("autocomplete", "nope");
+ }
+
+ /**
+ * Overridden to avoid selecting text when text input is disabled
+ */
+ @Override
+ public void setSelectionRange(int pos, int length) {
+ if (textInputEnabled) {
+ /*
+ * set selection range with a backwards direction: anchor at the
+ * back, focus at the front. This means that items that are too
+ * long to display will display from the start and not the end
+ * even on Firefox.
+ *
+ * We need the JSNI function to set selection range so that we
+ * can use the optional direction attribute to set the anchor to
+ * the end and the focus to the start. This makes Firefox work
+ * the same way as other browsers (#13477)
+ */
+ WidgetUtil.setSelectionRange(getElement(), pos, length,
+ "backward");
+
+ } else {
+ /*
+ * Setting the selectionrange for an uneditable textbox leads to
+ * unwanted behaviour when the width of the textbox is narrower
+ * than the width of the entry: the end of the entry is shown
+ * instead of the beginning. (see #13477)
+ *
+ * To avoid this, we set the caret to the beginning of the line.
+ */
+
+ super.setSelectionRange(0, 0);
+ }
+ }
+
+ }
+
+ /**
+ * Handler receiving notifications from the connector and updating the
+ * widget state accordingly.
+ *
+ * This class is still subject to change and should not be considered as
+ * public stable API.
+ *
+ * @since
+ */
+ public class DataReceivedHandler {
+
+ private Runnable navigationCallback = null;
+ /**
+ * Set true when popupopened has been clicked. Cleared on each
+ * UIDL-update. This handles the special case where are not filtering
+ * yet and the selected value has changed on the server-side. See #2119
+ * <p>
+ * For internal use only. May be removed or replaced in the future.
+ */
+ private boolean popupOpenerClicked = false;
+ private boolean performPostFilteringOnDataReceived = false;
+ /** For internal use only. May be removed or replaced in the future. */
+ private boolean waitingForFilteringResponse = false;
+
+ /**
+ * Called by the connector when new data for the last requested filter
+ * is received from the server.
+ */
+ public void dataReceived() {
+ suggestionPopup.showSuggestions(currentSuggestions, currentPage,
+ totalMatches);
+
+ waitingForFilteringResponse = false;
+
+ if (!popupOpenerClicked) {
+ navigateItemAfterPageChange();
+ }
+
+ if (performPostFilteringOnDataReceived) {
+ performPostFilteringOnDataReceived = false;
+ suggestionPopup.menu.doPostFilterSelectedItemAction();
+ }
+
+ popupOpenerClicked = false;
+ }
+
+ /*
+ * This method navigates to the proper item in the combobox page. This
+ * should be executed after setSuggestions() method which is called from
+ * vFilterSelect.showSuggestions(). ShowSuggestions() method builds the
+ * page content. As far as setSuggestions() method is called as
+ * deferred, navigateItemAfterPageChange method should be also be called
+ * as deferred. #11333
+ */
+ private void navigateItemAfterPageChange() {
+ if (navigationCallback != null) {
+ // pageChangeCallback is not reset here but after any server
+ // request in case you are in between two requests both changing
+ // the page back and forth
+
+ // we're paging w/ arrows
+ Scheduler.get().scheduleDeferred(new ScheduledCommand() {
+ @Override
+ public void execute() {
+ if (navigationCallback != null) {
+ navigationCallback.run();
+ }
+ }
+ });
+ }
+ }
+
+ /**
+ * Called by the connector when any request has been sent to the server,
+ * before waiting for a reply.
+ */
+ public void anyRequestSentToServer() {
+ navigationCallback = null;
+ }
+
+ /**
+ * Set a callback that is invoked when a page change occurs if there
+ * have not been intervening requests to the server. The callback is
+ * reset when any additional request is made to the server.
+ *
+ * @param callback
+ */
+ public void setNavigationCallback(Runnable callback) {
+ navigationCallback = callback;
+ }
+
+ /**
+ * Record that the popup opener has been clicked and the popup should be
+ * opened on the next request.
+ *
+ * This handles the special case where are not filtering yet and the
+ * selected value has changed on the server-side. See #2119. The flag is
+ * cleared on each UIDL reply.
+ */
+ public void popupOpenerClicked() {
+ popupOpenerClicked = true;
+ }
+
+ /**
+ * Cancel a pending request to perform post-filtering actions.
+ */
+ private void cancelPendingPostFiltering() {
+ performPostFilteringOnDataReceived = false;
+ }
+
+ /**
+ * Called by the connector when it has finished handling any reply from
+ * the server, regardless of what was updated.
+ */
+ public void serverReplyHandled() {
+ popupOpenerClicked = false;
+ }
+
+ /**
+ * For internal use only - this method will be removed in the future.
+ *
+ * @return true if the combo box is waiting for a reply from the server
+ * with a new page of data, false otherwise
+ */
+ public boolean isWaitingForFilteringResponse() {
+ return waitingForFilteringResponse;
+ }
+
+ /**
+ * Set a flag that filtering of options is pending a response from the
+ * server.
+ */
+ private void startWaitingForFilteringResponse() {
+ waitingForFilteringResponse = true;
+ }
+
+ /**
+ * Perform the post-filter action either now (if not waiting for a
+ * server response) or when a response is received.
+ */
+ private void doPostFilterWhenReady() {
+ if (isWaitingForFilteringResponse()) {
+ performPostFilteringOnDataReceived = true;
+ } else {
+ performPostFilteringOnDataReceived = false;
+ suggestionPopup.menu.doPostFilterSelectedItemAction();
+ }
+ }
+
+ /**
+ * Perform selection (if appropriate) based on a reply from the server.
+ * When this method is called, the suggestions have been reset if new
+ * ones (different from the previous list) were received from the
+ * server.
+ *
+ * @param selectedKey
+ * new selected key or null if none given by the server
+ * @param selectedCaption
+ * new selected item caption if sent by the server or null -
+ * this is used when the selected item is not on the current
+ * page
+ * @param oldSuggestionTextMatchTheOldSelection
+ * true if the old filtering text box contents matched
+ * exactly the old selection
+ */
+ public void updateSelectionFromServer(String selectedKey,
+ String selectedCaption,
+ boolean oldSuggestionTextMatchTheOldSelection) {
+ // when filtering with empty filter, server sets the selected key
+ // to "", which we don't select here. Otherwise we won't be able to
+ // reset back to the item that was selected before filtering
+ // started.
+ if (selectedKey != null && !selectedKey.equals("")) {
+ performSelection(selectedKey,
+ oldSuggestionTextMatchTheOldSelection,
+ !isWaitingForFilteringResponse() || popupOpenerClicked);
+ setSelectedCaption(null);
+ } else if (!isWaitingForFilteringResponse()
+ && selectedCaption != null) {
+ // scrolling to correct page is disabled, caption is passed as a
+ // special parameter
+ setSelectedCaption(selectedCaption);
+ } else {
+ if (!isWaitingForFilteringResponse() || popupOpenerClicked) {
+ resetSelection(popupOpenerClicked);
+ }
+ }
+ }
+
+ }
+
+ @Deprecated
+ public static final FilteringMode FILTERINGMODE_OFF = FilteringMode.OFF;
+ @Deprecated
+ public static final FilteringMode FILTERINGMODE_STARTSWITH = FilteringMode.STARTSWITH;
+ @Deprecated
+ public static final FilteringMode FILTERINGMODE_CONTAINS = FilteringMode.CONTAINS;
+
+ public static final String CLASSNAME = "v-filterselect";
+ private static final String STYLE_NO_INPUT = "no-input";
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public int pageLength = 10;
+
+ private boolean enableDebug = false;
+
+ private final FlowPanel panel = new FlowPanel();
+
+ /**
+ * The text box where the filter is written
+ * <p>
+ * For internal use only. May be removed or replaced in the future.
+ */
+ public final TextBox tb;
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public final SuggestionPopup suggestionPopup;
+
+ /**
+ * Used when measuring the width of the popup
+ */
+ private final HTML popupOpener = new HTML("") {
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.google.gwt.user.client.ui.Widget#onBrowserEvent(com.google.gwt
+ * .user.client.Event)
+ */
+
+ @Override
+ public void onBrowserEvent(Event event) {
+ super.onBrowserEvent(event);
+
+ /*
+ * Prevent the keyboard focus from leaving the textfield by
+ * preventing the default behaviour of the browser. Fixes #4285.
+ */
+ handleMouseDownEvent(event);
+ }
+ };
+
+ private class IconWidget extends Widget {
+ IconWidget(Icon icon) {
+ setElement(icon.getElement());
+ addDomHandler(VFilterSelect.this, ClickEvent.getType());
+ }
+ }
+
+ private IconWidget selectedItemIcon;
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public ComboBoxConnector connector;
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public int currentPage;
+
+ /**
+ * A collection of available suggestions (options) as received from the
+ * server.
+ * <p>
+ * For internal use only. May be removed or replaced in the future.
+ */
+ public final List<FilterSelectSuggestion> currentSuggestions = new ArrayList<FilterSelectSuggestion>();
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public String selectedOptionKey;
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public boolean initDone = false;
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public String lastFilter = "";
+
+ /**
+ * The current suggestion selected from the dropdown. This is one of the
+ * values in currentSuggestions except when filtering, in this case
+ * currentSuggestion might not be in currentSuggestions.
+ * <p>
+ * For internal use only. May be removed or replaced in the future.
+ */
+ public FilterSelectSuggestion currentSuggestion;
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public boolean allowNewItem;
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public int totalMatches;
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public boolean nullSelectionAllowed;
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public boolean nullSelectItem;
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public boolean enabled;
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public boolean readonly;
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public FilteringMode filteringmode = FilteringMode.OFF;
+
+ // shown in unfocused empty field, disappears on focus (e.g "Search here")
+ private static final String CLASSNAME_PROMPT = "prompt";
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public String inputPrompt = "";
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public boolean prompting = false;
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public int suggestionPopupMinWidth = 0;
+
+ public String suggestionPopupWidth = null;
+
+ private int popupWidth = -1;
+ /**
+ * Stores the last new item string to avoid double submissions. Cleared on
+ * uidl updates.
+ * <p>
+ * For internal use only. May be removed or replaced in the future.
+ */
+ public String lastNewItemString;
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public boolean focused = false;
+
+ /**
+ * If set to false, the component should not allow entering text to the
+ * field even for filtering.
+ */
+ private boolean textInputEnabled = true;
+
+ private final DataReceivedHandler dataReceivedHandler = new DataReceivedHandler();
+
+ /**
+ * Default constructor.
+ */
+ public VFilterSelect() {
+ tb = createTextBox();
+ suggestionPopup = createSuggestionPopup();
+
+ popupOpener.sinkEvents(Event.ONMOUSEDOWN);
+ Roles.getButtonRole().setAriaHiddenState(popupOpener.getElement(),
+ true);
+ Roles.getButtonRole().set(popupOpener.getElement());
+
+ panel.add(tb);
+ panel.add(popupOpener);
+ initWidget(panel);
+ Roles.getComboboxRole().set(panel.getElement());
+
+ tb.addKeyDownHandler(this);
+ tb.addKeyUpHandler(this);
+
+ tb.addFocusHandler(this);
+ tb.addBlurHandler(this);
+ tb.addClickHandler(this);
+
+ popupOpener.addClickHandler(this);
+
+ setStyleName(CLASSNAME);
+
+ sinkEvents(Event.ONPASTE);
+ }
+
+ private static double getMarginBorderPaddingWidth(Element element) {
+ final ComputedStyle s = new ComputedStyle(element);
+ return s.getMarginWidth() + s.getBorderWidth() + s.getPaddingWidth();
+
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.google.gwt.user.client.ui.Composite#onBrowserEvent(com.google.gwt
+ * .user.client.Event)
+ */
+ @Override
+ public void onBrowserEvent(Event event) {
+ super.onBrowserEvent(event);
+
+ if (event.getTypeInt() == Event.ONPASTE) {
+ if (textInputEnabled) {
+ Scheduler.get().scheduleDeferred(new ScheduledCommand() {
+
+ @Override
+ public void execute() {
+ filterOptions(currentPage);
+ }
+ });
+ }
+ }
+ }
+
+ /**
+ * This method will create the TextBox used by the VFilterSelect instance.
+ * It is invoked during the Constructor and should only be overridden if a
+ * custom TextBox shall be used. The overriding method cannot use any
+ * instance variables.
+ *
+ * @since 7.1.5
+ * @return TextBox instance used by this VFilterSelect
+ */
+ protected TextBox createTextBox() {
+ return new FilterSelectTextBox();
+ }
+
+ /**
+ * This method will create the SuggestionPopup used by the VFilterSelect
+ * instance. It is invoked during the Constructor and should only be
+ * overridden if a custom SuggestionPopup shall be used. The overriding
+ * method cannot use any instance variables.
+ *
+ * @since 7.1.5
+ * @return SuggestionPopup instance used by this VFilterSelect
+ */
+ protected SuggestionPopup createSuggestionPopup() {
+ return new SuggestionPopup();
+ }
+
+ @Override
+ public void setStyleName(String style) {
+ super.setStyleName(style);
+ updateStyleNames();
+ }
+
+ @Override
+ public void setStylePrimaryName(String style) {
+ super.setStylePrimaryName(style);
+ updateStyleNames();
+ }
+
+ protected void updateStyleNames() {
+ tb.setStyleName(getStylePrimaryName() + "-input");
+ popupOpener.setStyleName(getStylePrimaryName() + "-button");
+ suggestionPopup.setStyleName(getStylePrimaryName() + "-suggestpopup");
+ }
+
+ /**
+ * Does the Select have more pages?
+ *
+ * @return true if a next page exists, else false if the current page is the
+ * last page
+ */
+ public boolean hasNextPage() {
+ if (pageLength > 0 && totalMatches > (currentPage + 1) * pageLength) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Filters the options at a certain page. Uses the text box input as a
+ * filter
+ *
+ * @param page
+ * The page which items are to be filtered
+ */
+ public void filterOptions(int page) {
+ filterOptions(page, tb.getText());
+ }
+
+ /**
+ * Filters the options at certain page using the given filter
+ *
+ * @param page
+ * The page to filter
+ * @param filter
+ * The filter to apply to the components
+ */
+ public void filterOptions(int page, String filter) {
+ debug("VFS: filterOptions(" + page + ", " + filter + ")");
+
+ if (filter.equals(lastFilter) && currentPage == page) {
+ if (!suggestionPopup.isAttached()) {
+ suggestionPopup.showSuggestions(currentSuggestions, currentPage,
+ totalMatches);
+ }
+ return;
+ }
+ if (!filter.equals(lastFilter)) {
+ // when filtering, let the server decide the page unless we've
+ // set the filter to empty and explicitly said that we want to see
+ // the results starting from page 0.
+ if ("".equals(filter) && page != 0) {
+ // let server decide
+ page = -1;
+ } else {
+ page = 0;
+ }
+ }
+
+ dataReceivedHandler.startWaitingForFilteringResponse();
+ connector.requestPage(filter, page);
+
+ lastFilter = filter;
+ currentPage = page;
+ }
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public void updateReadOnly() {
+ debug("VFS: updateReadOnly()");
+ tb.setReadOnly(readonly || !textInputEnabled);
+ }
+
+ public void setTextInputEnabled(boolean textInputEnabled) {
+ debug("VFS: setTextInputEnabled()");
+ // Always update styles as they might have been overwritten
+ if (textInputEnabled) {
+ removeStyleDependentName(STYLE_NO_INPUT);
+ Roles.getTextboxRole().removeAriaReadonlyProperty(tb.getElement());
+ } else {
+ addStyleDependentName(STYLE_NO_INPUT);
+ Roles.getTextboxRole().setAriaReadonlyProperty(tb.getElement(),
+ true);
+ }
+
+ if (this.textInputEnabled == textInputEnabled) {
+ return;
+ }
+
+ this.textInputEnabled = textInputEnabled;
+ updateReadOnly();
+ }
+
+ /**
+ * Sets the text in the text box.
+ *
+ * @param text
+ * the text to set in the text box
+ */
+ public void setTextboxText(final String text) {
+ if (enableDebug) {
+ debug("VFS: setTextboxText(" + text + ")");
+ }
+ setText(text);
+ }
+
+ private void setText(final String text) {
+ /**
+ * To leave caret in the beginning of the line. SetSelectionRange
+ * wouldn't work on IE (see #13477)
+ */
+ Direction previousDirection = tb.getDirection();
+ tb.setDirection(Direction.RTL);
+ tb.setText(text);
+ tb.setDirection(previousDirection);
+ }
+
+ /**
+ * Turns prompting on. When prompting is turned on a command prompt is shown
+ * in the text box if nothing has been entered.
+ */
+ public void setPromptingOn() {
+ debug("VFS: setPromptingOn()");
+ if (!prompting) {
+ prompting = true;
+ addStyleDependentName(CLASSNAME_PROMPT);
+ }
+ setTextboxText(inputPrompt);
+ }
+
+ /**
+ * Turns prompting off. When prompting is turned on a command prompt is
+ * shown in the text box if nothing has been entered.
+ * <p>
+ * For internal use only. May be removed or replaced in the future.
+ *
+ * @param text
+ * The text the text box should contain.
+ */
+ public void setPromptingOff(String text) {
+ debug("VFS: setPromptingOff()");
+ setTextboxText(text);
+ if (prompting) {
+ prompting = false;
+ removeStyleDependentName(CLASSNAME_PROMPT);
+ }
+ }
+
+ /**
+ * Triggered when a suggestion is selected
+ *
+ * @param suggestion
+ * The suggestion that just got selected.
+ */
+ public void onSuggestionSelected(FilterSelectSuggestion suggestion) {
+ if (enableDebug) {
+ debug("VFS: onSuggestionSelected(" + suggestion.caption + ": "
+ + suggestion.key + ")");
+ }
+ dataReceivedHandler.cancelPendingPostFiltering();
+
+ currentSuggestion = suggestion;
+ String newKey;
+ if (suggestion.key.equals("")) {
+ // "nullselection"
+ newKey = "";
+ } else {
+ // normal selection
+ newKey = suggestion.getOptionKey();
+ }
+
+ String text = suggestion.getReplacementString();
+ if ("".equals(newKey) && !focused) {
+ setPromptingOn();
+ } else {
+ setPromptingOff(text);
+ }
+ setSelectedItemIcon(suggestion.getIconUri());
+
+ if (!(newKey.equals(selectedOptionKey)
+ || ("".equals(newKey) && selectedOptionKey == null))) {
+ selectedOptionKey = newKey;
+ connector.sendSelection(selectedOptionKey);
+
+ // currentPage = -1; // forget the page
+ }
+
+ if (getSelectedCaption() != null && newKey.equals("")) {
+ // In scrollToPage(false) mode selecting null seems to be broken
+ // if current selection is not on first page. The above clause is so
+ // hard to interpret that new clause added here :-(
+ selectedOptionKey = newKey;
+ explicitSelectedCaption = null;
+ connector.sendSelection(selectedOptionKey);
+ }
+
+ suggestionPopup.hide();
+ }
+
+ /**
+ * Sets the icon URI of the selected item. The icon is shown on the left
+ * side of the item caption text. Set the URI to null to remove the icon.
+ *
+ * @param iconUri
+ * The URI of the icon
+ */
+ public void setSelectedItemIcon(String iconUri) {
+
+ if (iconUri == null || iconUri.length() == 0) {
+ if (selectedItemIcon != null) {
+ panel.remove(selectedItemIcon);
+ selectedItemIcon = null;
+ afterSelectedItemIconChange();
+ }
+ } else {
+ if (selectedItemIcon != null) {
+ panel.remove(selectedItemIcon);
+ }
+ selectedItemIcon = new IconWidget(
+ connector.getConnection().getIcon(iconUri));
+ // Older IE versions don't scale icon correctly if DOM
+ // contains height and width attributes.
+ selectedItemIcon.getElement().removeAttribute("height");
+ selectedItemIcon.getElement().removeAttribute("width");
+ selectedItemIcon.addDomHandler(new LoadHandler() {
+ @Override
+ public void onLoad(LoadEvent event) {
+ afterSelectedItemIconChange();
+ }
+ }, LoadEvent.getType());
+ panel.insert(selectedItemIcon, 0);
+ afterSelectedItemIconChange();
+ }
+ }
+
+ private void afterSelectedItemIconChange() {
+ if (BrowserInfo.get().isWebkit()) {
+ // Some browsers need a nudge to reposition the text field
+ forceReflow();
+ }
+ updateRootWidth();
+ if (selectedItemIcon != null) {
+ updateSelectedIconPosition();
+ }
+ }
+
+ /**
+ * Perform selection based on a message from the server.
+ *
+ * This method is called when the server gave a non-empty selected item key,
+ * whereas null selection is handled by {@link #resetSelection()} and the
+ * special case where the selected item is not on the current page is
+ * handled separately by the caller.
+ *
+ * @param selectedKey
+ * non-empty selected item key
+ * @param oldSuggestionTextMatchTheOldSelection
+ * true if the suggestion box text matched the previous selection
+ * before the message from the server updating the selection
+ * @param updatePromptAndSelectionIfMatchFound
+ */
+ private void performSelection(String selectedKey,
+ boolean oldSuggestionTextMatchTheOldSelection,
+ boolean updatePromptAndSelectionIfMatchFound) {
+ // some item selected
+ for (FilterSelectSuggestion suggestion : currentSuggestions) {
+ String suggestionKey = suggestion.getOptionKey();
+ if (!suggestionKey.equals(selectedKey)) {
+ continue;
+ }
+ // at this point, suggestion key matches the new selection key
+ if (updatePromptAndSelectionIfMatchFound) {
+ if (!suggestionKey.equals(selectedOptionKey)
+ || suggestion.getReplacementString()
+ .equals(tb.getText())
+ || oldSuggestionTextMatchTheOldSelection) {
+ // Update text field if we've got a new
+ // selection
+ // Also update if we've got the same text to
+ // retain old text selection behavior
+ // OR if selected item caption is changed.
+ setPromptingOff(suggestion.getReplacementString());
+ selectedOptionKey = suggestionKey;
+ }
+ }
+ currentSuggestion = suggestion;
+ setSelectedItemIcon(suggestion.getIconUri());
+ // only a single item can be selected
+ break;
+ }
+ }
+
+ /**
+ * Reset the selection of the combo box to an empty string if focused, the
+ * input prompt if not.
+ *
+ * This method also clears the current suggestion and the selected option
+ * key.
+ */
+ private void resetSelection(boolean useSelectedCaption) {
+ // select nulled
+ if (!focused) {
+ // TODO it is unclear whether this is really needed anymore -
+ // client.updateComponent used to overwrite all styles so we had to
+ // set them again
+ setPromptingOff("");
+ if (enabled && !readonly) {
+ setPromptingOn();
+ }
+ } else {
+ // we have focus in field, prompting can't be set on,
+ // instead just clear the input if the value has changed from
+ // something else to null
+ if (selectedOptionKey != null
+ || (allowNewItem && !tb.getValue().isEmpty())) {
+ if (useSelectedCaption && getSelectedCaption() != null) {
+ tb.setValue(getSelectedCaption());
+ tb.selectAll();
+ } else {
+ tb.setValue("");
+ }
+ }
+ }
+ currentSuggestion = null; // #13217
+ setSelectedItemIcon(null);
+ selectedOptionKey = null;
+ }
+
+ private void forceReflow() {
+ WidgetUtil.setStyleTemporarily(tb.getElement(), "zoom", "1");
+ }
+
+ /**
+ * Positions the icon vertically in the middle. Should be called after the
+ * icon has loaded
+ */
+ private void updateSelectedIconPosition() {
+ // Position icon vertically to middle
+ int availableHeight = 0;
+ availableHeight = getOffsetHeight();
+
+ int iconHeight = WidgetUtil.getRequiredHeight(selectedItemIcon);
+ int marginTop = (availableHeight - iconHeight) / 2;
+ selectedItemIcon.getElement().getStyle().setMarginTop(marginTop,
+ Unit.PX);
+ }
+
+ private static Set<Integer> navigationKeyCodes = new HashSet<Integer>();
+ static {
+ navigationKeyCodes.add(KeyCodes.KEY_DOWN);
+ navigationKeyCodes.add(KeyCodes.KEY_UP);
+ navigationKeyCodes.add(KeyCodes.KEY_PAGEDOWN);
+ navigationKeyCodes.add(KeyCodes.KEY_PAGEUP);
+ navigationKeyCodes.add(KeyCodes.KEY_ENTER);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.google.gwt.event.dom.client.KeyDownHandler#onKeyDown(com.google.gwt
+ * .event.dom.client.KeyDownEvent)
+ */
+
+ @Override
+ public void onKeyDown(KeyDownEvent event) {
+ if (enabled && !readonly) {
+ int keyCode = event.getNativeKeyCode();
+
+ if (enableDebug) {
+ debug("VFS: key down: " + keyCode);
+ }
+ if (dataReceivedHandler.isWaitingForFilteringResponse()
+ && navigationKeyCodes.contains(keyCode)) {
+ /*
+ * Keyboard navigation events should not be handled while we are
+ * waiting for a response. This avoids flickering, disappearing
+ * items, wrongly interpreted responses and more.
+ */
+ if (enableDebug) {
+ debug("Ignoring " + keyCode
+ + " because we are waiting for a filtering response");
+ }
+ DOM.eventPreventDefault(DOM.eventGetCurrentEvent());
+ event.stopPropagation();
+ return;
+ }
+
+ if (suggestionPopup.isAttached()) {
+ if (enableDebug) {
+ debug("Keycode " + keyCode + " target is popup");
+ }
+ popupKeyDown(event);
+ } else {
+ if (enableDebug) {
+ debug("Keycode " + keyCode + " target is text field");
+ }
+ inputFieldKeyDown(event);
+ }
+ }
+ }
+
+ private void debug(String string) {
+ if (enableDebug) {
+ VConsole.error(string);
+ }
+ }
+
+ /**
+ * Triggered when a key is pressed in the text box
+ *
+ * @param event
+ * The KeyDownEvent
+ */
+ private void inputFieldKeyDown(KeyDownEvent event) {
+ if (enableDebug) {
+ debug("VFS: inputFieldKeyDown(" + event.getNativeKeyCode() + ")");
+ }
+ switch (event.getNativeKeyCode()) {
+ case KeyCodes.KEY_DOWN:
+ case KeyCodes.KEY_UP:
+ case KeyCodes.KEY_PAGEDOWN:
+ case KeyCodes.KEY_PAGEUP:
+ // open popup as from gadget
+ filterOptions(-1, "");
+ lastFilter = "";
+ tb.selectAll();
+ break;
+ case KeyCodes.KEY_ENTER:
+ /*
+ * This only handles the case when new items is allowed, a text is
+ * entered, the popup opener button is clicked to close the popup
+ * and enter is then pressed (see #7560).
+ */
+ if (!allowNewItem) {
+ return;
+ }
+
+ if (currentSuggestion != null && tb.getText()
+ .equals(currentSuggestion.getReplacementString())) {
+ // Retain behavior from #6686 by returning without stopping
+ // propagation if there's nothing to do
+ return;
+ }
+ suggestionPopup.menu.doSelectedItemAction();
+
+ event.stopPropagation();
+ break;
+ }
+
+ }
+
+ /**
+ * Triggered when a key was pressed in the suggestion popup.
+ *
+ * @param event
+ * The KeyDownEvent of the key
+ */
+ private void popupKeyDown(KeyDownEvent event) {
+ if (enableDebug) {
+ debug("VFS: popupKeyDown(" + event.getNativeKeyCode() + ")");
+ }
+ // Propagation of handled events is stopped so other handlers such as
+ // shortcut key handlers do not also handle the same events.
+ switch (event.getNativeKeyCode()) {
+ case KeyCodes.KEY_DOWN:
+ suggestionPopup.selectNextItem();
+
+ DOM.eventPreventDefault(DOM.eventGetCurrentEvent());
+ event.stopPropagation();
+ break;
+ case KeyCodes.KEY_UP:
+ suggestionPopup.selectPrevItem();
+
+ DOM.eventPreventDefault(DOM.eventGetCurrentEvent());
+ event.stopPropagation();
+ break;
+ case KeyCodes.KEY_PAGEDOWN:
+ selectNextPage();
+ event.stopPropagation();
+ break;
+ case KeyCodes.KEY_PAGEUP:
+ selectPrevPage();
+ event.stopPropagation();
+ break;
+ case KeyCodes.KEY_ESCAPE:
+ reset();
+ DOM.eventPreventDefault(DOM.eventGetCurrentEvent());
+ event.stopPropagation();
+ break;
+ case KeyCodes.KEY_TAB:
+ case KeyCodes.KEY_ENTER:
+
+ if (!allowNewItem) {
+ int selected = suggestionPopup.menu.getSelectedIndex();
+ if (selected != -1) {
+ onSuggestionSelected(currentSuggestions.get(selected));
+ } else {
+ // The way VFilterSelect is done, it handles enter and tab
+ // in exactly the same way so we close the popup in both
+ // cases even though we could leave it open when pressing
+ // enter
+ suggestionPopup.hide();
+ }
+ } else {
+ // Handle addition of new items.
+ suggestionPopup.menu.doSelectedItemAction();
+ }
+
+ event.stopPropagation();
+ break;
+ }
+
+ }
+
+ /*
+ * Show the prev page.
+ */
+ private void selectPrevPage() {
+ if (currentPage > 0) {
+ filterOptions(currentPage - 1, lastFilter);
+ dataReceivedHandler.setNavigationCallback(new Runnable() {
+ @Override
+ public void run() {
+ suggestionPopup.selectLastItem();
+ }
+ });
+ }
+ }
+
+ /*
+ * Show the next page.
+ */
+ private void selectNextPage() {
+ if (hasNextPage()) {
+ filterOptions(currentPage + 1, lastFilter);
+ dataReceivedHandler.setNavigationCallback(new Runnable() {
+ @Override
+ public void run() {
+ suggestionPopup.selectFirstItem();
+ }
+ });
+ }
+ }
+
+ /**
+ * Triggered when a key was depressed
+ *
+ * @param event
+ * The KeyUpEvent of the key depressed
+ */
+
+ @Override
+ public void onKeyUp(KeyUpEvent event) {
+ if (enableDebug) {
+ debug("VFS: onKeyUp(" + event.getNativeKeyCode() + ")");
+ }
+ if (enabled && !readonly) {
+ switch (event.getNativeKeyCode()) {
+ case KeyCodes.KEY_ENTER:
+ case KeyCodes.KEY_TAB:
+ case KeyCodes.KEY_SHIFT:
+ case KeyCodes.KEY_CTRL:
+ case KeyCodes.KEY_ALT:
+ case KeyCodes.KEY_DOWN:
+ case KeyCodes.KEY_UP:
+ case KeyCodes.KEY_PAGEDOWN:
+ case KeyCodes.KEY_PAGEUP:
+ case KeyCodes.KEY_ESCAPE:
+ // NOP
+ break;
+ default:
+ if (textInputEnabled) {
+ // when filtering, we always want to see the results on the
+ // first page first.
+ filterOptions(0);
+ }
+ break;
+ }
+ }
+ }
+
+ /**
+ * Resets the Select to its initial state
+ */
+ private void reset() {
+ debug("VFS: reset()");
+ if (currentSuggestion != null) {
+ String text = currentSuggestion.getReplacementString();
+ setPromptingOff(text);
+ setSelectedItemIcon(currentSuggestion.getIconUri());
+
+ selectedOptionKey = currentSuggestion.key;
+
+ } else {
+ if (focused || readonly || !enabled) {
+ setPromptingOff("");
+ } else {
+ setPromptingOn();
+ }
+ setSelectedItemIcon(null);
+
+ selectedOptionKey = null;
+ }
+
+ lastFilter = "";
+ suggestionPopup.hide();
+ }
+
+ /**
+ * Listener for popupopener
+ */
+
+ @Override
+ public void onClick(ClickEvent event) {
+ debug("VFS: onClick()");
+ if (textInputEnabled && event.getNativeEvent().getEventTarget()
+ .cast() == tb.getElement()) {
+ // Don't process clicks on the text field if text input is enabled
+ return;
+ }
+ if (enabled && !readonly) {
+ // ask suggestionPopup if it was just closed, we are using GWT
+ // Popup's auto close feature
+ if (!suggestionPopup.isJustClosed()) {
+ filterOptions(-1, "");
+ dataReceivedHandler.popupOpenerClicked();
+ lastFilter = "";
+ }
+ DOM.eventPreventDefault(DOM.eventGetCurrentEvent());
+ focus();
+ tb.selectAll();
+ }
+ }
+
+ /**
+ * Update minimum width for FilterSelect textarea based on input prompt and
+ * suggestions.
+ * <p>
+ * For internal use only. May be removed or replaced in the future.
+ */
+ public void updateSuggestionPopupMinWidth() {
+ // used only to calculate minimum width
+ String captions = WidgetUtil.escapeHTML(inputPrompt);
+
+ for (FilterSelectSuggestion suggestion : currentSuggestions) {
+ // Collect captions so we can calculate minimum width for
+ // textarea
+ if (captions.length() > 0) {
+ captions += "|";
+ }
+ captions += WidgetUtil
+ .escapeHTML(suggestion.getReplacementString());
+ }
+
+ // Calculate minimum textarea width
+ suggestionPopupMinWidth = minWidth(captions);
+ }
+
+ /**
+ * Calculate minimum width for FilterSelect textarea.
+ * <p>
+ * For internal use only. May be removed or replaced in the future.
+ */
+ public native int minWidth(String captions)
+ /*-{
+ if(!captions || captions.length <= 0)
+ return 0;
+ captions = captions.split("|");
+ var d = $wnd.document.createElement("div");
+ var html = "";
+ for(var i=0; i < captions.length; i++) {
+ html += "<div>" + captions[i] + "</div>";
+ // TODO apply same CSS classname as in suggestionmenu
+ }
+ d.style.position = "absolute";
+ d.style.top = "0";
+ d.style.left = "0";
+ d.style.visibility = "hidden";
+ d.innerHTML = html;
+ $wnd.document.body.appendChild(d);
+ var w = d.offsetWidth;
+ $wnd.document.body.removeChild(d);
+ return w;
+ }-*/;
+
+ /**
+ * A flag which prevents a focus event from taking place
+ */
+ boolean iePreventNextFocus = false;
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.google.gwt.event.dom.client.FocusHandler#onFocus(com.google.gwt.event
+ * .dom.client.FocusEvent)
+ */
+
+ @Override
+ public void onFocus(FocusEvent event) {
+ debug("VFS: onFocus()");
+
+ /*
+ * When we disable a blur event in ie we need to refocus the textfield.
+ * This will cause a focus event we do not want to process, so in that
+ * case we just ignore it.
+ */
+ if (BrowserInfo.get().isIE() && iePreventNextFocus) {
+ iePreventNextFocus = false;
+ return;
+ }
+
+ focused = true;
+ if (prompting && !readonly) {
+ setPromptingOff("");
+ }
+ addStyleDependentName("focus");
+
+ connector.sendFocusEvent();
+
+ connector.getConnection().getVTooltip()
+ .showAssistive(connector.getTooltipInfo(getElement()));
+ }
+
+ /**
+ * A flag which cancels the blur event and sets the focus back to the
+ * textfield if the Browser is IE
+ */
+ boolean preventNextBlurEventInIE = false;
+
+ private String explicitSelectedCaption;
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.google.gwt.event.dom.client.BlurHandler#onBlur(com.google.gwt.event
+ * .dom.client.BlurEvent)
+ */
+
+ @Override
+ public void onBlur(BlurEvent event) {
+ debug("VFS: onBlur()");
+
+ if (BrowserInfo.get().isIE() && preventNextBlurEventInIE) {
+ /*
+ * Clicking in the suggestion popup or on the popup button in IE
+ * causes a blur event to be sent for the field. In other browsers
+ * this is prevented by canceling/preventing default behavior for
+ * the focus event, in IE we handle it here by refocusing the text
+ * field and ignoring the resulting focus event for the textfield
+ * (in onFocus).
+ */
+ preventNextBlurEventInIE = false;
+
+ Element focusedElement = WidgetUtil.getFocusedElement();
+ if (getElement().isOrHasChild(focusedElement) || suggestionPopup
+ .getElement().isOrHasChild(focusedElement)) {
+
+ // IF the suggestion popup or another part of the VFilterSelect
+ // was focused, move the focus back to the textfield and prevent
+ // the triggered focus event (in onFocus).
+ iePreventNextFocus = true;
+ tb.setFocus(true);
+ return;
+ }
+ }
+
+ focused = false;
+ if (!readonly) {
+ if (selectedOptionKey == null) {
+ if (explicitSelectedCaption != null) {
+ setPromptingOff(explicitSelectedCaption);
+ } else {
+ setPromptingOn();
+ }
+ } else if (currentSuggestion != null) {
+ setPromptingOff(currentSuggestion.caption);
+ }
+ }
+ removeStyleDependentName("focus");
+
+ connector.sendBlurEvent();
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.client.Focusable#focus()
+ */
+
+ @Override
+ public void focus() {
+ debug("VFS: focus()");
+ focused = true;
+ if (prompting && !readonly) {
+ setPromptingOff("");
+ }
+ tb.setFocus(true);
+ }
+
+ /**
+ * Calculates the width of the select if the select has undefined width.
+ * Should be called when the width changes or when the icon changes.
+ * <p>
+ * For internal use only. May be removed or replaced in the future.
+ */
+ public void updateRootWidth() {
+ if (connector.isUndefinedWidth()) {
+
+ /*
+ * When the select has a undefined with we need to check that we are
+ * only setting the text box width relative to the first page width
+ * of the items. If this is not done the text box width will change
+ * when the popup is used to view longer items than the text box is
+ * wide.
+ */
+ int w = WidgetUtil.getRequiredWidth(this);
+
+ if ((!initDone || currentPage + 1 < 0)
+ && suggestionPopupMinWidth > w) {
+ /*
+ * We want to compensate for the paddings just to preserve the
+ * exact size as in Vaadin 6.x, but we get here before
+ * MeasuredSize has been initialized.
+ * Util.measureHorizontalPaddingAndBorder does not work with
+ * border-box, so we must do this the hard way.
+ */
+ Style style = getElement().getStyle();
+ String originalPadding = style.getPadding();
+ String originalBorder = style.getBorderWidth();
+ style.setPaddingLeft(0, Unit.PX);
+ style.setBorderWidth(0, Unit.PX);
+ style.setProperty("padding", originalPadding);
+ style.setProperty("borderWidth", originalBorder);
+
+ // Use util.getRequiredWidth instead of getOffsetWidth here
+
+ int iconWidth = selectedItemIcon == null ? 0
+ : WidgetUtil.getRequiredWidth(selectedItemIcon);
+ int buttonWidth = popupOpener == null ? 0
+ : WidgetUtil.getRequiredWidth(popupOpener);
+
+ /*
+ * Instead of setting the width of the wrapper, set the width of
+ * the combobox. Subtract the width of the icon and the
+ * popupopener
+ */
+
+ tb.setWidth((suggestionPopupMinWidth - iconWidth - buttonWidth)
+ + "px");
+
+ }
+
+ /*
+ * Lock the textbox width to its current value if it's not already
+ * locked
+ */
+ if (!tb.getElement().getStyle().getWidth().endsWith("px")) {
+ int iconWidth = selectedItemIcon == null ? 0
+ : selectedItemIcon.getOffsetWidth();
+ tb.setWidth((tb.getOffsetWidth() - iconWidth) + "px");
+ }
+ }
+ }
+
+ /**
+ * Get the width of the select in pixels where the text area and icon has
+ * been included.
+ *
+ * @return The width in pixels
+ */
+ private int getMainWidth() {
+ return getOffsetWidth();
+ }
+
+ @Override
+ public void setWidth(String width) {
+ super.setWidth(width);
+ if (width.length() != 0) {
+ tb.setWidth("100%");
+ }
+ }
+
+ /**
+ * Handles special behavior of the mouse down event
+ *
+ * @param event
+ */
+ private void handleMouseDownEvent(Event event) {
+ /*
+ * Prevent the keyboard focus from leaving the textfield by preventing
+ * the default behaviour of the browser. Fixes #4285.
+ */
+ if (event.getTypeInt() == Event.ONMOUSEDOWN) {
+ event.preventDefault();
+ event.stopPropagation();
+
+ /*
+ * In IE the above wont work, the blur event will still trigger. So,
+ * we set a flag here to prevent the next blur event from happening.
+ * This is not needed if do not already have focus, in that case
+ * there will not be any blur event and we should not cancel the
+ * next blur.
+ */
+ if (BrowserInfo.get().isIE() && focused) {
+ preventNextBlurEventInIE = true;
+ debug("VFS: Going to prevent next blur event on IE");
+ }
+ }
+ }
+
+ @Override
+ protected void onDetach() {
+ super.onDetach();
+ suggestionPopup.hide();
+ }
+
+ @Override
+ public com.google.gwt.user.client.Element getSubPartElement(
+ String subPart) {
+ String[] parts = subPart.split("/");
+ if ("textbox".equals(parts[0])) {
+ return tb.getElement();
+ } else if ("button".equals(parts[0])) {
+ return popupOpener.getElement();
+ } else if ("popup".equals(parts[0]) && suggestionPopup.isAttached()) {
+ if (parts.length == 2) {
+ return suggestionPopup.menu.getSubPartElement(parts[1]);
+ }
+ return suggestionPopup.getElement();
+ }
+ return null;
+ }
+
+ @Override
+ public String getSubPartName(
+ com.google.gwt.user.client.Element subElement) {
+ if (tb.getElement().isOrHasChild(subElement)) {
+ return "textbox";
+ } else if (popupOpener.getElement().isOrHasChild(subElement)) {
+ return "button";
+ } else if (suggestionPopup.getElement().isOrHasChild(subElement)) {
+ return "popup";
+ }
+ return null;
+ }
+
+ @Override
+ public void setAriaRequired(boolean required) {
+ AriaHelper.handleInputRequired(tb, required);
+ }
+
+ @Override
+ public void setAriaInvalid(boolean invalid) {
+ AriaHelper.handleInputInvalid(tb, invalid);
+ }
+
+ @Override
+ public void bindAriaCaption(
+ com.google.gwt.user.client.Element captionElement) {
+ AriaHelper.bindCaption(tb, captionElement);
+ }
+
+ @Override
+ public boolean isWorkPending() {
+ return dataReceivedHandler.isWaitingForFilteringResponse()
+ || suggestionPopup.lazyPageScroller.isRunning();
+ }
+
+ /**
+ * Sets the caption of selected item, if "scroll to page" is disabled. This
+ * method is meant for internal use and may change in future versions.
+ *
+ * @since 7.7
+ * @param selectedCaption
+ * the caption of selected item
+ */
+ public void setSelectedCaption(String selectedCaption) {
+ explicitSelectedCaption = selectedCaption;
+ if (selectedCaption != null) {
+ setPromptingOff(selectedCaption);
+ }
+ }
+
+ /**
+ * This method is meant for internal use and may change in future versions.
+ *
+ * @since 7.7
+ * @return the caption of selected item, if "scroll to page" is disabled
+ */
+ public String getSelectedCaption() {
+ return explicitSelectedCaption;
+ }
+
+ /**
+ * Returns a handler receiving notifications from the connector about
+ * communications.
+ *
+ * @return the dataReceivedHandler
+ */
+ public DataReceivedHandler getDataReceivedHandler() {
+ return dataReceivedHandler;
+ }
+
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/client/ui/VTree.java b/compatibility-client/src/main/java/com/vaadin/client/ui/VTree.java
new file mode 100644
index 0000000000..f9101a5e30
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/client/ui/VTree.java
@@ -0,0 +1,2267 @@
+/*
+ * Copyright 2000-2016 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.vaadin.client.ui;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
+
+import com.google.gwt.aria.client.ExpandedValue;
+import com.google.gwt.aria.client.Id;
+import com.google.gwt.aria.client.Roles;
+import com.google.gwt.aria.client.SelectedValue;
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.core.client.Scheduler;
+import com.google.gwt.core.client.Scheduler.ScheduledCommand;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.dom.client.NativeEvent;
+import com.google.gwt.dom.client.Node;
+import com.google.gwt.event.dom.client.BlurEvent;
+import com.google.gwt.event.dom.client.BlurHandler;
+import com.google.gwt.event.dom.client.ContextMenuEvent;
+import com.google.gwt.event.dom.client.ContextMenuHandler;
+import com.google.gwt.event.dom.client.FocusEvent;
+import com.google.gwt.event.dom.client.FocusHandler;
+import com.google.gwt.event.dom.client.KeyCodes;
+import com.google.gwt.event.dom.client.KeyDownEvent;
+import com.google.gwt.event.dom.client.KeyDownHandler;
+import com.google.gwt.event.dom.client.KeyPressEvent;
+import com.google.gwt.event.dom.client.KeyPressHandler;
+import com.google.gwt.user.client.Command;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.Event;
+import com.google.gwt.user.client.Window;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.SimplePanel;
+import com.google.gwt.user.client.ui.UIObject;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.client.ApplicationConnection;
+import com.vaadin.client.BrowserInfo;
+import com.vaadin.client.ComponentConnector;
+import com.vaadin.client.ConnectorMap;
+import com.vaadin.client.MouseEventDetailsBuilder;
+import com.vaadin.client.UIDL;
+import com.vaadin.client.Util;
+import com.vaadin.client.WidgetUtil;
+import com.vaadin.client.ui.aria.AriaHelper;
+import com.vaadin.client.ui.aria.HandlesAriaCaption;
+import com.vaadin.client.ui.dd.DDUtil;
+import com.vaadin.client.ui.dd.VAbstractDropHandler;
+import com.vaadin.client.ui.dd.VAcceptCallback;
+import com.vaadin.client.ui.dd.VDragAndDropManager;
+import com.vaadin.client.ui.dd.VDragEvent;
+import com.vaadin.client.ui.dd.VDropHandler;
+import com.vaadin.client.ui.dd.VHasDropHandler;
+import com.vaadin.client.ui.dd.VTransferable;
+import com.vaadin.client.ui.tree.TreeConnector;
+import com.vaadin.shared.MouseEventDetails;
+import com.vaadin.shared.MouseEventDetails.MouseButton;
+import com.vaadin.shared.ui.MultiSelectMode;
+import com.vaadin.shared.ui.dd.VerticalDropLocation;
+import com.vaadin.shared.ui.tree.TreeConstants;
+
+/**
+ *
+ */
+public class VTree extends FocusElementPanel
+ implements VHasDropHandler, FocusHandler, BlurHandler, KeyPressHandler,
+ KeyDownHandler, SubPartAware, ActionOwner, HandlesAriaCaption {
+ private String lastNodeKey = "";
+
+ public static final String CLASSNAME = "v-tree";
+
+ /**
+ * @deprecated As of 7.0, use {@link MultiSelectMode#DEFAULT} instead.
+ */
+ @Deprecated
+ public static final MultiSelectMode MULTISELECT_MODE_DEFAULT = MultiSelectMode.DEFAULT;
+
+ /**
+ * @deprecated As of 7.0, use {@link MultiSelectMode#SIMPLE} instead.
+ */
+ @Deprecated
+ public static final MultiSelectMode MULTISELECT_MODE_SIMPLE = MultiSelectMode.SIMPLE;
+
+ private static final int CHARCODE_SPACE = 32;
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public final FlowPanel body = new FlowPanel();
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public Set<String> selectedIds = new HashSet<String>();
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public ApplicationConnection client;
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public String paintableId;
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public boolean selectable;
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public boolean isMultiselect;
+
+ private String currentMouseOverKey;
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public TreeNode lastSelection;
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public TreeNode focusedNode;
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public MultiSelectMode multiSelectMode = MultiSelectMode.DEFAULT;
+
+ private final HashMap<String, TreeNode> keyToNode = new HashMap<String, TreeNode>();
+
+ /**
+ * This map contains captions and icon urls for actions like: * "33_c" ->
+ * "Edit" * "33_i" -> "http://dom.com/edit.png"
+ */
+ private final HashMap<String, String> actionMap = new HashMap<String, String>();
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public boolean immediate;
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public boolean isNullSelectionAllowed = true;
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public boolean isHtmlContentAllowed = false;
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public boolean disabled = false;
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public boolean readonly;
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public boolean rendering;
+
+ private VAbstractDropHandler dropHandler;
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public int dragMode;
+
+ private boolean selectionHasChanged = false;
+
+ /*
+ * to fix #14388. The cause of defect #14388: event 'clickEvent' is sent to
+ * server before updating of "selected" variable, but should be sent after
+ * it
+ */
+ private boolean clickEventPending = false;
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public String[] bodyActionKeys;
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public TreeConnector connector;
+
+ public VLazyExecutor iconLoaded = new VLazyExecutor(50,
+ new ScheduledCommand() {
+
+ @Override
+ public void execute() {
+ doLayout();
+ }
+
+ });
+
+ public VTree() {
+ super();
+ setStyleName(CLASSNAME);
+
+ Roles.getTreeRole().set(body.getElement());
+ add(body);
+
+ addFocusHandler(this);
+ addBlurHandler(this);
+
+ /*
+ * Listen to context menu events on the empty space in the tree
+ */
+ sinkEvents(Event.ONCONTEXTMENU);
+ addDomHandler(new ContextMenuHandler() {
+ @Override
+ public void onContextMenu(ContextMenuEvent event) {
+ handleBodyContextMenu(event);
+ }
+ }, ContextMenuEvent.getType());
+
+ /*
+ * Firefox auto-repeat works correctly only if we use a key press
+ * handler, other browsers handle it correctly when using a key down
+ * handler
+ */
+ if (BrowserInfo.get().isGecko()) {
+ addKeyPressHandler(this);
+ } else {
+ addKeyDownHandler(this);
+ }
+
+ /*
+ * We need to use the sinkEvents method to catch the keyUp events so we
+ * can cache a single shift. KeyUpHandler cannot do this. At the same
+ * time we catch the mouse down and up events so we can apply the text
+ * selection patch in IE
+ */
+ sinkEvents(Event.ONMOUSEDOWN | Event.ONMOUSEUP | Event.ONKEYUP);
+
+ /*
+ * Re-set the tab index to make sure that the FocusElementPanel's
+ * (super) focus element gets the tab index and not the element
+ * containing the tree.
+ */
+ setTabIndex(0);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.google.gwt.user.client.ui.Widget#onBrowserEvent(com.google.gwt.user
+ * .client.Event)
+ */
+ @Override
+ public void onBrowserEvent(Event event) {
+ super.onBrowserEvent(event);
+ if (event.getTypeInt() == Event.ONMOUSEDOWN) {
+ // Prevent default text selection in IE
+ if (BrowserInfo.get().isIE()) {
+ ((Element) event.getEventTarget().cast()).setPropertyJSO(
+ "onselectstart", applyDisableTextSelectionIEHack());
+ }
+ } else if (event.getTypeInt() == Event.ONMOUSEUP) {
+ // Remove IE text selection hack
+ if (BrowserInfo.get().isIE()) {
+ ((Element) event.getEventTarget().cast())
+ .setPropertyJSO("onselectstart", null);
+ }
+ } else if (event.getTypeInt() == Event.ONKEYUP) {
+ if (selectionHasChanged) {
+ if (event.getKeyCode() == getNavigationDownKey()
+ && !event.getShiftKey()) {
+ sendSelectionToServer();
+ event.preventDefault();
+ } else if (event.getKeyCode() == getNavigationUpKey()
+ && !event.getShiftKey()) {
+ sendSelectionToServer();
+ event.preventDefault();
+ } else if (event.getKeyCode() == KeyCodes.KEY_SHIFT) {
+ sendSelectionToServer();
+ event.preventDefault();
+ } else if (event.getKeyCode() == getNavigationSelectKey()) {
+ sendSelectionToServer();
+ event.preventDefault();
+ }
+ }
+ }
+ }
+
+ public String getActionCaption(String actionKey) {
+ return actionMap.get(actionKey + "_c");
+ }
+
+ public String getActionIcon(String actionKey) {
+ return actionMap.get(actionKey + "_i");
+ }
+
+ /**
+ * Returns the first root node of the tree or null if there are no root
+ * nodes.
+ *
+ * @return The first root {@link TreeNode}
+ */
+ protected TreeNode getFirstRootNode() {
+ if (body.getWidgetCount() == 0) {
+ return null;
+ }
+ return (TreeNode) body.getWidget(0);
+ }
+
+ /**
+ * Returns the last root node of the tree or null if there are no root
+ * nodes.
+ *
+ * @return The last root {@link TreeNode}
+ */
+ protected TreeNode getLastRootNode() {
+ if (body.getWidgetCount() == 0) {
+ return null;
+ }
+ return (TreeNode) body.getWidget(body.getWidgetCount() - 1);
+ }
+
+ /**
+ * Returns a list of all root nodes in the Tree in the order they appear in
+ * the tree.
+ *
+ * @return A list of all root {@link TreeNode}s.
+ */
+ protected List<TreeNode> getRootNodes() {
+ ArrayList<TreeNode> rootNodes = new ArrayList<TreeNode>();
+ for (int i = 0; i < body.getWidgetCount(); i++) {
+ rootNodes.add((TreeNode) body.getWidget(i));
+ }
+ return rootNodes;
+ }
+
+ private void updateTreeRelatedDragData(VDragEvent drag) {
+
+ currentMouseOverKey = findCurrentMouseOverKey(drag.getElementOver());
+
+ drag.getDropDetails().put("itemIdOver", currentMouseOverKey);
+ if (currentMouseOverKey != null) {
+ TreeNode treeNode = getNodeByKey(currentMouseOverKey);
+ VerticalDropLocation detail = treeNode
+ .getDropDetail(drag.getCurrentGwtEvent());
+ Boolean overTreeNode = null;
+ if (treeNode != null && !treeNode.isLeaf()
+ && detail == VerticalDropLocation.MIDDLE) {
+ overTreeNode = true;
+ }
+ drag.getDropDetails().put("itemIdOverIsNode", overTreeNode);
+ drag.getDropDetails().put("detail", detail);
+ } else {
+ drag.getDropDetails().put("itemIdOverIsNode", null);
+ drag.getDropDetails().put("detail", null);
+ }
+
+ }
+
+ private String findCurrentMouseOverKey(Element elementOver) {
+ TreeNode treeNode = WidgetUtil.findWidget(elementOver, TreeNode.class);
+ return treeNode == null ? null : treeNode.key;
+ }
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public void updateDropHandler(UIDL childUidl) {
+ if (dropHandler == null) {
+ dropHandler = new VAbstractDropHandler() {
+
+ @Override
+ public void dragEnter(VDragEvent drag) {
+ }
+
+ @Override
+ protected void dragAccepted(final VDragEvent drag) {
+
+ }
+
+ @Override
+ public void dragOver(final VDragEvent currentDrag) {
+ final Object oldIdOver = currentDrag.getDropDetails()
+ .get("itemIdOver");
+ final VerticalDropLocation oldDetail = (VerticalDropLocation) currentDrag
+ .getDropDetails().get("detail");
+
+ updateTreeRelatedDragData(currentDrag);
+ final VerticalDropLocation detail = (VerticalDropLocation) currentDrag
+ .getDropDetails().get("detail");
+ boolean nodeHasChanged = (currentMouseOverKey != null
+ && currentMouseOverKey != oldIdOver)
+ || (currentMouseOverKey == null
+ && oldIdOver != null);
+ boolean detailHasChanded = (detail != null
+ && detail != oldDetail)
+ || (detail == null && oldDetail != null);
+
+ if (nodeHasChanged || detailHasChanded) {
+ final String newKey = currentMouseOverKey;
+ TreeNode treeNode = keyToNode.get(oldIdOver);
+ if (treeNode != null) {
+ // clear old styles
+ treeNode.emphasis(null);
+ }
+ if (newKey != null) {
+ validate(new VAcceptCallback() {
+ @Override
+ public void accepted(VDragEvent event) {
+ VerticalDropLocation curDetail = (VerticalDropLocation) event
+ .getDropDetails().get("detail");
+ if (curDetail == detail && newKey
+ .equals(currentMouseOverKey)) {
+ getNodeByKey(newKey).emphasis(detail);
+ }
+ /*
+ * Else drag is already on a different
+ * node-detail pair, new criteria check is
+ * going on
+ */
+ }
+ }, currentDrag);
+
+ }
+ }
+
+ }
+
+ @Override
+ public void dragLeave(VDragEvent drag) {
+ cleanUp();
+ }
+
+ private void cleanUp() {
+ if (currentMouseOverKey != null) {
+ getNodeByKey(currentMouseOverKey).emphasis(null);
+ currentMouseOverKey = null;
+ }
+ }
+
+ @Override
+ public boolean drop(VDragEvent drag) {
+ cleanUp();
+ return super.drop(drag);
+ }
+
+ @Override
+ public ComponentConnector getConnector() {
+ return ConnectorMap.get(client).getConnector(VTree.this);
+ }
+
+ @Override
+ public ApplicationConnection getApplicationConnection() {
+ return client;
+ }
+
+ };
+ }
+ dropHandler.updateAcceptRules(childUidl);
+ }
+
+ public void setSelected(TreeNode treeNode, boolean selected) {
+ if (selected) {
+ if (!isMultiselect) {
+ while (selectedIds.size() > 0) {
+ final String id = selectedIds.iterator().next();
+ final TreeNode oldSelection = getNodeByKey(id);
+ if (oldSelection != null) {
+ // can be null if the node is not visible (parent
+ // collapsed)
+ oldSelection.setSelected(false);
+ }
+ selectedIds.remove(id);
+ }
+ }
+ treeNode.setSelected(true);
+ selectedIds.add(treeNode.key);
+ } else {
+ if (!isNullSelectionAllowed) {
+ if (!isMultiselect || selectedIds.size() == 1) {
+ return;
+ }
+ }
+ selectedIds.remove(treeNode.key);
+ treeNode.setSelected(false);
+ }
+
+ sendSelectionToServer();
+ }
+
+ /**
+ * Sends the selection to the server
+ */
+ private void sendSelectionToServer() {
+ Command command = new Command() {
+ @Override
+ public void execute() {
+ /*
+ * we should send selection to server immediately in 2 cases: 1)
+ * 'immediate' property of Tree is true 2) clickEventPending is
+ * true
+ */
+ client.updateVariable(paintableId, "selected",
+ selectedIds.toArray(new String[selectedIds.size()]),
+ clickEventPending || immediate);
+ clickEventPending = false;
+ selectionHasChanged = false;
+ }
+ };
+
+ /*
+ * Delaying the sending of the selection in webkit to ensure the
+ * selection is always sent when the tree has focus and after click
+ * events have been processed. This is due to the focusing
+ * implementation in FocusImplSafari which uses timeouts when focusing
+ * and blurring.
+ */
+ if (BrowserInfo.get().isWebkit()) {
+ Scheduler.get().scheduleDeferred(command);
+ } else {
+ command.execute();
+ }
+ }
+
+ /**
+ * Is a node selected in the tree
+ *
+ * @param treeNode
+ * The node to check
+ * @return
+ */
+ public boolean isSelected(TreeNode treeNode) {
+ return selectedIds.contains(treeNode.key);
+ }
+
+ public class TreeNode extends SimplePanel implements ActionOwner {
+
+ public static final String CLASSNAME = "v-tree-node";
+ public static final String CLASSNAME_FOCUSED = CLASSNAME + "-focused";
+
+ public String key;
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public String[] actionKeys = null;
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public boolean childrenLoaded;
+
+ Element nodeCaptionDiv;
+
+ protected Element nodeCaptionSpan;
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public FlowPanel childNodeContainer;
+
+ private boolean open;
+
+ private Icon icon;
+
+ private Event mouseDownEvent;
+
+ private int cachedHeight = -1;
+
+ private boolean focused = false;
+
+ public TreeNode() {
+ constructDom();
+ sinkEvents(Event.ONCLICK | Event.ONDBLCLICK | Event.MOUSEEVENTS
+ | Event.TOUCHEVENTS | Event.ONCONTEXTMENU);
+ }
+
+ public VerticalDropLocation getDropDetail(NativeEvent currentGwtEvent) {
+ if (cachedHeight < 0) {
+ /*
+ * Height is cached to avoid flickering (drop hints may change
+ * the reported offsetheight -> would change the drop detail)
+ */
+ cachedHeight = nodeCaptionDiv.getOffsetHeight();
+ }
+ VerticalDropLocation verticalDropLocation = DDUtil
+ .getVerticalDropLocation(nodeCaptionDiv, cachedHeight,
+ currentGwtEvent, 0.15);
+ return verticalDropLocation;
+ }
+
+ protected void emphasis(VerticalDropLocation detail) {
+ String base = "v-tree-node-drag-";
+ UIObject.setStyleName(getElement(), base + "top",
+ VerticalDropLocation.TOP == detail);
+ UIObject.setStyleName(getElement(), base + "bottom",
+ VerticalDropLocation.BOTTOM == detail);
+ UIObject.setStyleName(getElement(), base + "center",
+ VerticalDropLocation.MIDDLE == detail);
+ base = "v-tree-node-caption-drag-";
+ UIObject.setStyleName(nodeCaptionDiv, base + "top",
+ VerticalDropLocation.TOP == detail);
+ UIObject.setStyleName(nodeCaptionDiv, base + "bottom",
+ VerticalDropLocation.BOTTOM == detail);
+ UIObject.setStyleName(nodeCaptionDiv, base + "center",
+ VerticalDropLocation.MIDDLE == detail);
+
+ // also add classname to "folder node" into which the drag is
+ // targeted
+
+ TreeNode folder = null;
+ /* Possible parent of this TreeNode will be stored here */
+ TreeNode parentFolder = getParentNode();
+
+ // TODO fix my bugs
+ if (isLeaf()) {
+ folder = parentFolder;
+ // note, parent folder may be null if this is root node => no
+ // folder target exists
+ } else {
+ if (detail == VerticalDropLocation.TOP) {
+ folder = parentFolder;
+ } else {
+ folder = this;
+ }
+ // ensure we remove the dragfolder classname from the previous
+ // folder node
+ setDragFolderStyleName(this, false);
+ setDragFolderStyleName(parentFolder, false);
+ }
+ if (folder != null) {
+ setDragFolderStyleName(folder, detail != null);
+ }
+
+ }
+
+ private TreeNode getParentNode() {
+ Widget parent2 = getParent().getParent();
+ if (parent2 instanceof TreeNode) {
+ return (TreeNode) parent2;
+ }
+ return null;
+ }
+
+ private void setDragFolderStyleName(TreeNode folder, boolean add) {
+ if (folder != null) {
+ UIObject.setStyleName(folder.getElement(),
+ "v-tree-node-dragfolder", add);
+ UIObject.setStyleName(folder.nodeCaptionDiv,
+ "v-tree-node-caption-dragfolder", add);
+ }
+ }
+
+ /**
+ * Handles mouse selection
+ *
+ * @param ctrl
+ * Was the ctrl-key pressed
+ * @param shift
+ * Was the shift-key pressed
+ * @return Returns true if event was handled, else false
+ */
+ private boolean handleClickSelection(final boolean ctrl,
+ final boolean shift) {
+
+ // always when clicking an item, focus it
+ setFocusedNode(this, false);
+
+ if (!BrowserInfo.get().isOpera()) {
+ /*
+ * Ensure that the tree's focus element also gains focus
+ * (TreeNodes focus is faked using FocusElementPanel in browsers
+ * other than Opera).
+ */
+ focus();
+ }
+
+ executeEventCommand(new ScheduledCommand() {
+
+ @Override
+ public void execute() {
+
+ if (multiSelectMode == MultiSelectMode.SIMPLE
+ || !isMultiselect) {
+ toggleSelection();
+ lastSelection = TreeNode.this;
+ } else if (multiSelectMode == MultiSelectMode.DEFAULT) {
+ // Handle ctrl+click
+ if (isMultiselect && ctrl && !shift) {
+ toggleSelection();
+ lastSelection = TreeNode.this;
+
+ // Handle shift+click
+ } else if (isMultiselect && !ctrl && shift) {
+ deselectAll();
+ selectNodeRange(lastSelection.key, key);
+ sendSelectionToServer();
+
+ // Handle ctrl+shift click
+ } else if (isMultiselect && ctrl && shift) {
+ selectNodeRange(lastSelection.key, key);
+
+ // Handle click
+ } else {
+ // TODO should happen only if this alone not yet
+ // selected,
+ // now sending excess server calls
+ deselectAll();
+ toggleSelection();
+ lastSelection = TreeNode.this;
+ }
+ }
+ }
+ });
+
+ return true;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.google.gwt.user.client.ui.Widget#onBrowserEvent(com.google.gwt
+ * .user.client.Event)
+ */
+ @Override
+ public void onBrowserEvent(Event event) {
+ super.onBrowserEvent(event);
+ final int type = DOM.eventGetType(event);
+ final Element target = DOM.eventGetTarget(event);
+
+ if (type == Event.ONLOAD && icon != null
+ && target == icon.getElement()) {
+ iconLoaded.trigger();
+ }
+
+ if (disabled) {
+ return;
+ }
+
+ final boolean inCaption = isCaptionElement(target);
+ if (inCaption && client.hasEventListeners(VTree.this,
+ TreeConstants.ITEM_CLICK_EVENT_ID)
+
+ && (type == Event.ONDBLCLICK || type == Event.ONMOUSEUP)) {
+ fireClick(event);
+ }
+ if (type == Event.ONCLICK) {
+ if (getElement() == target) {
+ // state change
+ toggleState();
+ } else if (!readonly && inCaption) {
+ if (selectable) {
+ // caption click = selection change && possible click
+ // event
+ if (handleClickSelection(
+ event.getCtrlKey() || event.getMetaKey(),
+ event.getShiftKey())) {
+ event.preventDefault();
+ }
+ } else {
+ // Not selectable, only focus the node.
+ setFocusedNode(this);
+ }
+ }
+ event.stopPropagation();
+ } else if (type == Event.ONCONTEXTMENU) {
+ showContextMenu(event);
+ }
+
+ if (dragMode != 0 || dropHandler != null) {
+ if (type == Event.ONMOUSEDOWN || type == Event.ONTOUCHSTART) {
+ if (nodeCaptionDiv.isOrHasChild(
+ (Node) event.getEventTarget().cast())) {
+ if (dragMode > 0 && (type == Event.ONTOUCHSTART || event
+ .getButton() == NativeEvent.BUTTON_LEFT)) {
+ mouseDownEvent = event; // save event for possible
+ // dd operation
+ if (type == Event.ONMOUSEDOWN) {
+ event.preventDefault(); // prevent text
+ // selection
+ } else {
+ /*
+ * FIXME We prevent touch start event to be used
+ * as a scroll start event. Note that we cannot
+ * easily distinguish whether the user wants to
+ * drag or scroll. The same issue is in table
+ * that has scrollable area and has drag and
+ * drop enable. Some kind of timer might be used
+ * to resolve the issue.
+ */
+ event.stopPropagation();
+ }
+ }
+ }
+ } else if (type == Event.ONMOUSEMOVE || type == Event.ONMOUSEOUT
+ || type == Event.ONTOUCHMOVE) {
+
+ if (mouseDownEvent != null) {
+ // start actual drag on slight move when mouse is down
+ VTransferable t = new VTransferable();
+ t.setDragSource(ConnectorMap.get(client)
+ .getConnector(VTree.this));
+ t.setData("itemId", key);
+ VDragEvent drag = VDragAndDropManager.get().startDrag(t,
+ mouseDownEvent, true);
+
+ drag.createDragImage(nodeCaptionDiv, true);
+ event.stopPropagation();
+
+ mouseDownEvent = null;
+ }
+ } else if (type == Event.ONMOUSEUP) {
+ mouseDownEvent = null;
+ }
+ if (type == Event.ONMOUSEOVER) {
+ mouseDownEvent = null;
+ currentMouseOverKey = key;
+ event.stopPropagation();
+ }
+
+ } else if (type == Event.ONMOUSEDOWN
+ && event.getButton() == NativeEvent.BUTTON_LEFT) {
+ event.preventDefault(); // text selection
+ }
+ }
+
+ /**
+ * Checks if the given element is the caption or the icon.
+ *
+ * @param target
+ * The element to check
+ * @return true if the element is the caption or the icon
+ */
+ public boolean isCaptionElement(
+ com.google.gwt.dom.client.Element target) {
+ return (target == nodeCaptionSpan
+ || (icon != null && target == icon.getElement()));
+ }
+
+ private void fireClick(final Event evt) {
+ /*
+ * Ensure we have focus in tree before sending variables. Otherwise
+ * previously modified field may contain dirty variables.
+ */
+ if (!treeHasFocus) {
+ if (BrowserInfo.get().isOpera()) {
+ if (focusedNode == null) {
+ getNodeByKey(key).setFocused(true);
+ } else {
+ focusedNode.setFocused(true);
+ }
+ } else {
+ focus();
+ }
+ }
+
+ final MouseEventDetails details = MouseEventDetailsBuilder
+ .buildMouseEventDetails(evt);
+
+ executeEventCommand(new ScheduledCommand() {
+
+ @Override
+ public void execute() {
+ // Determine if we should send the event immediately to the
+ // server. We do not want to send the event if there is a
+ // selection event happening after this. In all other cases
+ // we want to send it immediately.
+ clickEventPending = false;
+ if ((details.getButton() == MouseButton.LEFT
+ || details.getButton() == MouseButton.MIDDLE)
+ && !details.isDoubleClick() && selectable) {
+ // Probably a selection that will cause a value change
+ // event to be sent
+ clickEventPending = true;
+
+ // The exception is that user clicked on the
+ // currently selected row and null selection is not
+ // allowed == no selection event
+ if (isSelected() && selectedIds.size() == 1
+ && !isNullSelectionAllowed) {
+ clickEventPending = false;
+ }
+ }
+ client.updateVariable(paintableId, "clickedKey", key,
+ false);
+ client.updateVariable(paintableId, "clickEvent",
+ details.toString(), !clickEventPending);
+ }
+ });
+ }
+
+ /*
+ * Must wait for Safari to focus before sending click and value change
+ * events (see #6373, #6374)
+ */
+ private void executeEventCommand(ScheduledCommand command) {
+ if (BrowserInfo.get().isWebkit() && !treeHasFocus) {
+ Scheduler.get().scheduleDeferred(command);
+ } else {
+ command.execute();
+ }
+ }
+
+ private void toggleSelection() {
+ if (selectable) {
+ VTree.this.setSelected(this, !isSelected());
+ }
+ }
+
+ private void toggleState() {
+ setState(!getState(), true);
+ }
+
+ protected void constructDom() {
+ String labelId = DOM.createUniqueId();
+
+ addStyleName(CLASSNAME);
+ String treeItemId = DOM.createUniqueId();
+ getElement().setId(treeItemId);
+ Roles.getTreeitemRole().set(getElement());
+ Roles.getTreeitemRole().setAriaSelectedState(getElement(),
+ SelectedValue.FALSE);
+ Roles.getTreeitemRole().setAriaLabelledbyProperty(getElement(),
+ Id.of(labelId));
+
+ nodeCaptionDiv = DOM.createDiv();
+ DOM.setElementProperty(nodeCaptionDiv, "className",
+ CLASSNAME + "-caption");
+ Element wrapper = DOM.createDiv();
+ wrapper.setId(labelId);
+ wrapper.setAttribute("for", treeItemId);
+
+ nodeCaptionSpan = DOM.createSpan();
+ DOM.appendChild(getElement(), nodeCaptionDiv);
+ DOM.appendChild(nodeCaptionDiv, wrapper);
+ DOM.appendChild(wrapper, nodeCaptionSpan);
+
+ if (BrowserInfo.get().isOpera()) {
+ /*
+ * Focus the caption div of the node to get keyboard navigation
+ * to work without scrolling up or down when focusing a node.
+ */
+ nodeCaptionDiv.setTabIndex(-1);
+ }
+
+ childNodeContainer = new FlowPanel();
+ childNodeContainer.setStyleName(CLASSNAME + "-children");
+ Roles.getGroupRole().set(childNodeContainer.getElement());
+ setWidget(childNodeContainer);
+ }
+
+ public boolean isLeaf() {
+ String[] styleNames = getStyleName().split(" ");
+ for (String styleName : styleNames) {
+ if (styleName.equals(CLASSNAME + "-leaf")) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public void setState(boolean state, boolean notifyServer) {
+ if (open == state) {
+ return;
+ }
+ if (state) {
+ if (!childrenLoaded && notifyServer) {
+ client.updateVariable(paintableId, "requestChildTree", true,
+ false);
+ }
+ if (notifyServer) {
+ client.updateVariable(paintableId, "expand",
+ new String[] { key }, true);
+ }
+ addStyleName(CLASSNAME + "-expanded");
+ Roles.getTreeitemRole().setAriaExpandedState(getElement(),
+ ExpandedValue.TRUE);
+ childNodeContainer.setVisible(true);
+ } else {
+ removeStyleName(CLASSNAME + "-expanded");
+ Roles.getTreeitemRole().setAriaExpandedState(getElement(),
+ ExpandedValue.FALSE);
+ childNodeContainer.setVisible(false);
+ if (notifyServer) {
+ client.updateVariable(paintableId, "collapse",
+ new String[] { key }, true);
+ }
+ }
+ open = state;
+
+ if (!rendering) {
+ doLayout();
+ }
+ }
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public boolean getState() {
+ return open;
+ }
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public void setText(String text) {
+ DOM.setInnerText(nodeCaptionSpan, text);
+ }
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public void setHtml(String html) {
+ nodeCaptionSpan.setInnerHTML(html);
+ }
+
+ public boolean isChildrenLoaded() {
+ return childrenLoaded;
+ }
+
+ /**
+ * Returns the children of the node
+ *
+ * @return A set of tree nodes
+ */
+ public List<TreeNode> getChildren() {
+ List<TreeNode> nodes = new LinkedList<TreeNode>();
+
+ if (!isLeaf() && isChildrenLoaded()) {
+ Iterator<Widget> iter = childNodeContainer.iterator();
+ while (iter.hasNext()) {
+ TreeNode node = (TreeNode) iter.next();
+ nodes.add(node);
+ }
+ }
+ return nodes;
+ }
+
+ @Override
+ public Action[] getActions() {
+ if (actionKeys == null) {
+ return new Action[] {};
+ }
+ final Action[] actions = new Action[actionKeys.length];
+ for (int i = 0; i < actions.length; i++) {
+ final String actionKey = actionKeys[i];
+ final TreeAction a = new TreeAction(this, String.valueOf(key),
+ actionKey);
+ a.setCaption(getActionCaption(actionKey));
+ a.setIconUrl(getActionIcon(actionKey));
+ actions[i] = a;
+ }
+ return actions;
+ }
+
+ @Override
+ public ApplicationConnection getClient() {
+ return client;
+ }
+
+ @Override
+ public String getPaintableId() {
+ return paintableId;
+ }
+
+ /**
+ * Adds/removes Vaadin specific style name.
+ * <p>
+ * For internal use only. May be removed or replaced in the future.
+ *
+ * @param selected
+ */
+ public void setSelected(boolean selected) {
+ // add style name to caption dom structure only, not to subtree
+ setStyleName(nodeCaptionDiv, "v-tree-node-selected", selected);
+ }
+
+ protected boolean isSelected() {
+ return VTree.this.isSelected(this);
+ }
+
+ /**
+ * Travels up the hierarchy looking for this node
+ *
+ * @param child
+ * The child which grandparent this is or is not
+ * @return True if this is a grandparent of the child node
+ */
+ public boolean isGrandParentOf(TreeNode child) {
+ TreeNode currentNode = child;
+ boolean isGrandParent = false;
+ while (currentNode != null) {
+ currentNode = currentNode.getParentNode();
+ if (currentNode == this) {
+ isGrandParent = true;
+ break;
+ }
+ }
+ return isGrandParent;
+ }
+
+ public boolean isSibling(TreeNode node) {
+ return node.getParentNode() == getParentNode();
+ }
+
+ public void showContextMenu(Event event) {
+ if (!readonly && !disabled) {
+ if (actionKeys != null) {
+ int left = event.getClientX();
+ int top = event.getClientY();
+ top += Window.getScrollTop();
+ left += Window.getScrollLeft();
+ client.getContextMenu().showAt(this, left, top);
+
+ event.stopPropagation();
+ event.preventDefault();
+ }
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.google.gwt.user.client.ui.Widget#onDetach()
+ */
+ @Override
+ protected void onDetach() {
+ super.onDetach();
+ client.getContextMenu().ensureHidden(this);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.google.gwt.user.client.ui.UIObject#toString()
+ */
+ @Override
+ public String toString() {
+ return nodeCaptionSpan.getInnerText();
+ }
+
+ /**
+ * Is the node focused?
+ *
+ * @param focused
+ * True if focused, false if not
+ */
+ public void setFocused(boolean focused) {
+ if (!this.focused && focused) {
+ nodeCaptionDiv.addClassName(CLASSNAME_FOCUSED);
+
+ this.focused = focused;
+ if (BrowserInfo.get().isOpera()) {
+ nodeCaptionDiv.focus();
+ }
+ treeHasFocus = true;
+ } else if (this.focused && !focused) {
+ nodeCaptionDiv.removeClassName(CLASSNAME_FOCUSED);
+ this.focused = focused;
+ treeHasFocus = false;
+ }
+ }
+
+ /**
+ * Scrolls the caption into view
+ */
+ public void scrollIntoView() {
+ WidgetUtil.scrollIntoViewVertically(nodeCaptionDiv);
+ }
+
+ public void setIcon(String iconUrl, String altText) {
+ if (icon != null) {
+ DOM.getFirstChild(nodeCaptionDiv)
+ .removeChild(icon.getElement());
+ }
+ icon = client.getIcon(iconUrl);
+ if (icon != null) {
+ DOM.insertBefore(DOM.getFirstChild(nodeCaptionDiv),
+ icon.getElement(), nodeCaptionSpan);
+ icon.setAlternateText(altText);
+ }
+ }
+
+ public void setNodeStyleName(String styleName) {
+ addStyleName(TreeNode.CLASSNAME + "-" + styleName);
+ setStyleName(nodeCaptionDiv,
+ TreeNode.CLASSNAME + "-caption-" + styleName, true);
+ childNodeContainer.addStyleName(
+ TreeNode.CLASSNAME + "-children-" + styleName);
+
+ }
+
+ }
+
+ @Override
+ public VDropHandler getDropHandler() {
+ return dropHandler;
+ }
+
+ public TreeNode getNodeByKey(String key) {
+ return keyToNode.get(key);
+ }
+
+ /**
+ * Deselects all items in the tree
+ */
+ public void deselectAll() {
+ for (String key : selectedIds) {
+ TreeNode node = keyToNode.get(key);
+ if (node != null) {
+ node.setSelected(false);
+ }
+ }
+ selectedIds.clear();
+ selectionHasChanged = true;
+ }
+
+ /**
+ * Selects a range of nodes
+ *
+ * @param startNodeKey
+ * The start node key
+ * @param endNodeKey
+ * The end node key
+ */
+ private void selectNodeRange(String startNodeKey, String endNodeKey) {
+
+ TreeNode startNode = keyToNode.get(startNodeKey);
+ TreeNode endNode = keyToNode.get(endNodeKey);
+
+ // The nodes have the same parent
+ if (startNode.getParent() == endNode.getParent()) {
+ doSiblingSelection(startNode, endNode);
+
+ // The start node is a grandparent of the end node
+ } else if (startNode.isGrandParentOf(endNode)) {
+ doRelationSelection(startNode, endNode);
+
+ // The end node is a grandparent of the start node
+ } else if (endNode.isGrandParentOf(startNode)) {
+ doRelationSelection(endNode, startNode);
+
+ } else {
+ doNoRelationSelection(startNode, endNode);
+ }
+ }
+
+ /**
+ * Selects a node and deselect all other nodes
+ *
+ * @param node
+ * The node to select
+ */
+ private void selectNode(TreeNode node, boolean deselectPrevious) {
+ if (deselectPrevious) {
+ deselectAll();
+ }
+
+ if (node != null) {
+ node.setSelected(true);
+ selectedIds.add(node.key);
+ lastSelection = node;
+ }
+ selectionHasChanged = true;
+ }
+
+ /**
+ * Deselects a node
+ *
+ * @param node
+ * The node to deselect
+ */
+ private void deselectNode(TreeNode node) {
+ node.setSelected(false);
+ selectedIds.remove(node.key);
+ selectionHasChanged = true;
+ }
+
+ /**
+ * Selects all the open children to a node
+ *
+ * @param node
+ * The parent node
+ */
+ private void selectAllChildren(TreeNode node, boolean includeRootNode) {
+ if (includeRootNode) {
+ node.setSelected(true);
+ selectedIds.add(node.key);
+ }
+
+ for (TreeNode child : node.getChildren()) {
+ if (!child.isLeaf() && child.getState()) {
+ selectAllChildren(child, true);
+ } else {
+ child.setSelected(true);
+ selectedIds.add(child.key);
+ }
+ }
+ selectionHasChanged = true;
+ }
+
+ /**
+ * Selects all children until a stop child is reached
+ *
+ * @param root
+ * The root not to start from
+ * @param stopNode
+ * The node to finish with
+ * @param includeRootNode
+ * Should the root node be selected
+ * @param includeStopNode
+ * Should the stop node be selected
+ *
+ * @return Returns false if the stop child was found, else true if all
+ * children was selected
+ */
+ private boolean selectAllChildrenUntil(TreeNode root, TreeNode stopNode,
+ boolean includeRootNode, boolean includeStopNode) {
+ if (includeRootNode) {
+ root.setSelected(true);
+ selectedIds.add(root.key);
+ }
+ if (root.getState() && root != stopNode) {
+ for (TreeNode child : root.getChildren()) {
+ if (!child.isLeaf() && child.getState() && child != stopNode) {
+ if (!selectAllChildrenUntil(child, stopNode, true,
+ includeStopNode)) {
+ return false;
+ }
+ } else if (child == stopNode) {
+ if (includeStopNode) {
+ child.setSelected(true);
+ selectedIds.add(child.key);
+ }
+ return false;
+ } else {
+ child.setSelected(true);
+ selectedIds.add(child.key);
+ }
+ }
+ }
+ selectionHasChanged = true;
+
+ return true;
+ }
+
+ /**
+ * Select a range between two nodes which have no relation to each other
+ *
+ * @param startNode
+ * The start node to start the selection from
+ * @param endNode
+ * The end node to end the selection to
+ */
+ private void doNoRelationSelection(TreeNode startNode, TreeNode endNode) {
+
+ TreeNode commonParent = getCommonGrandParent(startNode, endNode);
+ TreeNode startBranch = null, endBranch = null;
+
+ // Find the children of the common parent
+ List<TreeNode> children;
+ if (commonParent != null) {
+ children = commonParent.getChildren();
+ } else {
+ children = getRootNodes();
+ }
+
+ // Find the start and end branches
+ for (TreeNode node : children) {
+ if (nodeIsInBranch(startNode, node)) {
+ startBranch = node;
+ }
+ if (nodeIsInBranch(endNode, node)) {
+ endBranch = node;
+ }
+ }
+
+ // Swap nodes if necessary
+ if (children.indexOf(startBranch) > children.indexOf(endBranch)) {
+ TreeNode temp = startBranch;
+ startBranch = endBranch;
+ endBranch = temp;
+
+ temp = startNode;
+ startNode = endNode;
+ endNode = temp;
+ }
+
+ // Select all children under the start node
+ selectAllChildren(startNode, true);
+ TreeNode startParent = startNode.getParentNode();
+ TreeNode currentNode = startNode;
+ while (startParent != null && startParent != commonParent) {
+ List<TreeNode> startChildren = startParent.getChildren();
+ for (int i = startChildren.indexOf(currentNode)
+ + 1; i < startChildren.size(); i++) {
+ selectAllChildren(startChildren.get(i), true);
+ }
+
+ currentNode = startParent;
+ startParent = startParent.getParentNode();
+ }
+
+ // Select nodes until the end node is reached
+ for (int i = children.indexOf(startBranch) + 1; i <= children
+ .indexOf(endBranch); i++) {
+ selectAllChildrenUntil(children.get(i), endNode, true, true);
+ }
+
+ // Ensure end node was selected
+ endNode.setSelected(true);
+ selectedIds.add(endNode.key);
+ selectionHasChanged = true;
+ }
+
+ /**
+ * Examines the children of the branch node and returns true if a node is in
+ * that branch
+ *
+ * @param node
+ * The node to search for
+ * @param branch
+ * The branch to search in
+ * @return True if found, false if not found
+ */
+ private boolean nodeIsInBranch(TreeNode node, TreeNode branch) {
+ if (node == branch) {
+ return true;
+ }
+ for (TreeNode child : branch.getChildren()) {
+ if (child == node) {
+ return true;
+ }
+ if (!child.isLeaf() && child.getState()) {
+ if (nodeIsInBranch(node, child)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Selects a range of items which are in direct relation with each
+ * other.<br/>
+ * NOTE: The start node <b>MUST</b> be before the end node!
+ *
+ * @param startNode
+ *
+ * @param endNode
+ */
+ private void doRelationSelection(TreeNode startNode, TreeNode endNode) {
+ TreeNode currentNode = endNode;
+ while (currentNode != startNode) {
+ currentNode.setSelected(true);
+ selectedIds.add(currentNode.key);
+
+ // Traverse children above the selection
+ List<TreeNode> subChildren = currentNode.getParentNode()
+ .getChildren();
+ if (subChildren.size() > 1) {
+ selectNodeRange(subChildren.iterator().next().key,
+ currentNode.key);
+ } else if (subChildren.size() == 1) {
+ TreeNode n = subChildren.get(0);
+ n.setSelected(true);
+ selectedIds.add(n.key);
+ }
+
+ currentNode = currentNode.getParentNode();
+ }
+ startNode.setSelected(true);
+ selectedIds.add(startNode.key);
+ selectionHasChanged = true;
+ }
+
+ /**
+ * Selects a range of items which have the same parent.
+ *
+ * @param startNode
+ * The start node
+ * @param endNode
+ * The end node
+ */
+ private void doSiblingSelection(TreeNode startNode, TreeNode endNode) {
+ TreeNode parent = startNode.getParentNode();
+
+ List<TreeNode> children;
+ if (parent == null) {
+ // Topmost parent
+ children = getRootNodes();
+ } else {
+ children = parent.getChildren();
+ }
+
+ // Swap start and end point if needed
+ if (children.indexOf(startNode) > children.indexOf(endNode)) {
+ TreeNode temp = startNode;
+ startNode = endNode;
+ endNode = temp;
+ }
+
+ Iterator<TreeNode> childIter = children.iterator();
+ boolean startFound = false;
+ while (childIter.hasNext()) {
+ TreeNode node = childIter.next();
+ if (node == startNode) {
+ startFound = true;
+ }
+
+ if (startFound && node != endNode && node.getState()) {
+ selectAllChildren(node, true);
+ } else if (startFound && node != endNode) {
+ node.setSelected(true);
+ selectedIds.add(node.key);
+ }
+
+ if (node == endNode) {
+ node.setSelected(true);
+ selectedIds.add(node.key);
+ break;
+ }
+ }
+ selectionHasChanged = true;
+ }
+
+ /**
+ * Returns the first common parent of two nodes
+ *
+ * @param node1
+ * The first node
+ * @param node2
+ * The second node
+ * @return The common parent or null
+ */
+ public TreeNode getCommonGrandParent(TreeNode node1, TreeNode node2) {
+ // If either one does not have a grand parent then return null
+ if (node1.getParentNode() == null || node2.getParentNode() == null) {
+ return null;
+ }
+
+ // If the nodes are parents of each other then return null
+ if (node1.isGrandParentOf(node2) || node2.isGrandParentOf(node1)) {
+ return null;
+ }
+
+ // Get parents of node1
+ List<TreeNode> parents1 = new ArrayList<TreeNode>();
+ TreeNode parent1 = node1.getParentNode();
+ while (parent1 != null) {
+ parents1.add(parent1);
+ parent1 = parent1.getParentNode();
+ }
+
+ // Get parents of node2
+ List<TreeNode> parents2 = new ArrayList<TreeNode>();
+ TreeNode parent2 = node2.getParentNode();
+ while (parent2 != null) {
+ parents2.add(parent2);
+ parent2 = parent2.getParentNode();
+ }
+
+ // Search the parents for the first common parent
+ for (int i = 0; i < parents1.size(); i++) {
+ parent1 = parents1.get(i);
+ for (int j = 0; j < parents2.size(); j++) {
+ parent2 = parents2.get(j);
+ if (parent1 == parent2) {
+ return parent1;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Sets the node currently in focus
+ *
+ * @param node
+ * The node to focus or null to remove the focus completely
+ * @param scrollIntoView
+ * Scroll the node into view
+ */
+ public void setFocusedNode(TreeNode node, boolean scrollIntoView) {
+ // Unfocus previously focused node
+ if (focusedNode != null) {
+ focusedNode.setFocused(false);
+
+ Roles.getTreeRole().removeAriaActivedescendantProperty(
+ focusedNode.getElement());
+ }
+
+ if (node != null) {
+ node.setFocused(true);
+ Roles.getTreeitemRole().setAriaSelectedState(node.getElement(),
+ SelectedValue.TRUE);
+
+ /*
+ * FIXME: This code needs to be changed when the keyboard navigation
+ * doesn't immediately trigger a selection change anymore.
+ *
+ * Right now this function is called before and after the Tree is
+ * rebuilt when up/down arrow keys are pressed. This leads to the
+ * problem, that the newly selected item is announced too often with
+ * a screen reader.
+ *
+ * Behaviour is different when using the Tree with and without
+ * screen reader.
+ */
+ if (node.key.equals(lastNodeKey)) {
+ Roles.getTreeRole().setAriaActivedescendantProperty(
+ getFocusElement(), Id.of(node.getElement()));
+ } else {
+ lastNodeKey = node.key;
+ }
+ }
+
+ focusedNode = node;
+
+ if (node != null && scrollIntoView) {
+ /*
+ * Delay scrolling the focused node into view if we are still
+ * rendering. #5396
+ */
+ if (!rendering) {
+ node.scrollIntoView();
+ } else {
+ Scheduler.get().scheduleDeferred(new Command() {
+ @Override
+ public void execute() {
+ focusedNode.scrollIntoView();
+ }
+ });
+ }
+ }
+ }
+
+ /**
+ * Focuses a node and scrolls it into view
+ *
+ * @param node
+ * The node to focus
+ */
+ public void setFocusedNode(TreeNode node) {
+ setFocusedNode(node, true);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.google.gwt.event.dom.client.FocusHandler#onFocus(com.google.gwt.event
+ * .dom.client.FocusEvent)
+ */
+ @Override
+ public void onFocus(FocusEvent event) {
+ treeHasFocus = true;
+ // If no node has focus, focus the first item in the tree
+ if (focusedNode == null && lastSelection == null && selectable) {
+ setFocusedNode(getFirstRootNode(), false);
+ } else if (focusedNode != null && selectable) {
+ setFocusedNode(focusedNode, false);
+ } else if (lastSelection != null && selectable) {
+ setFocusedNode(lastSelection, false);
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.google.gwt.event.dom.client.BlurHandler#onBlur(com.google.gwt.event
+ * .dom.client.BlurEvent)
+ */
+ @Override
+ public void onBlur(BlurEvent event) {
+ treeHasFocus = false;
+ if (focusedNode != null) {
+ focusedNode.setFocused(false);
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.google.gwt.event.dom.client.KeyPressHandler#onKeyPress(com.google
+ * .gwt.event.dom.client.KeyPressEvent)
+ */
+ @Override
+ public void onKeyPress(KeyPressEvent event) {
+ NativeEvent nativeEvent = event.getNativeEvent();
+ int keyCode = nativeEvent.getKeyCode();
+ if (keyCode == 0 && nativeEvent.getCharCode() == ' ') {
+ // Provide a keyCode for space to be compatible with FireFox
+ // keypress event
+ keyCode = CHARCODE_SPACE;
+ }
+ if (handleKeyNavigation(keyCode,
+ event.isControlKeyDown() || event.isMetaKeyDown(),
+ event.isShiftKeyDown())) {
+ event.preventDefault();
+ event.stopPropagation();
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.google.gwt.event.dom.client.KeyDownHandler#onKeyDown(com.google.gwt
+ * .event.dom.client.KeyDownEvent)
+ */
+ @Override
+ public void onKeyDown(KeyDownEvent event) {
+ if (handleKeyNavigation(event.getNativeEvent().getKeyCode(),
+ event.isControlKeyDown() || event.isMetaKeyDown(),
+ event.isShiftKeyDown())) {
+ event.preventDefault();
+ event.stopPropagation();
+ }
+ }
+
+ /**
+ * Handles the keyboard navigation
+ *
+ * @param keycode
+ * The keycode of the pressed key
+ * @param ctrl
+ * Was ctrl pressed
+ * @param shift
+ * Was shift pressed
+ * @return Returns true if the key was handled, else false
+ */
+ protected boolean handleKeyNavigation(int keycode, boolean ctrl,
+ boolean shift) {
+ // Navigate down
+ if (keycode == getNavigationDownKey()) {
+ TreeNode node = null;
+ // If node is open and has children then move in to the children
+ if (!focusedNode.isLeaf() && focusedNode.getState()
+ && focusedNode.getChildren().size() > 0) {
+ node = focusedNode.getChildren().get(0);
+ }
+
+ // Else move down to the next sibling
+ else {
+ node = getNextSibling(focusedNode);
+ if (node == null) {
+ // Else jump to the parent and try to select the next
+ // sibling there
+ TreeNode current = focusedNode;
+ while (node == null && current.getParentNode() != null) {
+ node = getNextSibling(current.getParentNode());
+ current = current.getParentNode();
+ }
+ }
+ }
+
+ if (node != null) {
+ setFocusedNode(node);
+ if (selectable) {
+ if (!ctrl && !shift) {
+ selectNode(node, true);
+ } else if (shift && isMultiselect) {
+ deselectAll();
+ selectNodeRange(lastSelection.key, node.key);
+ } else if (shift) {
+ selectNode(node, true);
+ }
+ }
+ showTooltipForKeyboardNavigation(node);
+ }
+ return true;
+ }
+
+ // Navigate up
+ if (keycode == getNavigationUpKey()) {
+ TreeNode prev = getPreviousSibling(focusedNode);
+ TreeNode node = null;
+ if (prev != null) {
+ node = getLastVisibleChildInTree(prev);
+ } else if (focusedNode.getParentNode() != null) {
+ node = focusedNode.getParentNode();
+ }
+ if (node != null) {
+ setFocusedNode(node);
+ if (selectable) {
+ if (!ctrl && !shift) {
+ selectNode(node, true);
+ } else if (shift && isMultiselect) {
+ deselectAll();
+ selectNodeRange(lastSelection.key, node.key);
+ } else if (shift) {
+ selectNode(node, true);
+ }
+ }
+ showTooltipForKeyboardNavigation(node);
+ }
+ return true;
+ }
+
+ // Navigate left (close branch)
+ if (keycode == getNavigationLeftKey()) {
+ if (!focusedNode.isLeaf() && focusedNode.getState()) {
+ focusedNode.setState(false, true);
+ } else if (focusedNode.getParentNode() != null
+ && (focusedNode.isLeaf() || !focusedNode.getState())) {
+
+ if (ctrl || !selectable) {
+ setFocusedNode(focusedNode.getParentNode());
+ } else if (shift) {
+ doRelationSelection(focusedNode.getParentNode(),
+ focusedNode);
+ setFocusedNode(focusedNode.getParentNode());
+ } else {
+ focusAndSelectNode(focusedNode.getParentNode());
+ }
+ }
+ showTooltipForKeyboardNavigation(focusedNode);
+ return true;
+ }
+
+ // Navigate right (open branch)
+ if (keycode == getNavigationRightKey()) {
+ if (!focusedNode.isLeaf() && !focusedNode.getState()) {
+ focusedNode.setState(true, true);
+ } else if (!focusedNode.isLeaf()) {
+ if (ctrl || !selectable) {
+ setFocusedNode(focusedNode.getChildren().get(0));
+ } else if (shift) {
+ setSelected(focusedNode, true);
+ setFocusedNode(focusedNode.getChildren().get(0));
+ setSelected(focusedNode, true);
+ } else {
+ focusAndSelectNode(focusedNode.getChildren().get(0));
+ }
+ }
+ showTooltipForKeyboardNavigation(focusedNode);
+ return true;
+ }
+
+ // Selection
+ if (keycode == getNavigationSelectKey()) {
+ if (!focusedNode.isSelected()) {
+ selectNode(focusedNode,
+ (!isMultiselect
+ || multiSelectMode == MULTISELECT_MODE_SIMPLE)
+ && selectable);
+ } else {
+ deselectNode(focusedNode);
+ }
+ return true;
+ }
+
+ // Home selection
+ if (keycode == getNavigationStartKey()) {
+ TreeNode node = getFirstRootNode();
+ if (ctrl || !selectable) {
+ setFocusedNode(node);
+ } else if (shift) {
+ deselectAll();
+ selectNodeRange(focusedNode.key, node.key);
+ } else {
+ selectNode(node, true);
+ }
+ sendSelectionToServer();
+ showTooltipForKeyboardNavigation(node);
+ return true;
+ }
+
+ // End selection
+ if (keycode == getNavigationEndKey()) {
+ TreeNode lastNode = getLastRootNode();
+ TreeNode node = getLastVisibleChildInTree(lastNode);
+ if (ctrl || !selectable) {
+ setFocusedNode(node);
+ } else if (shift) {
+ deselectAll();
+ selectNodeRange(focusedNode.key, node.key);
+ } else {
+ selectNode(node, true);
+ }
+ sendSelectionToServer();
+ showTooltipForKeyboardNavigation(node);
+ return true;
+ }
+
+ return false;
+ }
+
+ private void showTooltipForKeyboardNavigation(TreeNode node) {
+ if (connector != null) {
+ getClient().getVTooltip().showAssistive(
+ connector.getTooltipInfo(node.nodeCaptionSpan));
+ }
+ }
+
+ private void focusAndSelectNode(TreeNode node) {
+ /*
+ * Keyboard navigation doesn't work reliably if the tree is in
+ * multiselect mode as well as isNullSelectionAllowed = false. It first
+ * tries to deselect the old focused node, which fails since there must
+ * be at least one selection. After this the newly focused node is
+ * selected and we've ended up with two selected nodes even though we
+ * only navigated with the arrow keys.
+ *
+ * Because of this, we first select the next node and later de-select
+ * the old one.
+ */
+ TreeNode oldFocusedNode = focusedNode;
+ setFocusedNode(node);
+ setSelected(focusedNode, true);
+ setSelected(oldFocusedNode, false);
+ }
+
+ /**
+ * Traverses the tree to the bottom most child
+ *
+ * @param root
+ * The root of the tree
+ * @return The bottom most child
+ */
+ private TreeNode getLastVisibleChildInTree(TreeNode root) {
+ if (root.isLeaf() || !root.getState()
+ || root.getChildren().size() == 0) {
+ return root;
+ }
+ List<TreeNode> children = root.getChildren();
+ return getLastVisibleChildInTree(children.get(children.size() - 1));
+ }
+
+ /**
+ * Gets the next sibling in the tree
+ *
+ * @param node
+ * The node to get the sibling for
+ * @return The sibling node or null if the node is the last sibling
+ */
+ private TreeNode getNextSibling(TreeNode node) {
+ TreeNode parent = node.getParentNode();
+ List<TreeNode> children;
+ if (parent == null) {
+ children = getRootNodes();
+ } else {
+ children = parent.getChildren();
+ }
+
+ int idx = children.indexOf(node);
+ if (idx < children.size() - 1) {
+ return children.get(idx + 1);
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns the previous sibling in the tree
+ *
+ * @param node
+ * The node to get the sibling for
+ * @return The sibling node or null if the node is the first sibling
+ */
+ private TreeNode getPreviousSibling(TreeNode node) {
+ TreeNode parent = node.getParentNode();
+ List<TreeNode> children;
+ if (parent == null) {
+ children = getRootNodes();
+ } else {
+ children = parent.getChildren();
+ }
+
+ int idx = children.indexOf(node);
+ if (idx > 0) {
+ return children.get(idx - 1);
+ }
+
+ return null;
+ }
+
+ /**
+ * Add this to the element mouse down event by using element.setPropertyJSO
+ * ("onselectstart",applyDisableTextSelectionIEHack()); Remove it then again
+ * when the mouse is depressed in the mouse up event.
+ *
+ * @return Returns the JSO preventing text selection
+ */
+ private native JavaScriptObject applyDisableTextSelectionIEHack()
+ /*-{
+ return function(){ return false; };
+ }-*/;
+
+ /**
+ * Get the key that moves the selection head upwards. By default it is the
+ * up arrow key but by overriding this you can change the key to whatever
+ * you want.
+ *
+ * @return The keycode of the key
+ */
+ protected int getNavigationUpKey() {
+ return KeyCodes.KEY_UP;
+ }
+
+ /**
+ * Get the key that moves the selection head downwards. By default it is the
+ * down arrow key but by overriding this you can change the key to whatever
+ * you want.
+ *
+ * @return The keycode of the key
+ */
+ protected int getNavigationDownKey() {
+ return KeyCodes.KEY_DOWN;
+ }
+
+ /**
+ * Get the key that scrolls to the left in the table. By default it is the
+ * left arrow key but by overriding this you can change the key to whatever
+ * you want.
+ *
+ * @return The keycode of the key
+ */
+ protected int getNavigationLeftKey() {
+ return KeyCodes.KEY_LEFT;
+ }
+
+ /**
+ * Get the key that scroll to the right on the table. By default it is the
+ * right arrow key but by overriding this you can change the key to whatever
+ * you want.
+ *
+ * @return The keycode of the key
+ */
+ protected int getNavigationRightKey() {
+ return KeyCodes.KEY_RIGHT;
+ }
+
+ /**
+ * Get the key that selects an item in the table. By default it is the space
+ * bar key but by overriding this you can change the key to whatever you
+ * want.
+ *
+ * @return
+ */
+ protected int getNavigationSelectKey() {
+ return CHARCODE_SPACE;
+ }
+
+ /**
+ * Get the key the moves the selection one page up in the table. By default
+ * this is the Page Up key but by overriding this you can change the key to
+ * whatever you want.
+ *
+ * @return
+ */
+ protected int getNavigationPageUpKey() {
+ return KeyCodes.KEY_PAGEUP;
+ }
+
+ /**
+ * Get the key the moves the selection one page down in the table. By
+ * default this is the Page Down key but by overriding this you can change
+ * the key to whatever you want.
+ *
+ * @return
+ */
+ protected int getNavigationPageDownKey() {
+ return KeyCodes.KEY_PAGEDOWN;
+ }
+
+ /**
+ * Get the key the moves the selection to the beginning of the table. By
+ * default this is the Home key but by overriding this you can change the
+ * key to whatever you want.
+ *
+ * @return
+ */
+ protected int getNavigationStartKey() {
+ return KeyCodes.KEY_HOME;
+ }
+
+ /**
+ * Get the key the moves the selection to the end of the table. By default
+ * this is the End key but by overriding this you can change the key to
+ * whatever you want.
+ *
+ * @return
+ */
+ protected int getNavigationEndKey() {
+ return KeyCodes.KEY_END;
+ }
+
+ private final String SUBPART_NODE_PREFIX = "n";
+ private final String EXPAND_IDENTIFIER = "expand";
+
+ /*
+ * In webkit, focus may have been requested for this component but not yet
+ * gained. Use this to trac if tree has gained the focus on webkit. See
+ * FocusImplSafari and #6373
+ */
+ private boolean treeHasFocus;
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.client.ui.SubPartAware#getSubPartElement(java
+ * .lang.String)
+ */
+ @Override
+ public com.google.gwt.user.client.Element getSubPartElement(
+ String subPart) {
+ if ("fe".equals(subPart)) {
+ if (BrowserInfo.get().isOpera() && focusedNode != null) {
+ return focusedNode.getElement();
+ }
+ return getFocusElement();
+ }
+
+ if (subPart.startsWith(SUBPART_NODE_PREFIX + "[")) {
+ boolean expandCollapse = false;
+
+ // Node
+ String[] nodes = subPart.split("/");
+ TreeNode treeNode = null;
+ try {
+ for (String node : nodes) {
+ if (node.startsWith(SUBPART_NODE_PREFIX)) {
+
+ // skip SUBPART_NODE_PREFIX"["
+ node = node.substring(SUBPART_NODE_PREFIX.length() + 1);
+ // skip "]"
+ node = node.substring(0, node.length() - 1);
+ int position = Integer.parseInt(node);
+ if (treeNode == null) {
+ treeNode = getRootNodes().get(position);
+ } else {
+ treeNode = treeNode.getChildren().get(position);
+ }
+ } else if (node.startsWith(EXPAND_IDENTIFIER)) {
+ expandCollapse = true;
+ }
+ }
+
+ if (expandCollapse) {
+ return treeNode.getElement();
+ } else {
+ return DOM.asOld(treeNode.nodeCaptionSpan);
+ }
+ } catch (Exception e) {
+ // Invalid locator string or node could not be found
+ return null;
+ }
+ }
+ return null;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.client.ui.SubPartAware#getSubPartName(com.google
+ * .gwt.user.client.Element)
+ */
+ @Override
+ public String getSubPartName(
+ com.google.gwt.user.client.Element subElement) {
+ // Supported identifiers:
+ //
+ // n[index]/n[index]/n[index]{/expand}
+ //
+ // Ends with "/expand" if the target is expand/collapse indicator,
+ // otherwise ends with the node
+
+ boolean isExpandCollapse = false;
+
+ if (!getElement().isOrHasChild(subElement)) {
+ return null;
+ }
+
+ if (subElement == getFocusElement()) {
+ return "fe";
+ }
+
+ TreeNode treeNode = WidgetUtil.findWidget(subElement, TreeNode.class);
+ if (treeNode == null) {
+ // Did not click on a node, let somebody else take care of the
+ // locator string
+ return null;
+ }
+
+ if (subElement == treeNode.getElement()) {
+ // Targets expand/collapse arrow
+ isExpandCollapse = true;
+ }
+
+ ArrayList<Integer> positions = new ArrayList<Integer>();
+ while (treeNode.getParentNode() != null) {
+ positions.add(0,
+ treeNode.getParentNode().getChildren().indexOf(treeNode));
+ treeNode = treeNode.getParentNode();
+ }
+ positions.add(0, getRootNodes().indexOf(treeNode));
+
+ String locator = "";
+ for (Integer i : positions) {
+ locator += SUBPART_NODE_PREFIX + "[" + i + "]/";
+ }
+
+ locator = locator.substring(0, locator.length() - 1);
+ if (isExpandCollapse) {
+ locator += "/" + EXPAND_IDENTIFIER;
+ }
+ return locator;
+ }
+
+ @Override
+ public Action[] getActions() {
+ if (bodyActionKeys == null) {
+ return new Action[] {};
+ }
+ final Action[] actions = new Action[bodyActionKeys.length];
+ for (int i = 0; i < actions.length; i++) {
+ final String actionKey = bodyActionKeys[i];
+ final TreeAction a = new TreeAction(this, null, actionKey);
+ a.setCaption(getActionCaption(actionKey));
+ a.setIconUrl(getActionIcon(actionKey));
+ actions[i] = a;
+ }
+ return actions;
+ }
+
+ @Override
+ public ApplicationConnection getClient() {
+ return client;
+ }
+
+ @Override
+ public String getPaintableId() {
+ return paintableId;
+ }
+
+ private void handleBodyContextMenu(ContextMenuEvent event) {
+ if (!readonly && !disabled) {
+ if (bodyActionKeys != null) {
+ int left = event.getNativeEvent().getClientX();
+ int top = event.getNativeEvent().getClientY();
+ top += Window.getScrollTop();
+ left += Window.getScrollLeft();
+ client.getContextMenu().showAt(this, left, top);
+ }
+ event.stopPropagation();
+ event.preventDefault();
+ }
+ }
+
+ public void registerAction(String key, String caption, String iconUrl) {
+ actionMap.put(key + "_c", caption);
+ if (iconUrl != null) {
+ actionMap.put(key + "_i", iconUrl);
+ } else {
+ actionMap.remove(key + "_i");
+ }
+
+ }
+
+ public void registerNode(TreeNode treeNode) {
+ keyToNode.put(treeNode.key, treeNode);
+ }
+
+ public void clearNodeToKeyMap() {
+ keyToNode.clear();
+ }
+
+ @Override
+ public void bindAriaCaption(
+ com.google.gwt.user.client.Element captionElement) {
+ AriaHelper.bindCaption(body, captionElement);
+ }
+
+ /**
+ * Tell LayoutManager that a layout is needed later for this VTree
+ */
+ private void doLayout() {
+ // This calls LayoutManager setNeedsMeasure and layoutNow
+ Util.notifyParentOfSizeChange(this, false);
+ }
+}
+
diff --git a/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/CalendarConnector.java b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/CalendarConnector.java
new file mode 100644
index 0000000000..f97ba9d14a
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/CalendarConnector.java
@@ -0,0 +1,713 @@
+/*
+ * Copyright 2000-2016 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.client.ui.calendar;
+
+import java.text.ParseException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+
+import com.google.gwt.core.shared.GWT;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.dom.client.NativeEvent;
+import com.google.gwt.event.dom.client.ContextMenuEvent;
+import com.google.gwt.i18n.client.DateTimeFormat;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.Window;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.client.ApplicationConnection;
+import com.vaadin.client.Paintable;
+import com.vaadin.client.TooltipInfo;
+import com.vaadin.client.UIDL;
+import com.vaadin.client.VConsole;
+import com.vaadin.client.WidgetUtil;
+import com.vaadin.client.communication.RpcProxy;
+import com.vaadin.client.communication.StateChangeEvent;
+import com.vaadin.client.ui.AbstractComponentConnector;
+import com.vaadin.client.ui.Action;
+import com.vaadin.client.ui.ActionOwner;
+import com.vaadin.client.ui.SimpleManagedLayout;
+import com.vaadin.client.ui.VCalendar;
+import com.vaadin.client.ui.VCalendar.BackwardListener;
+import com.vaadin.client.ui.VCalendar.DateClickListener;
+import com.vaadin.client.ui.VCalendar.EventClickListener;
+import com.vaadin.client.ui.VCalendar.EventMovedListener;
+import com.vaadin.client.ui.VCalendar.EventResizeListener;
+import com.vaadin.client.ui.VCalendar.ForwardListener;
+import com.vaadin.client.ui.VCalendar.MouseEventListener;
+import com.vaadin.client.ui.VCalendar.RangeSelectListener;
+import com.vaadin.client.ui.VCalendar.WeekClickListener;
+import com.vaadin.client.ui.calendar.schedule.CalendarDay;
+import com.vaadin.client.ui.calendar.schedule.CalendarEvent;
+import com.vaadin.client.ui.calendar.schedule.DateCell;
+import com.vaadin.client.ui.calendar.schedule.DateCell.DateCellSlot;
+import com.vaadin.client.ui.calendar.schedule.DateCellDayEvent;
+import com.vaadin.client.ui.calendar.schedule.DateUtil;
+import com.vaadin.client.ui.calendar.schedule.HasTooltipKey;
+import com.vaadin.client.ui.calendar.schedule.MonthEventLabel;
+import com.vaadin.client.ui.calendar.schedule.SimpleDayCell;
+import com.vaadin.client.ui.calendar.schedule.dd.CalendarDropHandler;
+import com.vaadin.client.ui.calendar.schedule.dd.CalendarMonthDropHandler;
+import com.vaadin.client.ui.calendar.schedule.dd.CalendarWeekDropHandler;
+import com.vaadin.shared.ui.Connect;
+import com.vaadin.shared.ui.Connect.LoadStyle;
+import com.vaadin.shared.ui.calendar.CalendarClientRpc;
+import com.vaadin.shared.ui.calendar.CalendarEventId;
+import com.vaadin.shared.ui.calendar.CalendarServerRpc;
+import com.vaadin.shared.ui.calendar.CalendarState;
+import com.vaadin.shared.ui.calendar.DateConstants;
+import com.vaadin.ui.Calendar;
+
+/**
+ * Handles communication between Calendar on the server side and
+ * {@link VCalendar} on the client side.
+ *
+ * @since 7.1
+ * @author Vaadin Ltd.
+ */
+@Connect(value = Calendar.class, loadStyle = LoadStyle.LAZY)
+public class CalendarConnector extends AbstractComponentConnector
+ implements ActionOwner, SimpleManagedLayout, Paintable {
+
+ private CalendarServerRpc rpc = RpcProxy.create(CalendarServerRpc.class,
+ this);
+
+ private final HashMap<String, String> actionMap = new HashMap<String, String>();
+ private HashMap<Object, String> tooltips = new HashMap<Object, String>();
+
+ private static final String DROPHANDLER_ACCEPT_CRITERIA_PAINT_TAG = "-ac";
+
+ /**
+ *
+ */
+ public CalendarConnector() {
+
+ // Listen to events
+ registerListeners();
+ }
+
+ @Override
+ protected void init() {
+ super.init();
+ registerRpc(CalendarClientRpc.class, new CalendarClientRpc() {
+ @Override
+ public void scroll(int scrollPosition) {
+ // TODO widget scroll
+ }
+ });
+ getLayoutManager().registerDependency(this, getWidget().getElement());
+ }
+
+ @Override
+ public void onUnregister() {
+ super.onUnregister();
+ getLayoutManager().unregisterDependency(this, getWidget().getElement());
+ }
+
+ @Override
+ public VCalendar getWidget() {
+ return (VCalendar) super.getWidget();
+ }
+
+ @Override
+ public CalendarState getState() {
+ return (CalendarState) super.getState();
+ }
+
+ /**
+ * Registers listeners on the calendar so server can be notified of the
+ * events
+ */
+ protected void registerListeners() {
+ getWidget().setListener(new DateClickListener() {
+ @Override
+ public void dateClick(String date) {
+ if (!getWidget().isDisabled()
+ && hasEventListener(CalendarEventId.DATECLICK)) {
+ rpc.dateClick(date);
+ }
+ }
+ });
+ getWidget().setListener(new ForwardListener() {
+ @Override
+ public void forward() {
+ if (hasEventListener(CalendarEventId.FORWARD)) {
+ rpc.forward();
+ }
+ }
+ });
+ getWidget().setListener(new BackwardListener() {
+ @Override
+ public void backward() {
+ if (hasEventListener(CalendarEventId.BACKWARD)) {
+ rpc.backward();
+ }
+ }
+ });
+ getWidget().setListener(new RangeSelectListener() {
+ @Override
+ public void rangeSelected(String value) {
+ if (hasEventListener(CalendarEventId.RANGESELECT)) {
+ rpc.rangeSelect(value);
+ }
+ }
+ });
+ getWidget().setListener(new WeekClickListener() {
+ @Override
+ public void weekClick(String event) {
+ if (!getWidget().isDisabled()
+ && hasEventListener(CalendarEventId.WEEKCLICK)) {
+ rpc.weekClick(event);
+ }
+ }
+ });
+ getWidget().setListener(new EventMovedListener() {
+ @Override
+ public void eventMoved(CalendarEvent event) {
+ if (hasEventListener(CalendarEventId.EVENTMOVE)) {
+ StringBuilder sb = new StringBuilder();
+ sb.append(DateUtil.formatClientSideDate(event.getStart()));
+ sb.append("-");
+ sb.append(DateUtil
+ .formatClientSideTime(event.getStartTime()));
+ rpc.eventMove(event.getIndex(), sb.toString());
+ }
+ }
+ });
+ getWidget().setListener(new EventResizeListener() {
+ @Override
+ public void eventResized(CalendarEvent event) {
+ if (hasEventListener(CalendarEventId.EVENTRESIZE)) {
+ StringBuilder buffer = new StringBuilder();
+
+ buffer.append(
+ DateUtil.formatClientSideDate(event.getStart()));
+ buffer.append("-");
+ buffer.append(DateUtil
+ .formatClientSideTime(event.getStartTime()));
+
+ String newStartDate = buffer.toString();
+
+ buffer = new StringBuilder();
+ buffer.append(
+ DateUtil.formatClientSideDate(event.getEnd()));
+ buffer.append("-");
+ buffer.append(
+ DateUtil.formatClientSideTime(event.getEndTime()));
+
+ String newEndDate = buffer.toString();
+
+ rpc.eventResize(event.getIndex(), newStartDate, newEndDate);
+ }
+ }
+ });
+ getWidget().setListener(new VCalendar.ScrollListener() {
+ @Override
+ public void scroll(int scrollPosition) {
+ // This call is @Delayed (== non-immediate)
+ rpc.scroll(scrollPosition);
+ }
+ });
+ getWidget().setListener(new EventClickListener() {
+ @Override
+ public void eventClick(CalendarEvent event) {
+ if (hasEventListener(CalendarEventId.EVENTCLICK)) {
+ rpc.eventClick(event.getIndex());
+ }
+ }
+ });
+ getWidget().setListener(new MouseEventListener() {
+ @Override
+ public void contextMenu(ContextMenuEvent event,
+ final Widget widget) {
+ final NativeEvent ne = event.getNativeEvent();
+ int left = ne.getClientX();
+ int top = ne.getClientY();
+ top += Window.getScrollTop();
+ left += Window.getScrollLeft();
+ getClient().getContextMenu().showAt(new ActionOwner() {
+ @Override
+ public String getPaintableId() {
+ return CalendarConnector.this.getPaintableId();
+ }
+
+ @Override
+ public ApplicationConnection getClient() {
+ return CalendarConnector.this.getClient();
+ }
+
+ @Override
+ @SuppressWarnings("deprecation")
+ public Action[] getActions() {
+ if (widget instanceof SimpleDayCell) {
+ /*
+ * Month view
+ */
+ SimpleDayCell cell = (SimpleDayCell) widget;
+ Date start = new Date(cell.getDate().getYear(),
+ cell.getDate().getMonth(),
+ cell.getDate().getDate(), 0, 0, 0);
+
+ Date end = new Date(cell.getDate().getYear(),
+ cell.getDate().getMonth(),
+ cell.getDate().getDate(), 23, 59, 59);
+
+ return CalendarConnector.this
+ .getActionsBetween(start, end);
+
+ } else if (widget instanceof MonthEventLabel) {
+ MonthEventLabel mel = (MonthEventLabel) widget;
+ CalendarEvent event = mel.getCalendarEvent();
+ Action[] actions = CalendarConnector.this
+ .getActionsBetween(event.getStartTime(),
+ event.getEndTime());
+ for (Action action : actions) {
+ ((VCalendarAction) action).setEvent(event);
+ }
+ return actions;
+
+ } else if (widget instanceof DateCell) {
+ /*
+ * Week and Day view
+ */
+ DateCell cell = (DateCell) widget;
+ int slotIndex = DOM.getChildIndex(cell.getElement(),
+ (Element) ne.getEventTarget().cast());
+ DateCellSlot slot = cell.getSlot(slotIndex);
+ return CalendarConnector.this.getActionsBetween(
+ slot.getFrom(), slot.getTo());
+ } else if (widget instanceof DateCellDayEvent) {
+ /*
+ * Context menu on event
+ */
+ DateCellDayEvent dayEvent = (DateCellDayEvent) widget;
+ CalendarEvent event = dayEvent.getCalendarEvent();
+
+ Action[] actions = CalendarConnector.this
+ .getActionsBetween(event.getStartTime(),
+ event.getEndTime());
+
+ for (Action action : actions) {
+ ((VCalendarAction) action).setEvent(event);
+ }
+
+ return actions;
+ }
+ return null;
+ }
+ }, left, top);
+ }
+ });
+ }
+
+ private boolean showingMonthView() {
+ return getState().days.size() > 7;
+ }
+
+ @Override
+ public void onStateChanged(StateChangeEvent stateChangeEvent) {
+ super.onStateChanged(stateChangeEvent);
+
+ CalendarState state = getState();
+ VCalendar widget = getWidget();
+
+ // Enable or disable the forward and backward navigation buttons
+ widget.setForwardNavigationEnabled(
+ hasEventListener(CalendarEventId.FORWARD));
+ widget.setBackwardNavigationEnabled(
+ hasEventListener(CalendarEventId.BACKWARD));
+
+ widget.set24HFormat(state.format24H);
+ widget.setDayNames(state.dayNames);
+ widget.setMonthNames(state.monthNames);
+ widget.setFirstDayNumber(state.firstVisibleDayOfWeek);
+ widget.setLastDayNumber(state.lastVisibleDayOfWeek);
+ widget.setFirstHourOfTheDay(state.firstHourOfDay);
+ widget.setLastHourOfTheDay(state.lastHourOfDay);
+ widget.setReadOnly(state.readOnly);
+ widget.setDisabled(!state.enabled);
+
+ widget.setRangeSelectAllowed(
+ hasEventListener(CalendarEventId.RANGESELECT));
+ widget.setRangeMoveAllowed(hasEventListener(CalendarEventId.EVENTMOVE));
+ widget.setEventMoveAllowed(hasEventListener(CalendarEventId.EVENTMOVE));
+ widget.setEventResizeAllowed(
+ hasEventListener(CalendarEventId.EVENTRESIZE));
+
+ widget.setEventCaptionAsHtml(state.eventCaptionAsHtml);
+
+ List<CalendarState.Day> days = state.days;
+ List<CalendarState.Event> events = state.events;
+
+ CalendarDropHandler dropHandler = getWidget().getDropHandler();
+ if (showingMonthView()) {
+ updateMonthView(days, events);
+ if (dropHandler != null
+ && !(dropHandler instanceof CalendarMonthDropHandler)) {
+ getWidget().setDropHandler(new CalendarMonthDropHandler(this));
+ }
+ } else {
+ updateWeekView(days, events);
+ if (dropHandler != null
+ && !(dropHandler instanceof CalendarWeekDropHandler)) {
+ getWidget().setDropHandler(new CalendarWeekDropHandler(this));
+ }
+ }
+
+ updateSizes();
+
+ registerEventToolTips(state.events);
+ updateActionMap(state.actions);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.terminal.gwt.client.Paintable#updateFromUIDL(com.vaadin.
+ * terminal .gwt.client.UIDL,
+ * com.vaadin.terminal.gwt.client.ApplicationConnection)
+ */
+ @Override
+ public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+ Iterator<Object> childIterator = uidl.getChildIterator();
+ while (childIterator.hasNext()) {
+ UIDL child = (UIDL) childIterator.next();
+ if (DROPHANDLER_ACCEPT_CRITERIA_PAINT_TAG.equals(child.getTag())) {
+ if (getWidget().getDropHandler() == null) {
+ getWidget().setDropHandler(showingMonthView()
+ ? new CalendarMonthDropHandler(this)
+ : new CalendarWeekDropHandler(this));
+ }
+ getWidget().getDropHandler().updateAcceptRules(child);
+ } else {
+ getWidget().setDropHandler(null);
+ }
+ }
+ }
+
+ /**
+ * Returns the ApplicationConnection used to connect to the server side
+ */
+ @Override
+ public ApplicationConnection getClient() {
+ return getConnection();
+ }
+
+ /**
+ * Register the description of the events as tooltips. This way, any event
+ * displaying widget can use the event index as a key to display the
+ * tooltip.
+ */
+ private void registerEventToolTips(List<CalendarState.Event> events) {
+ for (CalendarState.Event e : events) {
+ if (e.description != null && !"".equals(e.description)) {
+ tooltips.put(e.index, e.description);
+ } else {
+ tooltips.remove(e.index);
+ }
+ }
+ }
+
+ @Override
+ public TooltipInfo getTooltipInfo(
+ com.google.gwt.dom.client.Element element) {
+ TooltipInfo tooltipInfo = null;
+ Widget w = WidgetUtil.findWidget(element, null);
+ if (w instanceof HasTooltipKey) {
+ tooltipInfo = GWT.create(TooltipInfo.class);
+ String title = tooltips.get(((HasTooltipKey) w).getTooltipKey());
+ tooltipInfo.setTitle(title != null ? title : "");
+ }
+ if (tooltipInfo == null) {
+ tooltipInfo = super.getTooltipInfo(element);
+ }
+ return tooltipInfo;
+ }
+
+ @Override
+ public boolean hasTooltip() {
+ /*
+ * Tooltips are not processed until updateFromUIDL, so we can't be sure
+ * that there are no tooltips during onStateChange when this is used.
+ */
+ return true;
+ }
+
+ private void updateMonthView(List<CalendarState.Day> days,
+ List<CalendarState.Event> events) {
+ CalendarState state = getState();
+ getWidget().updateMonthView(state.firstDayOfWeek,
+ getWidget().getDateTimeFormat().parse(state.now), days.size(),
+ calendarEventListOf(events, state.format24H),
+ calendarDayListOf(days));
+ }
+
+ private void updateWeekView(List<CalendarState.Day> days,
+ List<CalendarState.Event> events) {
+ CalendarState state = getState();
+ getWidget().updateWeekView(state.scroll,
+ getWidget().getDateTimeFormat().parse(state.now), days.size(),
+ state.firstDayOfWeek,
+ calendarEventListOf(events, state.format24H),
+ calendarDayListOf(days));
+ }
+
+ private Action[] getActionsBetween(Date start, Date end) {
+ List<Action> actions = new ArrayList<Action>();
+ List<String> ids = new ArrayList<String>();
+
+ for (int i = 0; i < actionKeys.size(); i++) {
+ String actionKey = actionKeys.get(i);
+ String id = getActionID(actionKey);
+ if (!ids.contains(id)) {
+
+ Date actionStartDate;
+ Date actionEndDate;
+ try {
+ actionStartDate = getActionStartDate(actionKey);
+ actionEndDate = getActionEndDate(actionKey);
+ } catch (ParseException pe) {
+ VConsole.error("Failed to parse action date");
+ continue;
+ }
+
+ // Case 0: action inside event timeframe
+ // Action should start AFTER or AT THE SAME TIME as the event,
+ // and
+ // Action should end BEFORE or AT THE SAME TIME as the event
+ boolean test0 = actionStartDate.compareTo(start) >= 0
+ && actionEndDate.compareTo(end) <= 0;
+
+ // Case 1: action intersects start of timeframe
+ // Action end time must be between start and end of event
+ boolean test1 = actionEndDate.compareTo(start) > 0
+ && actionEndDate.compareTo(end) <= 0;
+
+ // Case 2: action intersects end of timeframe
+ // Action start time must be between start and end of event
+ boolean test2 = actionStartDate.compareTo(start) >= 0
+ && actionStartDate.compareTo(end) < 0;
+
+ // Case 3: event inside action timeframe
+ // Action should start AND END before the event is complete
+ boolean test3 = start.compareTo(actionStartDate) >= 0
+ && end.compareTo(actionEndDate) <= 0;
+
+ if (test0 || test1 || test2 || test3) {
+ VCalendarAction a = new VCalendarAction(this, rpc,
+ actionKey);
+ a.setCaption(getActionCaption(actionKey));
+ a.setIconUrl(getActionIcon(actionKey));
+ a.setActionStartDate(start);
+ a.setActionEndDate(end);
+ actions.add(a);
+ ids.add(id);
+ }
+ }
+ }
+
+ return actions.toArray(new Action[actions.size()]);
+ }
+
+ private List<String> actionKeys = new ArrayList<String>();
+
+ private void updateActionMap(List<CalendarState.Action> actions) {
+ actionMap.clear();
+ actionKeys.clear();
+
+ if (actions == null) {
+ return;
+ }
+
+ for (CalendarState.Action action : actions) {
+ String id = action.actionKey + "-" + action.startDate + "-"
+ + action.endDate;
+ actionMap.put(id + "_k", action.actionKey);
+ actionMap.put(id + "_c", action.caption);
+ actionMap.put(id + "_s", action.startDate);
+ actionMap.put(id + "_e", action.endDate);
+ actionKeys.add(id);
+ if (action.iconKey != null) {
+ actionMap.put(id + "_i", getResourceUrl(action.iconKey));
+
+ } else {
+ actionMap.remove(id + "_i");
+ }
+ }
+
+ Collections.sort(actionKeys);
+ }
+
+ /**
+ * Get the original action ID that was passed in from the shared state
+ *
+ * @since 7.1.2
+ * @param actionKey
+ * the unique action key
+ * @return
+ */
+ public String getActionID(String actionKey) {
+ return actionMap.get(actionKey + "_k");
+ }
+
+ /**
+ * Get the text that is displayed for a context menu item
+ *
+ * @param actionKey
+ * The unique action key
+ * @return
+ */
+ public String getActionCaption(String actionKey) {
+ return actionMap.get(actionKey + "_c");
+ }
+
+ /**
+ * Get the icon url for a context menu item
+ *
+ * @param actionKey
+ * The unique action key
+ * @return
+ */
+ public String getActionIcon(String actionKey) {
+ return actionMap.get(actionKey + "_i");
+ }
+
+ /**
+ * Get the start date for an action item
+ *
+ * @param actionKey
+ * The unique action key
+ * @return
+ * @throws ParseException
+ */
+ public Date getActionStartDate(String actionKey) throws ParseException {
+ String dateStr = actionMap.get(actionKey + "_s");
+ DateTimeFormat formatter = DateTimeFormat
+ .getFormat(DateConstants.ACTION_DATE_FORMAT_PATTERN);
+ return formatter.parse(dateStr);
+ }
+
+ /**
+ * Get the end date for an action item
+ *
+ * @param actionKey
+ * The unique action key
+ * @return
+ * @throws ParseException
+ */
+ public Date getActionEndDate(String actionKey) throws ParseException {
+ String dateStr = actionMap.get(actionKey + "_e");
+ DateTimeFormat formatter = DateTimeFormat
+ .getFormat(DateConstants.ACTION_DATE_FORMAT_PATTERN);
+ return formatter.parse(dateStr);
+ }
+
+ /**
+ * Returns ALL currently registered events. Use {@link #getActions(Date)} to
+ * get the actions for a specific date
+ */
+ @Override
+ public Action[] getActions() {
+ List<Action> actions = new ArrayList<Action>();
+ for (int i = 0; i < actionKeys.size(); i++) {
+ final String actionKey = actionKeys.get(i);
+ final VCalendarAction a = new VCalendarAction(this, rpc, actionKey);
+ a.setCaption(getActionCaption(actionKey));
+ a.setIconUrl(getActionIcon(actionKey));
+
+ try {
+ a.setActionStartDate(getActionStartDate(actionKey));
+ a.setActionEndDate(getActionEndDate(actionKey));
+ } catch (ParseException pe) {
+ VConsole.error(pe);
+ }
+
+ actions.add(a);
+ }
+ return actions.toArray(new Action[actions.size()]);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.terminal.gwt.client.ui.ActionOwner#getPaintableId()
+ */
+ @Override
+ public String getPaintableId() {
+ return getConnectorId();
+ }
+
+ private List<CalendarEvent> calendarEventListOf(
+ List<CalendarState.Event> events, boolean format24h) {
+ List<CalendarEvent> list = new ArrayList<CalendarEvent>(events.size());
+ for (CalendarState.Event event : events) {
+ final String dateFrom = event.dateFrom;
+ final String dateTo = event.dateTo;
+ final String timeFrom = event.timeFrom;
+ final String timeTo = event.timeTo;
+ CalendarEvent calendarEvent = new CalendarEvent();
+ calendarEvent.setAllDay(event.allDay);
+ calendarEvent.setCaption(event.caption);
+ calendarEvent.setDescription(event.description);
+ calendarEvent.setStart(getWidget().getDateFormat().parse(dateFrom));
+ calendarEvent.setEnd(getWidget().getDateFormat().parse(dateTo));
+ calendarEvent.setFormat24h(format24h);
+ calendarEvent.setStartTime(getWidget().getDateTimeFormat()
+ .parse(dateFrom + " " + timeFrom));
+ calendarEvent.setEndTime(getWidget().getDateTimeFormat()
+ .parse(dateTo + " " + timeTo));
+ calendarEvent.setStyleName(event.styleName);
+ calendarEvent.setIndex(event.index);
+ list.add(calendarEvent);
+ }
+ return list;
+ }
+
+ private List<CalendarDay> calendarDayListOf(List<CalendarState.Day> days) {
+ List<CalendarDay> list = new ArrayList<CalendarDay>(days.size());
+ for (CalendarState.Day day : days) {
+ CalendarDay d = new CalendarDay(day.date, day.localizedDateFormat,
+ day.dayOfWeek, day.week, day.yearOfWeek);
+
+ list.add(d);
+ }
+ return list;
+ }
+
+ @Override
+ public void layout() {
+ updateSizes();
+ }
+
+ private void updateSizes() {
+ int height = getLayoutManager()
+ .getOuterHeight(getWidget().getElement());
+ int width = getLayoutManager().getOuterWidth(getWidget().getElement());
+
+ if (isUndefinedWidth()) {
+ width = -1;
+ }
+ if (isUndefinedHeight()) {
+ height = -1;
+ }
+
+ getWidget().setSizeForChildren(width, height);
+
+ }
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/VCalendarAction.java b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/VCalendarAction.java
new file mode 100644
index 0000000000..f16a43fbb2
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/VCalendarAction.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright 2000-2016 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.client.ui.calendar;
+
+import java.util.Date;
+
+import com.google.gwt.i18n.client.DateTimeFormat;
+import com.vaadin.client.ui.Action;
+import com.vaadin.client.ui.calendar.schedule.CalendarEvent;
+import com.vaadin.shared.ui.calendar.CalendarServerRpc;
+import com.vaadin.shared.ui.calendar.DateConstants;
+
+/**
+ * Action performed by the calendar
+ *
+ * @since 7.1
+ * @author Vaadin Ltd.
+ */
+public class VCalendarAction extends Action {
+
+ private CalendarServerRpc rpc;
+
+ private String actionKey = "";
+
+ private Date actionStartDate;
+
+ private Date actionEndDate;
+
+ private CalendarEvent event;
+
+ private final DateTimeFormat dateformat_datetime = DateTimeFormat
+ .getFormat(DateConstants.ACTION_DATE_FORMAT_PATTERN);
+
+ /**
+ *
+ * @param owner
+ */
+ public VCalendarAction(CalendarConnector owner) {
+ super(owner);
+ }
+
+ /**
+ * Constructor
+ *
+ * @param owner
+ * The owner who trigger this kinds of events
+ * @param rpc
+ * The CalendarRpc which is used for executing actions
+ * @param key
+ * The unique action key which identifies this particular action
+ */
+ public VCalendarAction(CalendarConnector owner, CalendarServerRpc rpc,
+ String key) {
+ this(owner);
+ this.rpc = rpc;
+ actionKey = key;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.terminal.gwt.client.ui.Action#execute()
+ */
+ @Override
+ public void execute() {
+ String startDate = dateformat_datetime.format(actionStartDate);
+ String endDate = dateformat_datetime.format(actionEndDate);
+
+ if (event == null) {
+ rpc.actionOnEmptyCell(actionKey.split("-")[0], startDate, endDate);
+ } else {
+ rpc.actionOnEvent(actionKey.split("-")[0], startDate, endDate,
+ event.getIndex());
+ }
+
+ owner.getClient().getContextMenu().hide();
+ }
+
+ /**
+ * Get the date and time when the action starts
+ *
+ * @return
+ */
+ public Date getActionStartDate() {
+ return actionStartDate;
+ }
+
+ /**
+ * Set the date when the actions start
+ *
+ * @param actionStartDate
+ * The date and time when the action starts
+ */
+ public void setActionStartDate(Date actionStartDate) {
+ this.actionStartDate = actionStartDate;
+ }
+
+ /**
+ * Get the date and time when the action ends
+ *
+ * @return
+ */
+ public Date getActionEndDate() {
+ return actionEndDate;
+ }
+
+ /**
+ * Set the date and time when the action ends
+ *
+ * @param actionEndDate
+ * The date and time when the action ends
+ */
+ public void setActionEndDate(Date actionEndDate) {
+ this.actionEndDate = actionEndDate;
+ }
+
+ public CalendarEvent getEvent() {
+ return event;
+ }
+
+ public void setEvent(CalendarEvent event) {
+ this.event = event;
+ }
+
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/CalendarDay.java b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/CalendarDay.java
new file mode 100644
index 0000000000..c2ade39a6d
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/CalendarDay.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2000-2016 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.client.ui.calendar.schedule;
+
+/**
+ * Utility class used to represent a day when updating views. Only used
+ * internally.
+ *
+ * @since 7.1
+ * @author Vaadin Ltd.
+ */
+public class CalendarDay {
+ private String date;
+ private String localizedDateFormat;
+ private int dayOfWeek;
+ private int week;
+ private int yearOfWeek;
+
+ public CalendarDay(String date, String localizedDateFormat, int dayOfWeek,
+ int week, int yearOfWeek) {
+ super();
+ this.date = date;
+ this.localizedDateFormat = localizedDateFormat;
+ this.dayOfWeek = dayOfWeek;
+ this.week = week;
+ this.yearOfWeek = yearOfWeek;
+ }
+
+ public String getDate() {
+ return date;
+ }
+
+ public String getLocalizedDateFormat() {
+ return localizedDateFormat;
+ }
+
+ public int getDayOfWeek() {
+ return dayOfWeek;
+ }
+
+ public int getWeek() {
+ return week;
+ }
+
+ public int getYearOfWeek() {
+ return yearOfWeek;
+ }
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/CalendarEvent.java b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/CalendarEvent.java
new file mode 100644
index 0000000000..937b7c0ccb
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/CalendarEvent.java
@@ -0,0 +1,320 @@
+/*
+ * Copyright 2000-2016 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.client.ui.calendar.schedule;
+
+import java.util.Date;
+
+import com.google.gwt.i18n.client.DateTimeFormat;
+import com.vaadin.shared.ui.calendar.DateConstants;
+
+/**
+ * A client side implementation of a calendar event
+ *
+ * @since 7.1
+ * @author Vaadin Ltd.
+ */
+public class CalendarEvent {
+ private int index;
+ private String caption;
+ private Date start, end;
+ private String styleName;
+ private Date startTime, endTime;
+ private String description;
+ private int slotIndex = -1;
+ private boolean format24h;
+
+ DateTimeFormat dateformat_date = DateTimeFormat.getFormat("h:mm a");
+ DateTimeFormat dateformat_date24 = DateTimeFormat.getFormat("H:mm");
+ private boolean allDay;
+
+ /**
+ * @see com.vaadin.addon.calendar.event.CalendarEvent#getStyleName()
+ */
+ public String getStyleName() {
+ return styleName;
+ }
+
+ /**
+ * @see com.vaadin.addon.calendar.event.CalendarEvent#getStart()
+ */
+ public Date getStart() {
+ return start;
+ }
+
+ /**
+ * @see com.vaadin.addon.calendar.event.CalendarEvent#getStyleName()
+ * @param style
+ */
+ public void setStyleName(String style) {
+ styleName = style;
+ }
+
+ /**
+ * @see com.vaadin.addon.calendar.event.CalendarEvent#getStart()
+ * @param start
+ */
+ public void setStart(Date start) {
+ this.start = start;
+ }
+
+ /**
+ * @see com.vaadin.addon.calendar.event.CalendarEvent#getEnd()
+ * @return
+ */
+ public Date getEnd() {
+ return end;
+ }
+
+ /**
+ * @see com.vaadin.addon.calendar.event.CalendarEvent#getEnd()
+ * @param end
+ */
+ public void setEnd(Date end) {
+ this.end = end;
+ }
+
+ /**
+ * Returns the start time of the event
+ *
+ * @return Time embedded in the {@link Date} object
+ */
+ public Date getStartTime() {
+ return startTime;
+ }
+
+ /**
+ * Set the start time of the event
+ *
+ * @param startTime
+ * The time of the event. Use the time fields in the {@link Date}
+ * object
+ */
+ public void setStartTime(Date startTime) {
+ this.startTime = startTime;
+ }
+
+ /**
+ * Get the end time of the event
+ *
+ * @return Time embedded in the {@link Date} object
+ */
+ public Date getEndTime() {
+ return endTime;
+ }
+
+ /**
+ * Set the end time of the event
+ *
+ * @param endTime
+ * Time embedded in the {@link Date} object
+ */
+ public void setEndTime(Date endTime) {
+ this.endTime = endTime;
+ }
+
+ /**
+ * Get the (server side) index of the event
+ *
+ * @return
+ */
+ public int getIndex() {
+ return index;
+ }
+
+ /**
+ * Get the index of the slot where the event in rendered
+ *
+ * @return
+ */
+ public int getSlotIndex() {
+ return slotIndex;
+ }
+
+ /**
+ * Set the index of the slot where the event in rendered
+ *
+ * @param index
+ * The index of the slot
+ */
+ public void setSlotIndex(int index) {
+ slotIndex = index;
+ }
+
+ /**
+ * Set the (server side) index of the event
+ *
+ * @param index
+ * The index
+ */
+ public void setIndex(int index) {
+ this.index = index;
+ }
+
+ /**
+ * Get the caption of the event. The caption is the text displayed in the
+ * calendar on the event.
+ *
+ * @return
+ */
+ public String getCaption() {
+ return caption;
+ }
+
+ /**
+ * Set the caption of the event. The caption is the text displayed in the
+ * calendar on the event.
+ *
+ * @param caption
+ * The visible caption of the event
+ */
+ public void setCaption(String caption) {
+ this.caption = caption;
+ }
+
+ /**
+ * Get the description of the event. The description is the text displayed
+ * when hoovering over the event with the mouse
+ *
+ * @return
+ */
+ public String getDescription() {
+ return description;
+ }
+
+ /**
+ * Set the description of the event. The description is the text displayed
+ * when hoovering over the event with the mouse
+ *
+ * @param description
+ */
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ /**
+ * Does the event use the 24h time format
+ *
+ * @param format24h
+ * True if it uses the 24h format, false if it uses the 12h time
+ * format
+ */
+ public void setFormat24h(boolean format24h) {
+ this.format24h = format24h;
+ }
+
+ /**
+ * Is the event an all day event.
+ *
+ * @param allDay
+ * True if the event should be rendered all day
+ */
+ public void setAllDay(boolean allDay) {
+ this.allDay = allDay;
+ }
+
+ /**
+ * Is the event an all day event.
+ *
+ * @return
+ */
+ public boolean isAllDay() {
+ return allDay;
+ }
+
+ /**
+ * Get the time as a formatted string
+ *
+ * @return
+ */
+ public String getTimeAsText() {
+ if (format24h) {
+ return dateformat_date24.format(startTime);
+ } else {
+ return dateformat_date.format(startTime);
+ }
+ }
+
+ /**
+ * Get the amount of milliseconds between the start and end of the event
+ *
+ * @return
+ */
+ public long getRangeInMilliseconds() {
+ return getEndTime().getTime() - getStartTime().getTime();
+ }
+
+ /**
+ * Get the amount of minutes between the start and end of the event
+ *
+ * @return
+ */
+ public long getRangeInMinutes() {
+ return (getRangeInMilliseconds() / DateConstants.MINUTEINMILLIS);
+ }
+
+ /**
+ * Get the amount of minutes for the event on a specific day. This is useful
+ * if the event spans several days.
+ *
+ * @param targetDay
+ * The date to check
+ * @return
+ */
+ public long getRangeInMinutesForDay(Date targetDay) {
+ long rangeInMinutesForDay = 0;
+ // we must take into account that here can be not only 1 and 2 days, but
+ // 1, 2, 3, 4... days first and last days - special cases all another
+ // days between first and last - have range "ALL DAY"
+ if (isTimeOnDifferentDays()) {
+ if (targetDay.compareTo(getStart()) == 0) { // for first day
+ rangeInMinutesForDay = DateConstants.DAYINMINUTES
+ - (getStartTime().getTime() - getStart().getTime())
+ / DateConstants.MINUTEINMILLIS;
+ } else if (targetDay.compareTo(getEnd()) == 0) { // for last day
+ rangeInMinutesForDay = (getEndTime().getTime()
+ - getEnd().getTime()) / DateConstants.MINUTEINMILLIS;
+ } else { // for in-between days
+ rangeInMinutesForDay = DateConstants.DAYINMINUTES;
+ }
+ } else { // simple case - period is in one day
+ rangeInMinutesForDay = getRangeInMinutes();
+ }
+ return rangeInMinutesForDay;
+ }
+
+ /**
+ * Does the event span several days
+ *
+ * @return
+ */
+ @SuppressWarnings("deprecation")
+ public boolean isTimeOnDifferentDays() {
+ boolean isSeveralDays = false;
+
+ // if difference between start and end times is more than day - of
+ // course it is not one day, but several days
+ if (getEndTime().getTime()
+ - getStartTime().getTime() > DateConstants.DAYINMILLIS) {
+ isSeveralDays = true;
+ } else { // if difference <= day -> there can be different cases
+ if (getStart().compareTo(getEnd()) != 0
+ && getEndTime().compareTo(getEnd()) != 0) {
+ isSeveralDays = true;
+ }
+ }
+ return isSeveralDays;
+ }
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/DateCell.java b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/DateCell.java
new file mode 100644
index 0000000000..2590c4ed03
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/DateCell.java
@@ -0,0 +1,850 @@
+/*
+ * Copyright 2000-2016 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.client.ui.calendar.schedule;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.dom.client.NativeEvent;
+import com.google.gwt.dom.client.Node;
+import com.google.gwt.dom.client.NodeList;
+import com.google.gwt.dom.client.Style.Display;
+import com.google.gwt.dom.client.Style.Unit;
+import com.google.gwt.event.dom.client.ContextMenuEvent;
+import com.google.gwt.event.dom.client.ContextMenuHandler;
+import com.google.gwt.event.dom.client.KeyCodes;
+import com.google.gwt.event.dom.client.KeyDownEvent;
+import com.google.gwt.event.dom.client.KeyDownHandler;
+import com.google.gwt.event.dom.client.MouseDownEvent;
+import com.google.gwt.event.dom.client.MouseDownHandler;
+import com.google.gwt.event.dom.client.MouseMoveEvent;
+import com.google.gwt.event.dom.client.MouseMoveHandler;
+import com.google.gwt.event.dom.client.MouseUpEvent;
+import com.google.gwt.event.dom.client.MouseUpHandler;
+import com.google.gwt.event.shared.HandlerRegistration;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.Event;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.client.WidgetUtil;
+
+public class DateCell extends FocusableComplexPanel implements MouseDownHandler,
+ MouseMoveHandler, MouseUpHandler, KeyDownHandler, ContextMenuHandler {
+ private static final String DRAGEMPHASISSTYLE = " dragemphasis";
+ private Date date;
+ private int width;
+ private int eventRangeStart = -1;
+ private int eventRangeStop = -1;
+ final WeekGrid weekgrid;
+ private boolean disabled = false;
+ private int height;
+ private final Element[] slotElements;
+ private final List<DateCellSlot> slots = new ArrayList<DateCell.DateCellSlot>();
+ private int[] slotElementHeights;
+ private int startingSlotHeight;
+ private Date today;
+ private Element todaybar;
+ private final List<HandlerRegistration> handlers;
+ private final int numberOfSlots;
+ private final int firstHour;
+ private final int lastHour;
+
+ public class DateCellSlot extends Widget {
+
+ private final DateCell cell;
+
+ private final Date from;
+
+ private final Date to;
+
+ public DateCellSlot(DateCell cell, Date from, Date to) {
+ setElement(DOM.createDiv());
+ getElement().setInnerHTML("&nbsp;");
+ this.cell = cell;
+ this.from = from;
+ this.to = to;
+ }
+
+ public Date getFrom() {
+ return from;
+ }
+
+ public Date getTo() {
+ return to;
+ }
+
+ public DateCell getParentCell() {
+ return cell;
+ }
+ }
+
+ public DateCell(WeekGrid parent, Date date) {
+ weekgrid = parent;
+ Element mainElement = DOM.createDiv();
+ setElement(mainElement);
+ makeFocusable();
+ setDate(date);
+
+ addStyleName("v-calendar-day-times");
+
+ handlers = new LinkedList<HandlerRegistration>();
+
+ // 2 slots / hour
+ firstHour = weekgrid.getFirstHour();
+ lastHour = weekgrid.getLastHour();
+ numberOfSlots = (lastHour - firstHour + 1) * 2;
+ long slotTime = Math.round(
+ ((lastHour - firstHour + 1) * 3600000.0) / numberOfSlots);
+
+ slotElements = new Element[numberOfSlots];
+ slotElementHeights = new int[numberOfSlots];
+
+ slots.clear();
+ long start = getDate().getTime() + firstHour * 3600000;
+ long end = start + slotTime;
+ for (int i = 0; i < numberOfSlots; i++) {
+ DateCellSlot slot = new DateCellSlot(this, new Date(start),
+ new Date(end));
+ if (i % 2 == 0) {
+ slot.setStyleName("v-datecellslot-even");
+ } else {
+ slot.setStyleName("v-datecellslot");
+ }
+ Event.sinkEvents(slot.getElement(), Event.MOUSEEVENTS);
+ mainElement.appendChild(slot.getElement());
+ slotElements[i] = slot.getElement();
+ slots.add(slot);
+ start = end;
+ end = start + slotTime;
+ }
+
+ // Sink events for tooltip handling
+ Event.sinkEvents(mainElement, Event.MOUSEEVENTS);
+ }
+
+ public int getFirstHour() {
+ return firstHour;
+ }
+
+ public int getLastHour() {
+ return lastHour;
+ }
+
+ @Override
+ protected void onAttach() {
+ super.onAttach();
+
+ handlers.add(addHandler(this, MouseDownEvent.getType()));
+ handlers.add(addHandler(this, MouseUpEvent.getType()));
+ handlers.add(addHandler(this, MouseMoveEvent.getType()));
+ handlers.add(addDomHandler(this, ContextMenuEvent.getType()));
+ handlers.add(addKeyDownHandler(this));
+ }
+
+ @Override
+ protected void onDetach() {
+ for (HandlerRegistration handler : handlers) {
+ handler.removeHandler();
+ }
+ handlers.clear();
+
+ super.onDetach();
+ }
+
+ public int getSlotIndex(Element slotElement) {
+ for (int i = 0; i < slotElements.length; i++) {
+ if (slotElement == slotElements[i]) {
+ return i;
+ }
+ }
+
+ throw new IllegalArgumentException(
+ "Element not found in this DateCell");
+ }
+
+ public DateCellSlot getSlot(int index) {
+ return slots.get(index);
+ }
+
+ public int getNumberOfSlots() {
+ return numberOfSlots;
+ }
+
+ public void setTimeBarWidth(int timebarWidth) {
+ todaybar.getStyle().setWidth(timebarWidth, Unit.PX);
+ }
+
+ /**
+ * @param isHorizontalSized
+ * if true, this DateCell is sized with CSS and not via
+ * {@link #setWidthPX(int)}
+ */
+ public void setHorizontalSized(boolean isHorizontalSized) {
+ if (isHorizontalSized) {
+ addStyleDependentName("Hsized");
+
+ width = getOffsetWidth()
+ - WidgetUtil.measureHorizontalBorder(getElement());
+ // Update moveWidth for any DateCellDayEvent child
+ updateEventCellsWidth();
+ recalculateEventWidths();
+ } else {
+ removeStyleDependentName("Hsized");
+ }
+ }
+
+ /**
+ * @param isVerticalSized
+ * if true, this DateCell is sized with CSS and not via
+ * {@link #setHeightPX(int)}
+ */
+ public void setVerticalSized(boolean isVerticalSized) {
+ if (isVerticalSized) {
+ addStyleDependentName("Vsized");
+
+ // recalc heights&size for events. all other height sizes come
+ // from css
+ startingSlotHeight = slotElements[0].getOffsetHeight();
+ // Update slotHeight for each DateCellDayEvent child
+ updateEventCellsHeight();
+ recalculateEventPositions();
+
+ if (isToday()) {
+ recalculateTimeBarPosition();
+ }
+
+ } else {
+ removeStyleDependentName("Vsized");
+ }
+ }
+
+ public void setDate(Date date) {
+ this.date = date;
+ }
+
+ public void setWidthPX(int cellWidth) {
+ width = cellWidth;
+ setWidth(cellWidth + "px");
+ recalculateEventWidths();
+ }
+
+ public void setHeightPX(int height, int[] cellHeights) {
+ this.height = height;
+ slotElementHeights = cellHeights;
+ setHeight(height + "px");
+ recalculateCellHeights();
+ recalculateEventPositions();
+ if (today != null) {
+ recalculateTimeBarPosition();
+ }
+ }
+
+ // date methods are not deprecated in GWT
+ @SuppressWarnings("deprecation")
+ private void recalculateTimeBarPosition() {
+ int h = today.getHours();
+ int m = today.getMinutes();
+ if (h >= firstHour && h <= lastHour) {
+ int pixelTop = weekgrid.getPixelTopFor(m + 60 * h);
+ todaybar.getStyle().clearDisplay();
+ todaybar.getStyle().setTop(pixelTop, Unit.PX);
+ } else {
+ todaybar.getStyle().setDisplay(Display.NONE);
+ }
+ }
+
+ private void recalculateEventPositions() {
+ for (int i = 0; i < getWidgetCount(); i++) {
+ DateCellDayEvent dayEvent = (DateCellDayEvent) getWidget(i);
+ updatePositionFor(dayEvent, getDate(), dayEvent.getCalendarEvent());
+ }
+ }
+
+ public void recalculateEventWidths() {
+ List<DateCellGroup> groups = new ArrayList<DateCellGroup>();
+
+ int count = getWidgetCount();
+
+ List<Integer> handled = new ArrayList<Integer>();
+
+ // Iterate through all events and group them. Events that overlaps
+ // with each other, are added to the same group.
+ for (int i = 0; i < count; i++) {
+ if (handled.contains(i)) {
+ continue;
+ }
+
+ DateCellGroup curGroup = getOverlappingEvents(i);
+ handled.addAll(curGroup.getItems());
+
+ boolean newGroup = true;
+ // No need to check other groups, if size equals the count
+ if (curGroup.getItems().size() != count) {
+ // Check other groups. When the whole group overlaps with
+ // other group, the group is merged to the other.
+ for (DateCellGroup g : groups) {
+
+ if (WeekGridMinuteTimeRange.doesOverlap(
+ curGroup.getDateRange(), g.getDateRange())) {
+ newGroup = false;
+ updateGroup(g, curGroup);
+ }
+ }
+ } else {
+ if (newGroup) {
+ groups.add(curGroup);
+ }
+ break;
+ }
+
+ if (newGroup) {
+ groups.add(curGroup);
+ }
+ }
+
+ drawDayEvents(groups);
+ }
+
+ private void recalculateCellHeights() {
+ startingSlotHeight = height / numberOfSlots;
+
+ for (int i = 0; i < slotElements.length; i++) {
+ slotElements[i].getStyle().setHeight(slotElementHeights[i],
+ Unit.PX);
+ }
+
+ updateEventCellsHeight();
+ }
+
+ public int getSlotHeight() {
+ return startingSlotHeight;
+ }
+
+ public int getSlotBorder() {
+ return WidgetUtil.measureVerticalBorder(slotElements[0]);
+ }
+
+ private void drawDayEvents(List<DateCellGroup> groups) {
+ for (DateCellGroup g : groups) {
+ int col = 0;
+ int colCount = 0;
+ List<Integer> order = new ArrayList<Integer>();
+ Map<Integer, Integer> columns = new HashMap<Integer, Integer>();
+ for (Integer eventIndex : g.getItems()) {
+ DateCellDayEvent d = (DateCellDayEvent) getWidget(eventIndex);
+ d.setMoveWidth(width);
+
+ int freeSpaceCol = findFreeColumnSpaceOnLeft(
+ new WeekGridMinuteTimeRange(
+ d.getCalendarEvent().getStartTime(),
+ d.getCalendarEvent().getEndTime()),
+ order, columns);
+ if (freeSpaceCol >= 0) {
+ col = freeSpaceCol;
+ columns.put(eventIndex, col);
+ int newOrderindex = 0;
+ for (Integer i : order) {
+ if (columns.get(i) >= col) {
+ newOrderindex = order.indexOf(i);
+ break;
+ }
+ }
+ order.add(newOrderindex, eventIndex);
+ } else {
+ // New column
+ col = colCount++;
+ columns.put(eventIndex, col);
+ order.add(eventIndex);
+ }
+ }
+
+ // Update widths and left position
+ int eventWidth = (width / colCount);
+ for (Integer index : g.getItems()) {
+ DateCellDayEvent d = (DateCellDayEvent) getWidget(index);
+ d.getElement().getStyle().setMarginLeft(
+ (eventWidth * columns.get(index)), Unit.PX);
+ d.setWidth(eventWidth + "px");
+ d.setSlotHeightInPX(getSlotHeight());
+ }
+ }
+ }
+
+ private int findFreeColumnSpaceOnLeft(WeekGridMinuteTimeRange dateRange,
+ List<Integer> order, Map<Integer, Integer> columns) {
+ int freeSpot = -1;
+ int skipIndex = -1;
+ for (Integer eventIndex : order) {
+ int col = columns.get(eventIndex);
+ if (col == skipIndex) {
+ continue;
+ }
+
+ if (freeSpot != -1 && freeSpot != col) {
+ // Free spot found
+ return freeSpot;
+ }
+
+ DateCellDayEvent d = (DateCellDayEvent) getWidget(eventIndex);
+ WeekGridMinuteTimeRange nextRange = new WeekGridMinuteTimeRange(
+ d.getCalendarEvent().getStartTime(),
+ d.getCalendarEvent().getEndTime());
+
+ if (WeekGridMinuteTimeRange.doesOverlap(dateRange, nextRange)) {
+ skipIndex = col;
+ freeSpot = -1;
+ } else {
+ freeSpot = col;
+ }
+ }
+
+ return freeSpot;
+ }
+
+ /* Update top and bottom date range values. Add new index to the group. */
+ private void updateGroup(DateCellGroup targetGroup, DateCellGroup byGroup) {
+ Date newStart = targetGroup.getStart();
+ Date newEnd = targetGroup.getEnd();
+ if (byGroup.getStart().before(targetGroup.getStart())) {
+ newStart = byGroup.getEnd();
+ }
+ if (byGroup.getStart().after(targetGroup.getEnd())) {
+ newStart = byGroup.getStart();
+ }
+
+ targetGroup.setDateRange(new WeekGridMinuteTimeRange(newStart, newEnd));
+
+ for (Integer index : byGroup.getItems()) {
+ if (!targetGroup.getItems().contains(index)) {
+ targetGroup.add(index);
+ }
+ }
+ }
+
+ /**
+ * Returns all overlapping DayEvent indexes in the Group. Including the
+ * target.
+ *
+ * @param targetIndex
+ * Index of DayEvent in the current DateCell widget.
+ * @return Group that contains all Overlapping DayEvent indexes
+ */
+ public DateCellGroup getOverlappingEvents(int targetIndex) {
+ DateCellGroup g = new DateCellGroup(targetIndex);
+
+ int count = getWidgetCount();
+ DateCellDayEvent target = (DateCellDayEvent) getWidget(targetIndex);
+ WeekGridMinuteTimeRange targetRange = new WeekGridMinuteTimeRange(
+ target.getCalendarEvent().getStartTime(),
+ target.getCalendarEvent().getEndTime());
+ Date groupStart = targetRange.getStart();
+ Date groupEnd = targetRange.getEnd();
+
+ for (int i = 0; i < count; i++) {
+ if (targetIndex == i) {
+ continue;
+ }
+
+ DateCellDayEvent d = (DateCellDayEvent) getWidget(i);
+ WeekGridMinuteTimeRange nextRange = new WeekGridMinuteTimeRange(
+ d.getCalendarEvent().getStartTime(),
+ d.getCalendarEvent().getEndTime());
+ if (WeekGridMinuteTimeRange.doesOverlap(targetRange, nextRange)) {
+ g.add(i);
+
+ // Update top & bottom values to the greatest
+ if (nextRange.getStart().before(targetRange.getStart())) {
+ groupStart = targetRange.getStart();
+ }
+ if (nextRange.getEnd().after(targetRange.getEnd())) {
+ groupEnd = targetRange.getEnd();
+ }
+ }
+ }
+
+ g.setDateRange(new WeekGridMinuteTimeRange(groupStart, groupEnd));
+ return g;
+ }
+
+ public Date getDate() {
+ return date;
+ }
+
+ public void addEvent(Date targetDay, CalendarEvent calendarEvent) {
+ Element main = getElement();
+ DateCellDayEvent dayEvent = new DateCellDayEvent(this, weekgrid,
+ calendarEvent);
+ dayEvent.setSlotHeightInPX(getSlotHeight());
+ dayEvent.setDisabled(isDisabled());
+
+ if (startingSlotHeight > 0) {
+ updatePositionFor(dayEvent, targetDay, calendarEvent);
+ }
+
+ add(dayEvent, main);
+ }
+
+ // date methods are not deprecated in GWT
+ @SuppressWarnings("deprecation")
+ private void updatePositionFor(DateCellDayEvent dayEvent, Date targetDay,
+ CalendarEvent calendarEvent) {
+
+ if (shouldDisplay(calendarEvent)) {
+ dayEvent.getElement().getStyle().clearDisplay();
+
+ Date fromDt = calendarEvent.getStartTime();
+ int h = fromDt.getHours();
+ int m = fromDt.getMinutes();
+ long range = calendarEvent.getRangeInMinutesForDay(targetDay);
+
+ boolean onDifferentDays = calendarEvent.isTimeOnDifferentDays();
+ if (onDifferentDays) {
+ if (calendarEvent.getStart().compareTo(targetDay) != 0) {
+ // Current day slot is for the end date and all in-between
+ // days. Lets fix also the start & end times.
+ h = 0;
+ m = 0;
+ }
+ }
+
+ int startFromMinutes = (h * 60) + m;
+ dayEvent.updatePosition(startFromMinutes, range);
+ } else {
+ dayEvent.getElement().getStyle().setDisplay(Display.NONE);
+ }
+ }
+
+ public void addEvent(DateCellDayEvent dayEvent) {
+ Element main = getElement();
+ int index = 0;
+ List<CalendarEvent> events = new ArrayList<CalendarEvent>();
+
+ // events are the only widgets in this panel
+ // slots are just elements
+ for (; index < getWidgetCount(); index++) {
+ DateCellDayEvent dc = (DateCellDayEvent) getWidget(index);
+ dc.setDisabled(isDisabled());
+ events.add(dc.getCalendarEvent());
+ }
+ events.add(dayEvent.getCalendarEvent());
+
+ index = 0;
+ for (CalendarEvent e : weekgrid.getCalendar()
+ .sortEventsByDuration(events)) {
+ if (e.equals(dayEvent.getCalendarEvent())) {
+ break;
+ }
+ index++;
+ }
+ this.insert(dayEvent, main, index, true);
+ }
+
+ public void removeEvent(DateCellDayEvent dayEvent) {
+ remove(dayEvent);
+ }
+
+ /**
+ *
+ * @param event
+ * @return
+ *
+ * This method is not necessary in the long run.. Or here can be
+ * various types of implementations..
+ */
+ // Date methods not deprecated in GWT
+ @SuppressWarnings("deprecation")
+ private boolean shouldDisplay(CalendarEvent event) {
+ boolean display = true;
+ if (event.isTimeOnDifferentDays()) {
+ display = true;
+ } else { // only in case of one-day event we are able not to display
+ // event
+ // which is placed in unpublished parts on calendar
+ Date eventStart = event.getStartTime();
+ Date eventEnd = event.getEndTime();
+
+ int eventStartHours = eventStart.getHours();
+ int eventEndHours = eventEnd.getHours();
+
+ display = !(eventEndHours < firstHour
+ || eventStartHours > lastHour);
+ }
+ return display;
+ }
+
+ @Override
+ public void onKeyDown(KeyDownEvent event) {
+ int keycode = event.getNativeEvent().getKeyCode();
+ if (keycode == KeyCodes.KEY_ESCAPE && eventRangeStart > -1) {
+ cancelRangeSelect();
+ }
+ }
+
+ @Override
+ public void onMouseDown(MouseDownEvent event) {
+ if (event.getNativeButton() == NativeEvent.BUTTON_LEFT) {
+ Element e = Element.as(event.getNativeEvent().getEventTarget());
+ if (e.getClassName().contains("reserved") || isDisabled()
+ || !weekgrid.getParentCalendar().isRangeSelectAllowed()) {
+ eventRangeStart = -1;
+ } else {
+ eventRangeStart = event.getY();
+ eventRangeStop = eventRangeStart;
+ Event.setCapture(getElement());
+ setFocus(true);
+ }
+ }
+ }
+
+ @Override
+ @SuppressWarnings("deprecation")
+ public void onMouseUp(MouseUpEvent event) {
+ if (event.getNativeButton() != NativeEvent.BUTTON_LEFT) {
+ return;
+ }
+ Event.releaseCapture(getElement());
+ setFocus(false);
+ int dragDistance = Math.abs(eventRangeStart - event.getY());
+ if (dragDistance > 0 && eventRangeStart >= 0) {
+ Element main = getElement();
+ if (eventRangeStart > eventRangeStop) {
+ if (eventRangeStop <= -1) {
+ eventRangeStop = 0;
+ }
+ int temp = eventRangeStart;
+ eventRangeStart = eventRangeStop;
+ eventRangeStop = temp;
+ }
+
+ NodeList<Node> nodes = main.getChildNodes();
+
+ int slotStart = -1;
+ int slotEnd = -1;
+
+ // iterate over all child nodes, until we find first the start,
+ // and then the end
+ for (int i = 0; i < nodes.getLength(); i++) {
+ Element element = (Element) nodes.getItem(i);
+ boolean isRangeElement = element.getClassName()
+ .contains("v-daterange");
+
+ if (isRangeElement && slotStart == -1) {
+ slotStart = i;
+ slotEnd = i; // to catch one-slot selections
+
+ } else if (isRangeElement) {
+ slotEnd = i;
+
+ } else if (slotStart != -1 && slotEnd != -1) {
+ break;
+ }
+ }
+
+ clearSelectionRange();
+
+ int startMinutes = firstHour * 60 + slotStart * 30;
+ int endMinutes = (firstHour * 60) + (slotEnd + 1) * 30;
+ Date currentDate = getDate();
+ String yr = (currentDate.getYear() + 1900) + "-"
+ + (currentDate.getMonth() + 1) + "-"
+ + currentDate.getDate();
+ if (weekgrid.getCalendar().getRangeSelectListener() != null) {
+ weekgrid.getCalendar().getRangeSelectListener().rangeSelected(
+ yr + ":" + startMinutes + ":" + endMinutes);
+ }
+ eventRangeStart = -1;
+ } else {
+ // Click event
+ eventRangeStart = -1;
+ cancelRangeSelect();
+
+ }
+ }
+
+ @Override
+ public void onMouseMove(MouseMoveEvent event) {
+ if (event.getNativeButton() != NativeEvent.BUTTON_LEFT) {
+ return;
+ }
+
+ if (eventRangeStart >= 0) {
+ int newY = event.getY();
+ int fromY = 0;
+ int toY = 0;
+ if (newY < eventRangeStart) {
+ fromY = newY;
+ toY = eventRangeStart;
+ } else {
+ fromY = eventRangeStart;
+ toY = newY;
+ }
+ Element main = getElement();
+ eventRangeStop = newY;
+ NodeList<Node> nodes = main.getChildNodes();
+ for (int i = 0; i < nodes.getLength(); i++) {
+ Element c = (Element) nodes.getItem(i);
+
+ if (todaybar != c) {
+
+ int elemStart = c.getOffsetTop();
+ int elemStop = elemStart + getSlotHeight();
+ if (elemStart >= fromY && elemStart <= toY) {
+ c.addClassName("v-daterange");
+ } else if (elemStop >= fromY && elemStop <= toY) {
+ c.addClassName("v-daterange");
+ } else if (elemStop >= fromY && elemStart <= toY) {
+ c.addClassName("v-daterange");
+ } else {
+ c.removeClassName("v-daterange");
+ }
+ }
+ }
+ }
+
+ event.preventDefault();
+ }
+
+ public void cancelRangeSelect() {
+ Event.releaseCapture(getElement());
+ setFocus(false);
+
+ clearSelectionRange();
+ }
+
+ private void clearSelectionRange() {
+ if (eventRangeStart > -1) {
+ // clear all "selected" class names
+ Element main = getElement();
+ NodeList<Node> nodes = main.getChildNodes();
+
+ for (int i = 0; i <= 47; i++) {
+ Element c = (Element) nodes.getItem(i);
+ if (c == null) {
+ continue;
+ }
+ c.removeClassName("v-daterange");
+ }
+
+ eventRangeStart = -1;
+ }
+ }
+
+ public void setToday(Date today, int width) {
+ this.today = today;
+ addStyleDependentName("today");
+ Element lastChild = (Element) getElement().getLastChild();
+ if (lastChild.getClassName().equals("v-calendar-current-time")) {
+ todaybar = lastChild;
+ } else {
+ todaybar = DOM.createDiv();
+ todaybar.setClassName("v-calendar-current-time");
+ getElement().appendChild(todaybar);
+ }
+
+ if (width != -1) {
+ todaybar.getStyle().setWidth(width, Unit.PX);
+ }
+
+ // position is calculated later, when we know the cell heights
+ }
+
+ public Element getTodaybarElement() {
+ return todaybar;
+ }
+
+ public void setDisabled(boolean disabled) {
+ this.disabled = disabled;
+ }
+
+ public boolean isDisabled() {
+ return disabled;
+ }
+
+ public void setDateColor(String styleName) {
+ this.setStyleName("v-calendar-datecell " + styleName);
+ }
+
+ public boolean isToday() {
+ return today != null;
+ }
+
+ /**
+ * @deprecated As of 7.2, call or override
+ * {@link #addEmphasisStyle(Element)} instead
+ */
+ @Deprecated
+ public void addEmphasisStyle(
+ com.google.gwt.user.client.Element elementOver) {
+ String originalStylename = getStyleName(elementOver);
+ setStyleName(elementOver, originalStylename + DRAGEMPHASISSTYLE);
+ }
+
+ /**
+ * @since 7.2
+ */
+ public void addEmphasisStyle(Element elementOver) {
+ addEmphasisStyle(DOM.asOld(elementOver));
+ }
+
+ /**
+ * @deprecated As of 7.2, call or override
+ * {@link #removeEmphasisStyle(Element)} instead
+ */
+ @Deprecated
+ public void removeEmphasisStyle(
+ com.google.gwt.user.client.Element elementOver) {
+ String originalStylename = getStyleName(elementOver);
+ setStyleName(elementOver, originalStylename.substring(0,
+ originalStylename.length() - DRAGEMPHASISSTYLE.length()));
+ }
+
+ /**
+ * @since 7.2
+ */
+ public void removeEmphasisStyle(Element elementOver) {
+ removeEmphasisStyle(DOM.asOld(elementOver));
+ }
+
+ @Override
+ public void onContextMenu(ContextMenuEvent event) {
+ if (weekgrid.getCalendar().getMouseEventListener() != null) {
+ event.preventDefault();
+ event.stopPropagation();
+ weekgrid.getCalendar().getMouseEventListener().contextMenu(event,
+ DateCell.this);
+ }
+ }
+
+ private void updateEventCellsWidth() {
+ for (Widget widget : getChildren()) {
+ if (widget instanceof DateCellDayEvent) {
+ ((DateCellDayEvent) widget).setMoveWidth(width);
+ }
+ }
+ }
+
+ private void updateEventCellsHeight() {
+ for (Widget widget : getChildren()) {
+ if (widget instanceof DateCellDayEvent) {
+ ((DateCellDayEvent) widget).setSlotHeightInPX(getSlotHeight());
+ }
+ }
+ }
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/DateCellContainer.java b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/DateCellContainer.java
new file mode 100644
index 0000000000..92c39c0791
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/DateCellContainer.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2000-2016 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.client.ui.calendar.schedule;
+
+import java.util.Date;
+
+import com.google.gwt.event.dom.client.MouseDownEvent;
+import com.google.gwt.event.dom.client.MouseDownHandler;
+import com.google.gwt.event.dom.client.MouseUpEvent;
+import com.google.gwt.event.dom.client.MouseUpHandler;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.client.WidgetUtil;
+import com.vaadin.client.ui.VCalendar;
+
+/**
+ * Internally used class by the Calendar
+ *
+ * since 7.1
+ */
+public class DateCellContainer extends FlowPanel
+ implements MouseDownHandler, MouseUpHandler {
+
+ private Date date;
+
+ private Widget clickTargetWidget;
+
+ private VCalendar calendar;
+
+ private static int borderWidth = -1;
+
+ public DateCellContainer() {
+ setStylePrimaryName("v-calendar-datecell");
+ }
+
+ public static int measureBorderWidth(DateCellContainer dc) {
+ if (borderWidth == -1) {
+ borderWidth = WidgetUtil.measureHorizontalBorder(dc.getElement());
+ }
+ return borderWidth;
+ }
+
+ public void setCalendar(VCalendar calendar) {
+ this.calendar = calendar;
+ }
+
+ public void setDate(Date date) {
+ this.date = date;
+ }
+
+ public Date getDate() {
+ return date;
+ }
+
+ public boolean hasEvent(int slotIndex) {
+ return hasDateCell(slotIndex)
+ && ((WeeklyLongEventsDateCell) getChildren().get(slotIndex))
+ .getEvent() != null;
+ }
+
+ public boolean hasDateCell(int slotIndex) {
+ return (getChildren().size() - 1) >= slotIndex;
+ }
+
+ public WeeklyLongEventsDateCell getDateCell(int slotIndex) {
+ if (!hasDateCell(slotIndex)) {
+ addEmptyEventCells(slotIndex - (getChildren().size() - 1));
+ }
+ return (WeeklyLongEventsDateCell) getChildren().get(slotIndex);
+ }
+
+ public void addEmptyEventCells(int eventCount) {
+ for (int i = 0; i < eventCount; i++) {
+ addEmptyEventCell();
+ }
+ }
+
+ public void addEmptyEventCell() {
+ WeeklyLongEventsDateCell dateCell = new WeeklyLongEventsDateCell();
+ dateCell.addMouseDownHandler(this);
+ dateCell.addMouseUpHandler(this);
+ add(dateCell);
+ }
+
+ @Override
+ public void onMouseDown(MouseDownEvent event) {
+ clickTargetWidget = (Widget) event.getSource();
+
+ event.stopPropagation();
+ }
+
+ @Override
+ public void onMouseUp(MouseUpEvent event) {
+ if (event.getSource() == clickTargetWidget
+ && clickTargetWidget instanceof WeeklyLongEventsDateCell
+ && !calendar.isDisabledOrReadOnly()) {
+ CalendarEvent calendarEvent = ((WeeklyLongEventsDateCell) clickTargetWidget)
+ .getEvent();
+ if (calendar.getEventClickListener() != null) {
+ calendar.getEventClickListener().eventClick(calendarEvent);
+ }
+ }
+ }
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/DateCellDayEvent.java b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/DateCellDayEvent.java
new file mode 100644
index 0000000000..7404f557a8
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/DateCellDayEvent.java
@@ -0,0 +1,664 @@
+/*
+ * Copyright 2000-2016 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.client.ui.calendar.schedule;
+
+import java.util.Date;
+import java.util.LinkedList;
+import java.util.List;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.dom.client.EventTarget;
+import com.google.gwt.dom.client.NativeEvent;
+import com.google.gwt.dom.client.Style;
+import com.google.gwt.dom.client.Style.Position;
+import com.google.gwt.dom.client.Style.Unit;
+import com.google.gwt.event.dom.client.ContextMenuEvent;
+import com.google.gwt.event.dom.client.ContextMenuHandler;
+import com.google.gwt.event.dom.client.KeyCodes;
+import com.google.gwt.event.dom.client.KeyDownEvent;
+import com.google.gwt.event.dom.client.KeyDownHandler;
+import com.google.gwt.event.dom.client.MouseDownEvent;
+import com.google.gwt.event.dom.client.MouseDownHandler;
+import com.google.gwt.event.dom.client.MouseMoveEvent;
+import com.google.gwt.event.dom.client.MouseMoveHandler;
+import com.google.gwt.event.dom.client.MouseUpEvent;
+import com.google.gwt.event.dom.client.MouseUpHandler;
+import com.google.gwt.event.shared.HandlerRegistration;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.Event;
+import com.google.gwt.user.client.ui.HorizontalPanel;
+import com.vaadin.client.WidgetUtil;
+import com.vaadin.shared.ui.calendar.DateConstants;
+
+/**
+ * Internally used by the calendar
+ *
+ * @since 7.1
+ */
+public class DateCellDayEvent extends FocusableHTML
+ implements MouseDownHandler, MouseUpHandler, MouseMoveHandler,
+ KeyDownHandler, ContextMenuHandler, HasTooltipKey {
+
+ private final DateCell dateCell;
+ private Element caption = null;
+ private final Element eventContent;
+ private CalendarEvent calendarEvent = null;
+ private HandlerRegistration moveRegistration;
+ private int startY = -1;
+ private int startX = -1;
+ private String moveWidth;
+ public static final int halfHourInMilliSeconds = 1800 * 1000;
+ private Date startDatetimeFrom;
+ private Date startDatetimeTo;
+ private boolean mouseMoveStarted;
+ private int top;
+ private int startYrelative;
+ private int startXrelative;
+ private boolean disabled;
+ private final WeekGrid weekGrid;
+ private Element topResizeBar;
+ private Element bottomResizeBar;
+ private Element clickTarget;
+ private final Integer eventIndex;
+ private int slotHeight;
+ private final List<HandlerRegistration> handlers;
+ private boolean mouseMoveCanceled;
+
+ public DateCellDayEvent(DateCell dateCell, WeekGrid parent,
+ CalendarEvent event) {
+ super();
+ this.dateCell = dateCell;
+
+ handlers = new LinkedList<HandlerRegistration>();
+
+ setStylePrimaryName("v-calendar-event");
+ setCalendarEvent(event);
+
+ weekGrid = parent;
+
+ Style s = getElement().getStyle();
+ if (event.getStyleName().length() > 0) {
+ addStyleDependentName(event.getStyleName());
+ }
+ s.setPosition(Position.ABSOLUTE);
+
+ caption = DOM.createDiv();
+ caption.addClassName("v-calendar-event-caption");
+ getElement().appendChild(caption);
+
+ eventContent = DOM.createDiv();
+ eventContent.addClassName("v-calendar-event-content");
+ getElement().appendChild(eventContent);
+
+ if (weekGrid.getCalendar().isEventResizeAllowed()) {
+ topResizeBar = DOM.createDiv();
+ bottomResizeBar = DOM.createDiv();
+
+ topResizeBar.addClassName("v-calendar-event-resizetop");
+ bottomResizeBar.addClassName("v-calendar-event-resizebottom");
+
+ getElement().appendChild(topResizeBar);
+ getElement().appendChild(bottomResizeBar);
+ }
+
+ eventIndex = event.getIndex();
+ }
+
+ @Override
+ protected void onAttach() {
+ super.onAttach();
+ handlers.add(addMouseDownHandler(this));
+ handlers.add(addMouseUpHandler(this));
+ handlers.add(addKeyDownHandler(this));
+ handlers.add(addDomHandler(this, ContextMenuEvent.getType()));
+ }
+
+ @Override
+ protected void onDetach() {
+ for (HandlerRegistration handler : handlers) {
+ handler.removeHandler();
+ }
+ handlers.clear();
+ super.onDetach();
+ }
+
+ public void setSlotHeightInPX(int slotHeight) {
+ this.slotHeight = slotHeight;
+ }
+
+ public void updatePosition(long startFromMinutes, long durationInMinutes) {
+ if (startFromMinutes < 0) {
+ startFromMinutes = 0;
+ }
+ top = weekGrid.getPixelTopFor((int) startFromMinutes);
+
+ getElement().getStyle().setTop(top, Unit.PX);
+ if (durationInMinutes > 0) {
+ int heightMinutes = weekGrid.getPixelLengthFor(
+ (int) startFromMinutes, (int) durationInMinutes);
+ setHeight(heightMinutes);
+ } else {
+ setHeight(-1);
+ }
+
+ boolean multiRowCaption = (durationInMinutes > 30);
+ updateCaptions(multiRowCaption);
+ }
+
+ public int getTop() {
+ return top;
+ }
+
+ public void setMoveWidth(int width) {
+ moveWidth = width + "px";
+ }
+
+ public void setHeight(int h) {
+ if (h == -1) {
+ getElement().getStyle().setProperty("height", "");
+ eventContent.getStyle().setProperty("height", "");
+ } else {
+ getElement().getStyle().setHeight(h, Unit.PX);
+ // FIXME measure the border height (2px) from the DOM
+ eventContent.getStyle().setHeight(h - 2, Unit.PX);
+ }
+ }
+
+ /**
+ * @param bigMode
+ * If false, event is so small that caption must be in time-row
+ */
+ private void updateCaptions(boolean bigMode) {
+ String innerHtml;
+ String timeAsText = calendarEvent.getTimeAsText();
+ String htmlOrText;
+
+ if (dateCell.weekgrid.getCalendar().isEventCaptionAsHtml()) {
+ htmlOrText = calendarEvent.getCaption();
+ } else {
+ htmlOrText = WidgetUtil.escapeHTML(calendarEvent.getCaption());
+ }
+
+ if (bigMode) {
+ innerHtml = "<span>" + timeAsText + "</span><br />" + htmlOrText;
+ } else {
+ innerHtml = "<span>" + timeAsText + "<span>:</span></span> "
+ + htmlOrText;
+ }
+ caption.setInnerHTML(innerHtml);
+ eventContent.setInnerHTML("");
+ }
+
+ @Override
+ public void onKeyDown(KeyDownEvent event) {
+ int keycode = event.getNativeEvent().getKeyCode();
+ if (keycode == KeyCodes.KEY_ESCAPE && mouseMoveStarted) {
+ cancelMouseMove();
+ }
+ }
+
+ @Override
+ public void onMouseDown(MouseDownEvent event) {
+ startX = event.getClientX();
+ startY = event.getClientY();
+ if (isDisabled()
+ || event.getNativeButton() != NativeEvent.BUTTON_LEFT) {
+ return;
+ }
+
+ clickTarget = Element.as(event.getNativeEvent().getEventTarget());
+ mouseMoveCanceled = false;
+
+ if (weekGrid.getCalendar().isEventMoveAllowed()
+ || clickTargetsResize()) {
+ moveRegistration = addMouseMoveHandler(this);
+ setFocus(true);
+ try {
+ startYrelative = (int) ((double) event.getRelativeY(caption)
+ % slotHeight);
+ startXrelative = (event.getRelativeX(weekGrid.getElement())
+ - weekGrid.timebar.getOffsetWidth())
+ % getDateCellWidth();
+ } catch (Exception e) {
+ GWT.log("Exception calculating relative start position", e);
+ }
+ mouseMoveStarted = false;
+ Style s = getElement().getStyle();
+ s.setZIndex(1000);
+ startDatetimeFrom = (Date) calendarEvent.getStartTime().clone();
+ startDatetimeTo = (Date) calendarEvent.getEndTime().clone();
+ Event.setCapture(getElement());
+ }
+
+ // make sure the right cursor is always displayed
+ if (clickTargetsResize()) {
+ addGlobalResizeStyle();
+ }
+
+ /*
+ * We need to stop the event propagation or else the WeekGrid range
+ * select will kick in
+ */
+ event.stopPropagation();
+ event.preventDefault();
+ }
+
+ @Override
+ public void onMouseUp(MouseUpEvent event) {
+ if (mouseMoveCanceled
+ || event.getNativeButton() != NativeEvent.BUTTON_LEFT) {
+ return;
+ }
+
+ Event.releaseCapture(getElement());
+ setFocus(false);
+ if (moveRegistration != null) {
+ moveRegistration.removeHandler();
+ moveRegistration = null;
+ }
+ int endX = event.getClientX();
+ int endY = event.getClientY();
+ int xDiff = 0, yDiff = 0;
+ if (startX != -1 && startY != -1) {
+ // Drag started
+ xDiff = startX - endX;
+ yDiff = startY - endY;
+ }
+
+ startX = -1;
+ startY = -1;
+ mouseMoveStarted = false;
+ Style s = getElement().getStyle();
+ s.setZIndex(1);
+ if (!clickTargetsResize()) {
+ // check if mouse has moved over threshold of 3 pixels
+ boolean mouseMoved = (xDiff < -3 || xDiff > 3 || yDiff < -3
+ || yDiff > 3);
+
+ if (!weekGrid.getCalendar().isDisabledOrReadOnly() && mouseMoved) {
+ // Event Move:
+ // - calendar must be enabled
+ // - calendar must not be in read-only mode
+ weekGrid.eventMoved(this);
+ } else if (!weekGrid.getCalendar().isDisabled()) {
+ // Event Click:
+ // - calendar must be enabled (read-only is allowed)
+ EventTarget et = event.getNativeEvent().getEventTarget();
+ Element e = Element.as(et);
+ if (e == caption || e == eventContent
+ || e.getParentElement() == caption) {
+ if (weekGrid.getCalendar()
+ .getEventClickListener() != null) {
+ weekGrid.getCalendar().getEventClickListener()
+ .eventClick(calendarEvent);
+ }
+ }
+ }
+
+ } else { // click targeted resize bar
+ removeGlobalResizeStyle();
+ if (weekGrid.getCalendar().getEventResizeListener() != null) {
+ weekGrid.getCalendar().getEventResizeListener()
+ .eventResized(calendarEvent);
+ }
+ dateCell.recalculateEventWidths();
+ }
+ }
+
+ @Override
+ @SuppressWarnings("deprecation")
+ public void onMouseMove(MouseMoveEvent event) {
+ if (startY < 0 && startX < 0) {
+ return;
+ }
+ if (isDisabled()) {
+ Event.releaseCapture(getElement());
+ mouseMoveStarted = false;
+ startY = -1;
+ startX = -1;
+ removeGlobalResizeStyle();
+ return;
+ }
+ int currentY = event.getClientY();
+ int currentX = event.getClientX();
+ int moveY = (currentY - startY);
+ int moveX = (currentX - startX);
+ if ((moveY < 5 && moveY > -6) && (moveX < 5 && moveX > -6)) {
+ return;
+ }
+ if (!mouseMoveStarted) {
+ setWidth(moveWidth);
+ getElement().getStyle().setMarginLeft(0, Unit.PX);
+ mouseMoveStarted = true;
+ }
+
+ HorizontalPanel parent = (HorizontalPanel) getParent().getParent();
+ int relativeX = event.getRelativeX(parent.getElement())
+ - weekGrid.timebar.getOffsetWidth();
+ int halfHourDiff = 0;
+ if (moveY > 0) {
+ halfHourDiff = (startYrelative + moveY) / slotHeight;
+ } else {
+ halfHourDiff = (moveY - startYrelative) / slotHeight;
+ }
+
+ int dateCellWidth = getDateCellWidth();
+ long dayDiff = 0;
+ if (moveX >= 0) {
+ dayDiff = (startXrelative + moveX) / dateCellWidth;
+ } else {
+ dayDiff = (moveX - (dateCellWidth - startXrelative))
+ / dateCellWidth;
+ }
+
+ int dayOffset = relativeX / dateCellWidth;
+
+ // sanity check for right side overflow
+ int dateCellCount = weekGrid.getDateCellCount();
+ if (dayOffset >= dateCellCount) {
+ dayOffset--;
+ dayDiff--;
+ }
+
+ int dayOffsetPx = calculateDateCellOffsetPx(dayOffset)
+ + weekGrid.timebar.getOffsetWidth();
+
+ GWT.log("DateCellWidth: " + dateCellWidth + " dayDiff: " + dayDiff
+ + " dayOffset: " + dayOffset + " dayOffsetPx: " + dayOffsetPx
+ + " startXrelative: " + startXrelative + " moveX: " + moveX);
+
+ if (relativeX < 0 || relativeX >= getDatesWidth()) {
+ return;
+ }
+
+ Style s = getElement().getStyle();
+
+ Date from = calendarEvent.getStartTime();
+ Date to = calendarEvent.getEndTime();
+ long duration = to.getTime() - from.getTime();
+
+ if (!clickTargetsResize()
+ && weekGrid.getCalendar().isEventMoveAllowed()) {
+ long daysMs = dayDiff * DateConstants.DAYINMILLIS;
+ from.setTime(startDatetimeFrom.getTime() + daysMs);
+ from.setTime(from.getTime()
+ + ((long) halfHourInMilliSeconds * halfHourDiff));
+ to.setTime((from.getTime() + duration));
+
+ calendarEvent.setStartTime(from);
+ calendarEvent.setEndTime(to);
+ calendarEvent.setStart(new Date(from.getTime()));
+ calendarEvent.setEnd(new Date(to.getTime()));
+
+ // Set new position for the event
+ long startFromMinutes = (from.getHours() * 60) + from.getMinutes();
+ long range = calendarEvent.getRangeInMinutes();
+ startFromMinutes = calculateStartFromMinute(startFromMinutes, from,
+ to, dayOffsetPx);
+ if (startFromMinutes < 0) {
+ range += startFromMinutes;
+ }
+ updatePosition(startFromMinutes, range);
+
+ s.setLeft(dayOffsetPx, Unit.PX);
+
+ if (weekGrid.getDateCellWidths() != null) {
+ s.setWidth(weekGrid.getDateCellWidths()[dayOffset], Unit.PX);
+ } else {
+ setWidth(moveWidth);
+ }
+
+ } else if (clickTarget == topResizeBar) {
+ long oldStartTime = startDatetimeFrom.getTime();
+ long newStartTime = oldStartTime
+ + ((long) halfHourInMilliSeconds * halfHourDiff);
+
+ if (!isTimeRangeTooSmall(newStartTime, startDatetimeTo.getTime())) {
+ newStartTime = startDatetimeTo.getTime() - getMinTimeRange();
+ }
+
+ from.setTime(newStartTime);
+
+ calendarEvent.setStartTime(from);
+ calendarEvent.setStart(new Date(from.getTime()));
+
+ // Set new position for the event
+ long startFromMinutes = (from.getHours() * 60) + from.getMinutes();
+ long range = calendarEvent.getRangeInMinutes();
+
+ updatePosition(startFromMinutes, range);
+
+ } else if (clickTarget == bottomResizeBar) {
+ long oldEndTime = startDatetimeTo.getTime();
+ long newEndTime = oldEndTime
+ + ((long) halfHourInMilliSeconds * halfHourDiff);
+
+ if (!isTimeRangeTooSmall(startDatetimeFrom.getTime(), newEndTime)) {
+ newEndTime = startDatetimeFrom.getTime() + getMinTimeRange();
+ }
+
+ to.setTime(newEndTime);
+
+ calendarEvent.setEndTime(to);
+ calendarEvent.setEnd(new Date(to.getTime()));
+
+ // Set new position for the event
+ long startFromMinutes = (startDatetimeFrom.getHours() * 60)
+ + startDatetimeFrom.getMinutes();
+ long range = calendarEvent.getRangeInMinutes();
+ startFromMinutes = calculateStartFromMinute(startFromMinutes, from,
+ to, dayOffsetPx);
+ if (startFromMinutes < 0) {
+ range += startFromMinutes;
+ }
+ updatePosition(startFromMinutes, range);
+ }
+ }
+
+ private void cancelMouseMove() {
+ mouseMoveCanceled = true;
+
+ // reset and remove everything related to the event handling
+ Event.releaseCapture(getElement());
+ setFocus(false);
+
+ if (moveRegistration != null) {
+ moveRegistration.removeHandler();
+ moveRegistration = null;
+ }
+
+ mouseMoveStarted = false;
+ removeGlobalResizeStyle();
+
+ Style s = getElement().getStyle();
+ s.setZIndex(1);
+
+ // reset the position of the event
+ int dateCellWidth = getDateCellWidth();
+ int dayOffset = startXrelative / dateCellWidth;
+ s.clearLeft();
+
+ calendarEvent.setStartTime(startDatetimeFrom);
+ calendarEvent.setEndTime(startDatetimeTo);
+
+ long startFromMinutes = (startDatetimeFrom.getHours() * 60)
+ + startDatetimeFrom.getMinutes();
+ long range = calendarEvent.getRangeInMinutes();
+
+ startFromMinutes = calculateStartFromMinute(startFromMinutes,
+ startDatetimeFrom, startDatetimeTo, dayOffset);
+ if (startFromMinutes < 0) {
+ range += startFromMinutes;
+ }
+
+ updatePosition(startFromMinutes, range);
+
+ startY = -1;
+ startX = -1;
+
+ // to reset the event width
+ ((DateCell) getParent()).recalculateEventWidths();
+ }
+
+ // date methods are not deprecated in GWT
+ @SuppressWarnings("deprecation")
+ private long calculateStartFromMinute(long startFromMinutes, Date from,
+ Date to, int dayOffset) {
+ boolean eventStartAtDifferentDay = from.getDate() != to.getDate();
+ if (eventStartAtDifferentDay) {
+ long minutesOnPrevDay = (getTargetDateByCurrentPosition(dayOffset)
+ .getTime() - from.getTime()) / DateConstants.MINUTEINMILLIS;
+ startFromMinutes = -1 * minutesOnPrevDay;
+ }
+
+ return startFromMinutes;
+ }
+
+ /**
+ * @param dateOffset
+ * @return the amount of pixels the given date is from the left side
+ */
+ private int calculateDateCellOffsetPx(int dateOffset) {
+ int dateCellOffset = 0;
+ int[] dateWidths = weekGrid.getDateCellWidths();
+
+ if (dateWidths != null) {
+ for (int i = 0; i < dateOffset; i++) {
+ dateCellOffset += dateWidths[i] + 1;
+ }
+ } else {
+ dateCellOffset = dateOffset * weekGrid.getDateCellWidth();
+ }
+
+ return dateCellOffset;
+ }
+
+ /**
+ * Check if the given time range is too small for events
+ *
+ * @param start
+ * @param end
+ * @return
+ */
+ private boolean isTimeRangeTooSmall(long start, long end) {
+ return (end - start) >= getMinTimeRange();
+ }
+
+ /**
+ * @return the minimum amount of ms that an event must last when resized
+ */
+ private long getMinTimeRange() {
+ return DateConstants.MINUTEINMILLIS * 30;
+ }
+
+ /**
+ * Build the string for sending resize events to server
+ *
+ * @param event
+ * @return
+ */
+ private String buildResizeString(CalendarEvent event) {
+ StringBuilder buffer = new StringBuilder();
+ buffer.append(event.getIndex());
+ buffer.append(",");
+ buffer.append(DateUtil.formatClientSideDate(event.getStart()));
+ buffer.append("-");
+ buffer.append(DateUtil.formatClientSideTime(event.getStartTime()));
+ buffer.append(",");
+ buffer.append(DateUtil.formatClientSideDate(event.getEnd()));
+ buffer.append("-");
+ buffer.append(DateUtil.formatClientSideTime(event.getEndTime()));
+
+ return buffer.toString();
+ }
+
+ private Date getTargetDateByCurrentPosition(int left) {
+ DateCell newParent = (DateCell) weekGrid.content
+ .getWidget((left / getDateCellWidth()) + 1);
+ Date targetDate = newParent.getDate();
+ return targetDate;
+ }
+
+ private int getDateCellWidth() {
+ return weekGrid.getDateCellWidth();
+ }
+
+ /* Returns total width of all date cells. */
+ private int getDatesWidth() {
+ if (weekGrid.width == -1) {
+ // Undefined width. Needs to be calculated by the known cell
+ // widths.
+ int count = weekGrid.content.getWidgetCount() - 1;
+ return count * getDateCellWidth();
+ }
+
+ return weekGrid.getInternalWidth();
+ }
+
+ /**
+ * @return true if the current mouse movement is resizing
+ */
+ private boolean clickTargetsResize() {
+ return weekGrid.getCalendar().isEventResizeAllowed()
+ && (clickTarget == topResizeBar
+ || clickTarget == bottomResizeBar);
+ }
+
+ private void addGlobalResizeStyle() {
+ if (clickTarget == topResizeBar) {
+ weekGrid.getCalendar().addStyleDependentName("nresize");
+ } else if (clickTarget == bottomResizeBar) {
+ weekGrid.getCalendar().addStyleDependentName("sresize");
+ }
+ }
+
+ private void removeGlobalResizeStyle() {
+ weekGrid.getCalendar().removeStyleDependentName("nresize");
+ weekGrid.getCalendar().removeStyleDependentName("sresize");
+ }
+
+ public void setCalendarEvent(CalendarEvent calendarEvent) {
+ this.calendarEvent = calendarEvent;
+ }
+
+ public CalendarEvent getCalendarEvent() {
+ return calendarEvent;
+ }
+
+ public void setDisabled(boolean disabled) {
+ this.disabled = disabled;
+ }
+
+ public boolean isDisabled() {
+ return disabled;
+ }
+
+ @Override
+ public void onContextMenu(ContextMenuEvent event) {
+ if (dateCell.weekgrid.getCalendar().getMouseEventListener() != null) {
+ event.preventDefault();
+ event.stopPropagation();
+ dateCell.weekgrid.getCalendar().getMouseEventListener()
+ .contextMenu(event, this);
+ }
+ }
+
+ @Override
+ public Object getTooltipKey() {
+ return eventIndex;
+ }
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/DateCellGroup.java b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/DateCellGroup.java
new file mode 100644
index 0000000000..907a71c449
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/DateCellGroup.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2000-2016 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.client.ui.calendar.schedule;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * Internally used by the calendar
+ *
+ * @since 7.1
+ */
+public class DateCellGroup {
+ private WeekGridMinuteTimeRange range;
+ private final List<Integer> items;
+
+ public DateCellGroup(Integer index) {
+ items = new ArrayList<Integer>();
+ items.add(index);
+ }
+
+ public WeekGridMinuteTimeRange getDateRange() {
+ return range;
+ }
+
+ public Date getStart() {
+ return range.getStart();
+ }
+
+ public Date getEnd() {
+ return range.getEnd();
+ }
+
+ public void setDateRange(WeekGridMinuteTimeRange range) {
+ this.range = range;
+ }
+
+ public List<Integer> getItems() {
+ return items;
+ }
+
+ public void add(Integer index) {
+ items.add(index);
+ }
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/DateUtil.java b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/DateUtil.java
new file mode 100644
index 0000000000..4a11e95245
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/DateUtil.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2000-2016 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.client.ui.calendar.schedule;
+
+import java.util.Date;
+
+import com.google.gwt.i18n.client.DateTimeFormat;
+import com.vaadin.shared.ui.calendar.DateConstants;
+
+/**
+ * Utility class for {@link Date} operations
+ *
+ * @since 7.1
+ * @author Vaadin Ltd.
+ */
+public class DateUtil {
+
+ /**
+ * Checks if dates are same day without checking datetimes.
+ *
+ * @param date1
+ * @param date2
+ * @return
+ */
+ @SuppressWarnings("deprecation")
+ public static boolean compareDate(Date date1, Date date2) {
+ if (date1.getDate() == date2.getDate()
+ && date1.getYear() == date2.getYear()
+ && date1.getMonth() == date2.getMonth()) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * @param date
+ * the date to format
+ *
+ * @return given Date as String, for communicating to server-side
+ */
+ public static String formatClientSideDate(Date date) {
+ DateTimeFormat dateformat_date = DateTimeFormat
+ .getFormat(DateConstants.CLIENT_DATE_FORMAT);
+ return dateformat_date.format(date);
+ }
+
+ /**
+ * @param date
+ * the date to format
+ * @return given Date as String, for communicating to server-side
+ */
+ public static String formatClientSideTime(Date date) {
+ DateTimeFormat dateformat_date = DateTimeFormat
+ .getFormat(DateConstants.CLIENT_TIME_FORMAT);
+ return dateformat_date.format(date);
+ }
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/DayToolbar.java b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/DayToolbar.java
new file mode 100644
index 0000000000..0ba1023945
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/DayToolbar.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright 2000-2016 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.client.ui.calendar.schedule;
+
+import java.util.Iterator;
+
+import com.google.gwt.dom.client.Style.Unit;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.user.client.ui.Button;
+import com.google.gwt.user.client.ui.HorizontalPanel;
+import com.google.gwt.user.client.ui.Label;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.client.ui.VCalendar;
+
+/**
+ *
+ * @since 7.1
+ * @author Vaadin Ltd.
+ *
+ */
+public class DayToolbar extends HorizontalPanel implements ClickHandler {
+ private int width = 0;
+ protected static final int MARGINLEFT = 50;
+ protected static final int MARGINRIGHT = 20;
+ protected Button backLabel;
+ protected Button nextLabel;
+ private boolean verticalSized;
+ private boolean horizontalSized;
+ private VCalendar calendar;
+
+ public DayToolbar(VCalendar vcalendar) {
+ calendar = vcalendar;
+
+ setStylePrimaryName("v-calendar-header-week");
+ backLabel = new Button();
+ backLabel.setStylePrimaryName("v-calendar-back");
+ nextLabel = new Button();
+ nextLabel.addClickHandler(this);
+ nextLabel.setStylePrimaryName("v-calendar-next");
+ backLabel.addClickHandler(this);
+ setBorderWidth(0);
+ setSpacing(0);
+ }
+
+ public void setWidthPX(int width) {
+ this.width = (width - MARGINLEFT) - MARGINRIGHT;
+ // super.setWidth(this.width + "px");
+ if (getWidgetCount() == 0) {
+ return;
+ }
+ updateCellWidths();
+ }
+
+ public void updateCellWidths() {
+ int count = getWidgetCount();
+ if (count > 0) {
+ setCellWidth(backLabel, MARGINLEFT + "px");
+ setCellWidth(nextLabel, MARGINRIGHT + "px");
+ setCellHorizontalAlignment(nextLabel, ALIGN_RIGHT);
+ int cellw = width / (count - 2);
+ if (cellw > 0) {
+ int[] cellWidths = VCalendar.distributeSize(width, count - 2,
+ 0);
+ for (int i = 1; i < count - 1; i++) {
+ Widget widget = getWidget(i);
+ // if (remain > 0) {
+ // setCellWidth(widget, cellw2 + "px");
+ // remain--;
+ // } else {
+ // setCellWidth(widget, cellw + "px");
+ // }
+ setCellWidth(widget, cellWidths[i - 1] + "px");
+ widget.setWidth(cellWidths[i - 1] + "px");
+ }
+ }
+ }
+ }
+
+ public void add(String dayName, final String date,
+ String localized_date_format, String extraClass) {
+ Label l = new Label(dayName + " " + localized_date_format);
+ l.setStylePrimaryName("v-calendar-header-day");
+
+ if (extraClass != null) {
+ l.addStyleDependentName(extraClass);
+ }
+
+ if (verticalSized) {
+ l.addStyleDependentName("Vsized");
+ }
+ if (horizontalSized) {
+ l.addStyleDependentName("Hsized");
+ }
+
+ l.addClickHandler(new ClickHandler() {
+ @Override
+ public void onClick(ClickEvent event) {
+ if (calendar.getDateClickListener() != null) {
+ calendar.getDateClickListener().dateClick(date);
+ }
+ }
+ });
+
+ add(l);
+ }
+
+ public void addBackButton() {
+ if (!calendar.isBackwardNavigationEnabled()) {
+ nextLabel.getElement().getStyle().setHeight(0, Unit.PX);
+ }
+ add(backLabel);
+ }
+
+ public void addNextButton() {
+ if (!calendar.isForwardNavigationEnabled()) {
+ backLabel.getElement().getStyle().setHeight(0, Unit.PX);
+ }
+ add(nextLabel);
+ }
+
+ @Override
+ public void onClick(ClickEvent event) {
+ if (!calendar.isDisabled()) {
+ if (event.getSource() == nextLabel) {
+ if (calendar.getForwardListener() != null) {
+ calendar.getForwardListener().forward();
+ }
+ } else if (event.getSource() == backLabel) {
+ if (calendar.getBackwardListener() != null) {
+ calendar.getBackwardListener().backward();
+ }
+ }
+ }
+ }
+
+ public void setVerticalSized(boolean sized) {
+ verticalSized = sized;
+ updateDayLabelSizedStyleNames();
+ }
+
+ public void setHorizontalSized(boolean sized) {
+ horizontalSized = sized;
+ updateDayLabelSizedStyleNames();
+ }
+
+ private void updateDayLabelSizedStyleNames() {
+ Iterator<Widget> it = iterator();
+ while (it.hasNext()) {
+ updateWidgetSizedStyleName(it.next());
+ }
+ }
+
+ private void updateWidgetSizedStyleName(Widget w) {
+ if (verticalSized) {
+ w.addStyleDependentName("Vsized");
+ } else {
+ w.removeStyleDependentName("VSized");
+ }
+ if (horizontalSized) {
+ w.addStyleDependentName("Hsized");
+ } else {
+ w.removeStyleDependentName("HSized");
+ }
+ }
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/FocusableComplexPanel.java b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/FocusableComplexPanel.java
new file mode 100644
index 0000000000..a498525c92
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/FocusableComplexPanel.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright 2000-2016 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.client.ui.calendar.schedule;
+
+import com.google.gwt.event.dom.client.BlurEvent;
+import com.google.gwt.event.dom.client.BlurHandler;
+import com.google.gwt.event.dom.client.FocusEvent;
+import com.google.gwt.event.dom.client.FocusHandler;
+import com.google.gwt.event.dom.client.HasBlurHandlers;
+import com.google.gwt.event.dom.client.HasFocusHandlers;
+import com.google.gwt.event.dom.client.HasKeyDownHandlers;
+import com.google.gwt.event.dom.client.HasKeyPressHandlers;
+import com.google.gwt.event.dom.client.KeyDownEvent;
+import com.google.gwt.event.dom.client.KeyDownHandler;
+import com.google.gwt.event.dom.client.KeyPressEvent;
+import com.google.gwt.event.dom.client.KeyPressHandler;
+import com.google.gwt.event.shared.HandlerRegistration;
+import com.google.gwt.user.client.ui.ComplexPanel;
+import com.google.gwt.user.client.ui.impl.FocusImpl;
+import com.vaadin.client.Focusable;
+
+/**
+ * A ComplexPanel that can be focused
+ *
+ * @since 7.1
+ * @author Vaadin Ltd.
+ *
+ */
+public class FocusableComplexPanel extends ComplexPanel
+ implements HasFocusHandlers, HasBlurHandlers, HasKeyDownHandlers,
+ HasKeyPressHandlers, Focusable {
+
+ protected void makeFocusable() {
+ // make focusable, as we don't need access key magic we don't need to
+ // use FocusImpl.createFocusable
+ getElement().setTabIndex(0);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.google.gwt.event.dom.client.HasFocusHandlers#addFocusHandler(com.
+ * google.gwt.event.dom.client.FocusHandler)
+ */
+ @Override
+ public HandlerRegistration addFocusHandler(FocusHandler handler) {
+ return addDomHandler(handler, FocusEvent.getType());
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.google.gwt.event.dom.client.HasBlurHandlers#addBlurHandler(com.google
+ * .gwt.event.dom.client.BlurHandler)
+ */
+ @Override
+ public HandlerRegistration addBlurHandler(BlurHandler handler) {
+ return addDomHandler(handler, BlurEvent.getType());
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.google.gwt.event.dom.client.HasKeyDownHandlers#addKeyDownHandler(
+ * com.google.gwt.event.dom.client.KeyDownHandler)
+ */
+ @Override
+ public HandlerRegistration addKeyDownHandler(KeyDownHandler handler) {
+ return addDomHandler(handler, KeyDownEvent.getType());
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.google.gwt.event.dom.client.HasKeyPressHandlers#addKeyPressHandler
+ * (com.google.gwt.event.dom.client.KeyPressHandler)
+ */
+ @Override
+ public HandlerRegistration addKeyPressHandler(KeyPressHandler handler) {
+ return addDomHandler(handler, KeyPressEvent.getType());
+ }
+
+ /**
+ * Sets/Removes the keyboard focus to the panel.
+ *
+ * @param focus
+ * If set to true then the focus is moved to the panel, if set to
+ * false the focus is removed
+ */
+ public void setFocus(boolean focus) {
+ if (focus) {
+ FocusImpl.getFocusImplForPanel().focus(getElement());
+ } else {
+ FocusImpl.getFocusImplForPanel().blur(getElement());
+ }
+ }
+
+ /**
+ * Focus the panel
+ */
+ @Override
+ public void focus() {
+ setFocus(true);
+ }
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/FocusableGrid.java b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/FocusableGrid.java
new file mode 100644
index 0000000000..fd46f5553b
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/FocusableGrid.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright 2000-2016 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.client.ui.calendar.schedule;
+
+import com.google.gwt.event.dom.client.BlurEvent;
+import com.google.gwt.event.dom.client.BlurHandler;
+import com.google.gwt.event.dom.client.FocusEvent;
+import com.google.gwt.event.dom.client.FocusHandler;
+import com.google.gwt.event.dom.client.HasBlurHandlers;
+import com.google.gwt.event.dom.client.HasFocusHandlers;
+import com.google.gwt.event.dom.client.HasKeyDownHandlers;
+import com.google.gwt.event.dom.client.HasKeyPressHandlers;
+import com.google.gwt.event.dom.client.KeyDownEvent;
+import com.google.gwt.event.dom.client.KeyDownHandler;
+import com.google.gwt.event.dom.client.KeyPressEvent;
+import com.google.gwt.event.dom.client.KeyPressHandler;
+import com.google.gwt.event.shared.HandlerRegistration;
+import com.google.gwt.user.client.ui.Grid;
+import com.google.gwt.user.client.ui.impl.FocusImpl;
+import com.vaadin.client.Focusable;
+
+/**
+ * A Grid that can be focused
+ *
+ * @since 7.1
+ * @author Vaadin Ltd.
+ *
+ */
+public class FocusableGrid extends Grid implements HasFocusHandlers,
+ HasBlurHandlers, HasKeyDownHandlers, HasKeyPressHandlers, Focusable {
+
+ /**
+ * Constructor
+ */
+ public FocusableGrid() {
+ super();
+ makeFocusable();
+ }
+
+ public FocusableGrid(int rows, int columns) {
+ super(rows, columns);
+ makeFocusable();
+ }
+
+ protected void makeFocusable() {
+ // make focusable, as we don't need access key magic we don't need to
+ // use FocusImpl.createFocusable
+ getElement().setTabIndex(0);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.google.gwt.event.dom.client.HasFocusHandlers#addFocusHandler(com.
+ * google.gwt.event.dom.client.FocusHandler)
+ */
+ @Override
+ public HandlerRegistration addFocusHandler(FocusHandler handler) {
+ return addDomHandler(handler, FocusEvent.getType());
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.google.gwt.event.dom.client.HasBlurHandlers#addBlurHandler(com.google
+ * .gwt.event.dom.client.BlurHandler)
+ */
+ @Override
+ public HandlerRegistration addBlurHandler(BlurHandler handler) {
+ return addDomHandler(handler, BlurEvent.getType());
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.google.gwt.event.dom.client.HasKeyDownHandlers#addKeyDownHandler(
+ * com.google.gwt.event.dom.client.KeyDownHandler)
+ */
+ @Override
+ public HandlerRegistration addKeyDownHandler(KeyDownHandler handler) {
+ return addDomHandler(handler, KeyDownEvent.getType());
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.google.gwt.event.dom.client.HasKeyPressHandlers#addKeyPressHandler
+ * (com.google.gwt.event.dom.client.KeyPressHandler)
+ */
+ @Override
+ public HandlerRegistration addKeyPressHandler(KeyPressHandler handler) {
+ return addDomHandler(handler, KeyPressEvent.getType());
+ }
+
+ /**
+ * Sets/Removes the keyboard focus to the panel.
+ *
+ * @param focus
+ * If set to true then the focus is moved to the panel, if set to
+ * false the focus is removed
+ */
+ public void setFocus(boolean focus) {
+ if (focus) {
+ FocusImpl.getFocusImplForPanel().focus(getElement());
+ } else {
+ FocusImpl.getFocusImplForPanel().blur(getElement());
+ }
+ }
+
+ /**
+ * Focus the panel
+ */
+ @Override
+ public void focus() {
+ setFocus(true);
+ }
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/FocusableHTML.java b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/FocusableHTML.java
new file mode 100644
index 0000000000..3a838a58a3
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/FocusableHTML.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2000-2016 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.client.ui.calendar.schedule;
+
+import com.google.gwt.event.dom.client.BlurEvent;
+import com.google.gwt.event.dom.client.BlurHandler;
+import com.google.gwt.event.dom.client.FocusEvent;
+import com.google.gwt.event.dom.client.FocusHandler;
+import com.google.gwt.event.dom.client.HasBlurHandlers;
+import com.google.gwt.event.dom.client.HasFocusHandlers;
+import com.google.gwt.event.dom.client.HasKeyDownHandlers;
+import com.google.gwt.event.dom.client.HasKeyPressHandlers;
+import com.google.gwt.event.dom.client.KeyDownEvent;
+import com.google.gwt.event.dom.client.KeyDownHandler;
+import com.google.gwt.event.dom.client.KeyPressEvent;
+import com.google.gwt.event.dom.client.KeyPressHandler;
+import com.google.gwt.event.shared.HandlerRegistration;
+import com.google.gwt.user.client.ui.HTML;
+import com.google.gwt.user.client.ui.impl.FocusImpl;
+import com.vaadin.client.Focusable;
+
+/**
+ * A HTML widget that can be focused
+ *
+ * @since 7.1
+ * @author Vaadin Ltd.
+ *
+ */
+public class FocusableHTML extends HTML implements HasFocusHandlers,
+ HasBlurHandlers, HasKeyDownHandlers, HasKeyPressHandlers, Focusable {
+
+ /**
+ * Constructor
+ */
+ public FocusableHTML() {
+ // make focusable, as we don't need access key magic we don't need to
+ // use FocusImpl.createFocusable
+ getElement().setTabIndex(0);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.google.gwt.event.dom.client.HasFocusHandlers#addFocusHandler(com.
+ * google.gwt.event.dom.client.FocusHandler)
+ */
+ @Override
+ public HandlerRegistration addFocusHandler(FocusHandler handler) {
+ return addDomHandler(handler, FocusEvent.getType());
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.google.gwt.event.dom.client.HasBlurHandlers#addBlurHandler(com.google
+ * .gwt.event.dom.client.BlurHandler)
+ */
+ @Override
+ public HandlerRegistration addBlurHandler(BlurHandler handler) {
+ return addDomHandler(handler, BlurEvent.getType());
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.google.gwt.event.dom.client.HasKeyDownHandlers#addKeyDownHandler(
+ * com.google.gwt.event.dom.client.KeyDownHandler)
+ */
+ @Override
+ public HandlerRegistration addKeyDownHandler(KeyDownHandler handler) {
+ return addDomHandler(handler, KeyDownEvent.getType());
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.google.gwt.event.dom.client.HasKeyPressHandlers#addKeyPressHandler
+ * (com.google.gwt.event.dom.client.KeyPressHandler)
+ */
+ @Override
+ public HandlerRegistration addKeyPressHandler(KeyPressHandler handler) {
+ return addDomHandler(handler, KeyPressEvent.getType());
+ }
+
+ /**
+ * Sets/Removes the keyboard focus to the panel.
+ *
+ * @param focus
+ * If set to true then the focus is moved to the panel, if set to
+ * false the focus is removed
+ */
+ public void setFocus(boolean focus) {
+ if (focus) {
+ FocusImpl.getFocusImplForPanel().focus(getElement());
+ } else {
+ FocusImpl.getFocusImplForPanel().blur(getElement());
+ }
+ }
+
+ /**
+ * Focus the panel
+ */
+ @Override
+ public void focus() {
+ setFocus(true);
+ }
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/HasTooltipKey.java b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/HasTooltipKey.java
new file mode 100644
index 0000000000..936f978abb
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/HasTooltipKey.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2000-2016 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.client.ui.calendar.schedule;
+
+/**
+ * For Calendar client-side internal use only.
+ *
+ * @since 7.1
+ * @author Vaadin Ltd.
+ *
+ */
+public interface HasTooltipKey {
+ /**
+ * Gets the key associated for the Widget implementing this interface. This
+ * key is used for getting a tooltip title identified by the key
+ *
+ * @return the tooltip key
+ */
+ Object getTooltipKey();
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/MonthEventLabel.java b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/MonthEventLabel.java
new file mode 100644
index 0000000000..c62b21592a
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/MonthEventLabel.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright 2000-2016 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.client.ui.calendar.schedule;
+
+import java.util.Date;
+
+import com.google.gwt.event.dom.client.ContextMenuEvent;
+import com.google.gwt.event.dom.client.ContextMenuHandler;
+import com.google.gwt.user.client.ui.HTML;
+import com.vaadin.client.Util;
+import com.vaadin.client.ui.VCalendar;
+
+/**
+ * The label in a month cell
+ *
+ * @since 7.1
+ */
+public class MonthEventLabel extends HTML implements HasTooltipKey {
+
+ private static final String STYLENAME = "v-calendar-event";
+
+ private boolean timeSpecificEvent = false;
+ private Integer eventIndex;
+ private VCalendar calendar;
+ private String caption;
+ private Date time;
+
+ private CalendarEvent calendarEvent;
+
+ /**
+ * Default constructor
+ */
+ public MonthEventLabel() {
+ setStylePrimaryName(STYLENAME);
+
+ addDomHandler(new ContextMenuHandler() {
+ @Override
+ public void onContextMenu(ContextMenuEvent event) {
+ calendar.getMouseEventListener().contextMenu(event,
+ MonthEventLabel.this);
+ event.stopPropagation();
+ event.preventDefault();
+ }
+ }, ContextMenuEvent.getType());
+ }
+
+ public void setCalendarEvent(CalendarEvent e) {
+ calendarEvent = e;
+ }
+
+ /**
+ * Set the time of the event label
+ *
+ * @param date
+ * The date object that specifies the time
+ */
+ public void setTime(Date date) {
+ time = date;
+ renderCaption();
+ }
+
+ /**
+ * Set the caption of the event label
+ *
+ * @param caption
+ * The caption string, can be HTML if
+ * {@link VCalendar#isEventCaptionAsHtml()} is true
+ */
+ public void setCaption(String caption) {
+ this.caption = caption;
+ renderCaption();
+ }
+
+ /**
+ * Renders the caption in the DIV element
+ */
+ private void renderCaption() {
+ StringBuilder html = new StringBuilder();
+ String textOrHtml;
+ if (calendar.isEventCaptionAsHtml()) {
+ textOrHtml = caption;
+ } else {
+ textOrHtml = Util.escapeHTML(caption);
+ }
+
+ if (caption != null && time != null) {
+ html.append("<span class=\"" + STYLENAME + "-time\">");
+ html.append(calendar.getTimeFormat().format(time));
+ html.append("</span> ");
+ html.append(textOrHtml);
+ } else if (caption != null) {
+ html.append(textOrHtml);
+ } else if (time != null) {
+ html.append("<span class=\"" + STYLENAME + "-time\">");
+ html.append(calendar.getTimeFormat().format(time));
+ html.append("</span>");
+ }
+ super.setHTML(html.toString());
+ }
+
+ /**
+ * Set the (server side) index of the event
+ *
+ * @param index
+ * The integer index
+ */
+ public void setEventIndex(int index) {
+ eventIndex = index;
+ }
+
+ /**
+ * Set the Calendar instance this label belongs to
+ *
+ * @param calendar
+ * The calendar instance
+ */
+ public void setCalendar(VCalendar calendar) {
+ this.calendar = calendar;
+ }
+
+ /**
+ * Is the event bound to a specific time
+ *
+ * @return
+ */
+ public boolean isTimeSpecificEvent() {
+ return timeSpecificEvent;
+ }
+
+ /**
+ * Is the event bound to a specific time
+ *
+ * @param timeSpecificEvent
+ * True if the event is bound to a time, false if it is only
+ * bound to the day
+ */
+ public void setTimeSpecificEvent(boolean timeSpecificEvent) {
+ this.timeSpecificEvent = timeSpecificEvent;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.google.gwt.user.client.ui.HTML#setHTML(java.lang.String)
+ */
+ @Override
+ public void setHTML(String html) {
+ throw new UnsupportedOperationException(
+ "Use setCaption() and setTime() instead");
+ }
+
+ @Override
+ public Object getTooltipKey() {
+ return eventIndex;
+ }
+
+ public CalendarEvent getCalendarEvent() {
+ return calendarEvent;
+ }
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/MonthGrid.java b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/MonthGrid.java
new file mode 100644
index 0000000000..119fe27992
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/MonthGrid.java
@@ -0,0 +1,215 @@
+/*
+ * Copyright 2000-2016 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.client.ui.calendar.schedule;
+
+import java.util.Date;
+
+import com.google.gwt.event.dom.client.KeyCodes;
+import com.google.gwt.event.dom.client.KeyDownEvent;
+import com.google.gwt.event.dom.client.KeyDownHandler;
+import com.google.gwt.event.shared.HandlerRegistration;
+import com.vaadin.client.ui.VCalendar;
+
+/**
+ *
+ * @since 7.1
+ * @author Vaadin Ltd.
+ *
+ */
+public class MonthGrid extends FocusableGrid implements KeyDownHandler {
+
+ private SimpleDayCell selectionStart;
+ private SimpleDayCell selectionEnd;
+ private final VCalendar calendar;
+ private boolean rangeSelectDisabled;
+ private boolean enabled = true;
+ private final HandlerRegistration keyDownHandler;
+
+ public MonthGrid(VCalendar parent, int rows, int columns) {
+ super(rows, columns);
+ calendar = parent;
+ setCellSpacing(0);
+ setCellPadding(0);
+ setStylePrimaryName("v-calendar-month");
+
+ keyDownHandler = addKeyDownHandler(this);
+ }
+
+ @Override
+ protected void onUnload() {
+ keyDownHandler.removeHandler();
+ super.onUnload();
+ }
+
+ public void setSelectionEnd(SimpleDayCell simpleDayCell) {
+ selectionEnd = simpleDayCell;
+ updateSelection();
+ }
+
+ public void setSelectionStart(SimpleDayCell simpleDayCell) {
+ if (!rangeSelectDisabled && isEnabled()) {
+ selectionStart = simpleDayCell;
+ setFocus(true);
+ }
+
+ }
+
+ private void updateSelection() {
+ if (selectionStart == null) {
+ return;
+ }
+ if (selectionStart != null && selectionEnd != null) {
+ Date startDate = selectionStart.getDate();
+ Date endDate = selectionEnd.getDate();
+ for (int row = 0; row < getRowCount(); row++) {
+ for (int cell = 0; cell < getCellCount(row); cell++) {
+ SimpleDayCell sdc = (SimpleDayCell) getWidget(row, cell);
+ if (sdc == null) {
+ return;
+ }
+ Date d = sdc.getDate();
+ if (startDate.compareTo(d) <= 0
+ && endDate.compareTo(d) >= 0) {
+ sdc.addStyleDependentName("selected");
+ } else if (startDate.compareTo(d) >= 0
+ && endDate.compareTo(d) <= 0) {
+ sdc.addStyleDependentName("selected");
+ } else {
+ sdc.removeStyleDependentName("selected");
+ }
+ }
+ }
+ }
+ }
+
+ public void setSelectionReady() {
+ if (selectionStart != null && selectionEnd != null) {
+ String value = "";
+ Date startDate = selectionStart.getDate();
+ Date endDate = selectionEnd.getDate();
+ if (startDate.compareTo(endDate) > 0) {
+ Date temp = startDate;
+ startDate = endDate;
+ endDate = temp;
+ }
+
+ if (calendar.getRangeSelectListener() != null) {
+ value = calendar.getDateFormat().format(startDate) + "TO"
+ + calendar.getDateFormat().format(endDate);
+ calendar.getRangeSelectListener().rangeSelected(value);
+ }
+ selectionStart = null;
+ selectionEnd = null;
+ setFocus(false);
+ }
+ }
+
+ public void cancelRangeSelection() {
+ if (selectionStart != null && selectionEnd != null) {
+ for (int row = 0; row < getRowCount(); row++) {
+ for (int cell = 0; cell < getCellCount(row); cell++) {
+ SimpleDayCell sdc = (SimpleDayCell) getWidget(row, cell);
+ if (sdc == null) {
+ return;
+ }
+ sdc.removeStyleDependentName("selected");
+ }
+ }
+ }
+ setFocus(false);
+ selectionStart = null;
+ }
+
+ public void updateCellSizes(int totalWidthPX, int totalHeightPX) {
+ boolean setHeight = totalHeightPX > 0;
+ boolean setWidth = totalWidthPX > 0;
+ int rows = getRowCount();
+ int cells = getCellCount(0);
+ int cellWidth = (totalWidthPX / cells) - 1;
+ int widthRemainder = totalWidthPX % cells;
+ // Division for cells might not be even. Distribute it evenly to
+ // will whole space.
+ int heightPX = totalHeightPX;
+ int cellHeight = heightPX / rows;
+ int heightRemainder = heightPX % rows;
+
+ for (int i = 0; i < rows; i++) {
+ for (int j = 0; j < cells; j++) {
+ SimpleDayCell sdc = (SimpleDayCell) getWidget(i, j);
+
+ if (setWidth) {
+ if (widthRemainder > 0) {
+ sdc.setWidth(cellWidth + 1 + "px");
+ widthRemainder--;
+
+ } else {
+ sdc.setWidth(cellWidth + "px");
+ }
+ }
+
+ if (setHeight) {
+ if (heightRemainder > 0) {
+ sdc.setHeightPX(cellHeight + 1, true);
+
+ } else {
+ sdc.setHeightPX(cellHeight, true);
+ }
+ } else {
+ sdc.setHeightPX(-1, true);
+ }
+ }
+ heightRemainder--;
+ }
+ }
+
+ /**
+ * Disable or enable possibility to select ranges
+ */
+ public void setRangeSelect(boolean b) {
+ rangeSelectDisabled = !b;
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ @Override
+ public void onKeyDown(KeyDownEvent event) {
+ int keycode = event.getNativeKeyCode();
+ if (KeyCodes.KEY_ESCAPE == keycode && selectionStart != null) {
+ cancelRangeSelection();
+ }
+ }
+
+ public int getDayCellIndex(SimpleDayCell dayCell) {
+ int rows = getRowCount();
+ int cells = getCellCount(0);
+ for (int i = 0; i < rows; i++) {
+ for (int j = 0; j < cells; j++) {
+ SimpleDayCell sdc = (SimpleDayCell) getWidget(i, j);
+ if (dayCell == sdc) {
+ return i * cells + j;
+ }
+ }
+ }
+
+ return -1;
+ }
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/SimpleDayCell.java b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/SimpleDayCell.java
new file mode 100644
index 0000000000..45ae5ed8e5
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/SimpleDayCell.java
@@ -0,0 +1,736 @@
+/*
+ * Copyright 2000-2016 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.client.ui.calendar.schedule;
+
+import java.util.Date;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.dom.client.NativeEvent;
+import com.google.gwt.event.dom.client.KeyCodes;
+import com.google.gwt.event.dom.client.KeyDownEvent;
+import com.google.gwt.event.dom.client.KeyDownHandler;
+import com.google.gwt.event.dom.client.MouseDownEvent;
+import com.google.gwt.event.dom.client.MouseDownHandler;
+import com.google.gwt.event.dom.client.MouseMoveEvent;
+import com.google.gwt.event.dom.client.MouseMoveHandler;
+import com.google.gwt.event.dom.client.MouseOverEvent;
+import com.google.gwt.event.dom.client.MouseOverHandler;
+import com.google.gwt.event.dom.client.MouseUpEvent;
+import com.google.gwt.event.dom.client.MouseUpHandler;
+import com.google.gwt.event.shared.HandlerRegistration;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.Event;
+import com.google.gwt.user.client.ui.HTML;
+import com.google.gwt.user.client.ui.Label;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.client.ui.FocusableFlowPanel;
+import com.vaadin.client.ui.VCalendar;
+import com.vaadin.shared.ui.calendar.DateConstants;
+
+/**
+ * A class representing a single cell within the calendar in month-view
+ *
+ * @since 7.1
+ * @author Vaadin Ltd.
+ */
+public class SimpleDayCell extends FocusableFlowPanel implements MouseUpHandler,
+ MouseDownHandler, MouseOverHandler, MouseMoveHandler {
+
+ private static int BOTTOMSPACERHEIGHT = -1;
+ private static int EVENTHEIGHT = -1;
+ private static final int BORDERPADDINGSIZE = 1;
+
+ private final VCalendar calendar;
+ private Date date;
+ private int intHeight;
+ private final HTML bottomspacer;
+ private final Label caption;
+ private final CalendarEvent[] events = new CalendarEvent[10];
+ private final int cell;
+ private final int row;
+ private boolean monthNameVisible;
+ private HandlerRegistration mouseUpRegistration;
+ private HandlerRegistration mouseDownRegistration;
+ private HandlerRegistration mouseOverRegistration;
+ private boolean monthEventMouseDown;
+ private boolean labelMouseDown;
+ private int eventCount = 0;
+
+ private int startX = -1;
+ private int startY = -1;
+ private int startYrelative;
+ private int startXrelative;
+ // "from" date of date which is source of Dnd
+ private Date dndSourceDateFrom;
+ // "to" date of date which is source of Dnd
+ private Date dndSourceDateTo;
+ // "from" time of date which is source of Dnd
+ private Date dndSourceStartDateTime;
+ // "to" time of date which is source of Dnd
+ private Date dndSourceEndDateTime;
+
+ private int prevDayDiff = 0;
+ private int prevWeekDiff = 0;
+ private HandlerRegistration moveRegistration;
+ private CalendarEvent moveEvent;
+ private Widget clickedWidget;
+ private HandlerRegistration bottomSpacerMouseDownHandler;
+ private boolean scrollable = false;
+ private MonthGrid monthGrid;
+ private HandlerRegistration keyDownHandler;
+
+ public SimpleDayCell(VCalendar calendar, int row, int cell) {
+ this.calendar = calendar;
+ this.row = row;
+ this.cell = cell;
+ setStylePrimaryName("v-calendar-month-day");
+ caption = new Label();
+ bottomspacer = new HTML();
+ bottomspacer.setStyleName("v-calendar-bottom-spacer-empty");
+ bottomspacer.setWidth(3 + "em");
+ caption.setStyleName("v-calendar-day-number");
+ add(caption);
+ add(bottomspacer);
+ caption.addMouseDownHandler(this);
+ caption.addMouseUpHandler(this);
+ }
+
+ @Override
+ public void onLoad() {
+ BOTTOMSPACERHEIGHT = bottomspacer.getOffsetHeight();
+ EVENTHEIGHT = BOTTOMSPACERHEIGHT;
+ }
+
+ public void setMonthGrid(MonthGrid monthGrid) {
+ this.monthGrid = monthGrid;
+ }
+
+ public MonthGrid getMonthGrid() {
+ return monthGrid;
+ }
+
+ @SuppressWarnings("deprecation")
+ public void setDate(Date date) {
+ int dateOfMonth = date.getDate();
+ if (monthNameVisible) {
+ caption.setText(dateOfMonth + " "
+ + calendar.getMonthNames()[date.getMonth()]);
+ } else {
+ caption.setText("" + dateOfMonth);
+ }
+ this.date = date;
+ }
+
+ public Date getDate() {
+ return date;
+ }
+
+ public void reDraw(boolean clear) {
+ setHeightPX(intHeight + BORDERPADDINGSIZE, clear);
+ }
+
+ /*
+ * Events and whole cell content are drawn by this method. By the
+ * clear-argument, you can choose to clear all old content. Notice that
+ * clearing will also remove all element's event handlers.
+ */
+ public void setHeightPX(int px, boolean clear) {
+ // measure from DOM if needed
+ if (px < 0) {
+ intHeight = getOffsetHeight() - BORDERPADDINGSIZE;
+ } else {
+ intHeight = px - BORDERPADDINGSIZE;
+ }
+
+ // Couldn't measure height or it ended up negative. Don't bother
+ // continuing
+ if (intHeight == -1) {
+ return;
+ }
+
+ if (clear) {
+ while (getWidgetCount() > 1) {
+ remove(1);
+ }
+ }
+
+ // How many events can be shown in UI
+ int slots = 0;
+ if (scrollable) {
+ for (int i = 0; i < events.length; i++) {
+ if (events[i] != null) {
+ slots = i + 1;
+ }
+ }
+ setHeight(intHeight + "px"); // Fixed height
+ } else {
+ // Dynamic height by the content
+ DOM.removeElementAttribute(getElement(), "height");
+ slots = (intHeight - caption.getOffsetHeight() - BOTTOMSPACERHEIGHT)
+ / EVENTHEIGHT;
+ if (slots > 10) {
+ slots = 10;
+ }
+ }
+
+ updateEvents(slots, clear);
+
+ }
+
+ public void updateEvents(int slots, boolean clear) {
+ int eventsAdded = 0;
+
+ for (int i = 0; i < slots; i++) {
+ CalendarEvent e = events[i];
+ if (e == null) {
+ // Empty slot
+ HTML slot = new HTML();
+ slot.setStyleName("v-calendar-spacer");
+ if (!clear) {
+ remove(i + 1);
+ insert(slot, i + 1);
+ } else {
+ add(slot);
+ }
+ } else {
+ // Event slot
+ eventsAdded++;
+ if (!clear) {
+ Widget w = getWidget(i + 1);
+ if (!(w instanceof MonthEventLabel)) {
+ remove(i + 1);
+ insert(createMonthEventLabel(e), i + 1);
+ }
+ } else {
+ add(createMonthEventLabel(e));
+ }
+ }
+ }
+
+ int remainingSpace = intHeight - ((slots * EVENTHEIGHT)
+ + BOTTOMSPACERHEIGHT + caption.getOffsetHeight());
+ int newHeight = remainingSpace + BOTTOMSPACERHEIGHT;
+ if (newHeight < 0) {
+ newHeight = EVENTHEIGHT;
+ }
+ bottomspacer.setHeight(newHeight + "px");
+
+ if (clear) {
+ add(bottomspacer);
+ }
+
+ int more = eventCount - eventsAdded;
+ if (more > 0) {
+ if (bottomSpacerMouseDownHandler == null) {
+ bottomSpacerMouseDownHandler = bottomspacer
+ .addMouseDownHandler(this);
+ }
+ bottomspacer.setStyleName("v-calendar-bottom-spacer");
+ bottomspacer.setText("+ " + more);
+ } else {
+ if (!scrollable && bottomSpacerMouseDownHandler != null) {
+ bottomSpacerMouseDownHandler.removeHandler();
+ bottomSpacerMouseDownHandler = null;
+ }
+
+ if (scrollable) {
+ bottomspacer.setText("[ - ]");
+ } else {
+ bottomspacer.setStyleName("v-calendar-bottom-spacer-empty");
+ bottomspacer.setText("");
+ }
+ }
+ }
+
+ private MonthEventLabel createMonthEventLabel(CalendarEvent e) {
+ long rangeInMillis = e.getRangeInMilliseconds();
+ boolean timeEvent = rangeInMillis <= DateConstants.DAYINMILLIS
+ && !e.isAllDay();
+ Date fromDatetime = e.getStartTime();
+
+ // Create a new MonthEventLabel
+ MonthEventLabel eventDiv = new MonthEventLabel();
+ eventDiv.addStyleDependentName("month");
+ eventDiv.addMouseDownHandler(this);
+ eventDiv.addMouseUpHandler(this);
+ eventDiv.setCalendar(calendar);
+ eventDiv.setEventIndex(e.getIndex());
+ eventDiv.setCalendarEvent(e);
+
+ if (timeEvent) {
+ eventDiv.setTimeSpecificEvent(true);
+ if (e.getStyleName() != null) {
+ eventDiv.addStyleDependentName(e.getStyleName());
+ }
+ eventDiv.setCaption(e.getCaption());
+ eventDiv.setTime(fromDatetime);
+
+ } else {
+ eventDiv.setTimeSpecificEvent(false);
+ Date from = e.getStart();
+ Date to = e.getEnd();
+ if (e.getStyleName().length() > 0) {
+ eventDiv.addStyleName("month-event " + e.getStyleName());
+ } else {
+ eventDiv.addStyleName("month-event");
+ }
+ int fromCompareToDate = from.compareTo(date);
+ int toCompareToDate = to.compareTo(date);
+ eventDiv.addStyleDependentName("all-day");
+ if (fromCompareToDate == 0) {
+ eventDiv.addStyleDependentName("start");
+ eventDiv.setCaption(e.getCaption());
+
+ } else if (fromCompareToDate < 0 && cell == 0) {
+ eventDiv.addStyleDependentName("continued-from");
+ eventDiv.setCaption(e.getCaption());
+ }
+ if (toCompareToDate == 0) {
+ eventDiv.addStyleDependentName("end");
+ } else if (toCompareToDate > 0
+ && (cell + 1) == getMonthGrid().getCellCount(row)) {
+ eventDiv.addStyleDependentName("continued-to");
+ }
+ if (e.getStyleName() != null) {
+ eventDiv.addStyleDependentName(e.getStyleName() + "-all-day");
+ }
+ }
+
+ return eventDiv;
+ }
+
+ private void setUnlimitedCellHeight() {
+ scrollable = true;
+ addStyleDependentName("scrollable");
+ }
+
+ private void setLimitedCellHeight() {
+ scrollable = false;
+ removeStyleDependentName("scrollable");
+ }
+
+ public void addCalendarEvent(CalendarEvent e) {
+ eventCount++;
+ int slot = e.getSlotIndex();
+ if (slot == -1) {
+ for (int i = 0; i < events.length; i++) {
+ if (events[i] == null) {
+ events[i] = e;
+ e.setSlotIndex(i);
+ break;
+ }
+ }
+ } else {
+ events[slot] = e;
+ }
+ }
+
+ @SuppressWarnings("deprecation")
+ public void setMonthNameVisible(boolean b) {
+ monthNameVisible = b;
+ int dateOfMonth = date.getDate();
+ caption.setText(
+ dateOfMonth + " " + calendar.getMonthNames()[date.getMonth()]);
+ }
+
+ public HandlerRegistration addMouseMoveHandler(MouseMoveHandler handler) {
+ return addDomHandler(handler, MouseMoveEvent.getType());
+ }
+
+ @Override
+ protected void onAttach() {
+ super.onAttach();
+ mouseUpRegistration = addDomHandler(this, MouseUpEvent.getType());
+ mouseDownRegistration = addDomHandler(this, MouseDownEvent.getType());
+ mouseOverRegistration = addDomHandler(this, MouseOverEvent.getType());
+ }
+
+ @Override
+ protected void onDetach() {
+ mouseUpRegistration.removeHandler();
+ mouseDownRegistration.removeHandler();
+ mouseOverRegistration.removeHandler();
+ super.onDetach();
+ }
+
+ @Override
+ public void onMouseUp(MouseUpEvent event) {
+ if (event.getNativeButton() != NativeEvent.BUTTON_LEFT) {
+ return;
+ }
+
+ Widget w = (Widget) event.getSource();
+ if (moveRegistration != null) {
+ Event.releaseCapture(getElement());
+ moveRegistration.removeHandler();
+ moveRegistration = null;
+ keyDownHandler.removeHandler();
+ keyDownHandler = null;
+ }
+
+ if (w == bottomspacer && monthEventMouseDown) {
+ GWT.log("Mouse up over bottomspacer");
+
+ } else if (clickedWidget instanceof MonthEventLabel
+ && monthEventMouseDown) {
+ MonthEventLabel mel = (MonthEventLabel) clickedWidget;
+
+ int endX = event.getClientX();
+ int endY = event.getClientY();
+ int xDiff = 0, yDiff = 0;
+ if (startX != -1 && startY != -1) {
+ xDiff = startX - endX;
+ yDiff = startY - endY;
+ }
+ startX = -1;
+ startY = -1;
+ prevDayDiff = 0;
+ prevWeekDiff = 0;
+
+ if (xDiff < -3 || xDiff > 3 || yDiff < -3 || yDiff > 3) {
+ eventMoved(moveEvent);
+
+ } else if (calendar.getEventClickListener() != null) {
+ CalendarEvent e = getEventByWidget(mel);
+ calendar.getEventClickListener().eventClick(e);
+ }
+
+ moveEvent = null;
+ } else if (w == this) {
+ getMonthGrid().setSelectionReady();
+
+ } else if (w instanceof Label && labelMouseDown) {
+ String clickedDate = calendar.getDateFormat().format(date);
+ if (calendar.getDateClickListener() != null) {
+ calendar.getDateClickListener().dateClick(clickedDate);
+ }
+ }
+ monthEventMouseDown = false;
+ labelMouseDown = false;
+ clickedWidget = null;
+ }
+
+ @Override
+ public void onMouseDown(MouseDownEvent event) {
+ if (calendar.isDisabled()
+ || event.getNativeButton() != NativeEvent.BUTTON_LEFT) {
+ return;
+ }
+
+ Widget w = (Widget) event.getSource();
+ clickedWidget = w;
+
+ if (w instanceof MonthEventLabel) {
+ // event clicks should be allowed even when read-only
+ monthEventMouseDown = true;
+
+ if (calendar.isEventMoveAllowed()) {
+ startCalendarEventDrag(event, (MonthEventLabel) w);
+ }
+ } else if (w == bottomspacer) {
+ if (scrollable) {
+ setLimitedCellHeight();
+ } else {
+ setUnlimitedCellHeight();
+ }
+ reDraw(true);
+ } else if (w instanceof Label) {
+ labelMouseDown = true;
+ } else if (w == this && !scrollable) {
+ MonthGrid grid = getMonthGrid();
+ if (grid.isEnabled() && calendar.isRangeSelectAllowed()) {
+ grid.setSelectionStart(this);
+ grid.setSelectionEnd(this);
+ }
+ }
+
+ event.stopPropagation();
+ event.preventDefault();
+ }
+
+ @Override
+ public void onMouseOver(MouseOverEvent event) {
+ event.preventDefault();
+ getMonthGrid().setSelectionEnd(this);
+ }
+
+ @Override
+ public void onMouseMove(MouseMoveEvent event) {
+ if (clickedWidget instanceof MonthEventLabel && !monthEventMouseDown
+ || (startY < 0 && startX < 0)) {
+ return;
+ }
+
+ MonthEventLabel w = (MonthEventLabel) clickedWidget;
+
+ if (calendar.isDisabledOrReadOnly()) {
+ Event.releaseCapture(getElement());
+ monthEventMouseDown = false;
+ startY = -1;
+ startX = -1;
+ return;
+ }
+
+ int currentY = event.getClientY();
+ int currentX = event.getClientX();
+ int moveY = (currentY - startY);
+ int moveX = (currentX - startX);
+ if ((moveY < 5 && moveY > -6) && (moveX < 5 && moveX > -6)) {
+ return;
+ }
+
+ int dateCellWidth = getWidth();
+ int dateCellHeigth = getHeigth();
+
+ Element parent = getMonthGrid().getElement();
+ int relativeX = event.getRelativeX(parent);
+ int relativeY = event.getRelativeY(parent);
+ int weekDiff = 0;
+ if (moveY > 0) {
+ weekDiff = (startYrelative + moveY) / dateCellHeigth;
+ } else {
+ weekDiff = (moveY - (dateCellHeigth - startYrelative))
+ / dateCellHeigth;
+ }
+
+ int dayDiff = 0;
+ if (moveX >= 0) {
+ dayDiff = (startXrelative + moveX) / dateCellWidth;
+ } else {
+ dayDiff = (moveX - (dateCellWidth - startXrelative))
+ / dateCellWidth;
+ }
+ // Check boundaries
+ if (relativeY < 0
+ || relativeY >= (calendar.getMonthGrid().getRowCount()
+ * dateCellHeigth)
+ || relativeX < 0
+ || relativeX >= (calendar.getMonthGrid().getColumnCount()
+ * dateCellWidth)) {
+ return;
+ }
+
+ GWT.log("Event moving delta: " + weekDiff + " weeks " + dayDiff
+ + " days" + " (" + getCell() + "," + getRow() + ")");
+
+ CalendarEvent e = moveEvent;
+ if (e == null) {
+ e = getEventByWidget(w);
+ }
+
+ Date from = e.getStart();
+ Date to = e.getEnd();
+
+ long daysMs = dayDiff * DateConstants.DAYINMILLIS;
+ long weeksMs = weekDiff * DateConstants.WEEKINMILLIS;
+
+ setDates(e, from, to, weeksMs + daysMs, false);
+ e.setStart(from);
+ e.setEnd(to);
+ if (w.isTimeSpecificEvent()) {
+ Date start = new Date();
+ Date end = new Date();
+ setDates(e, start, end, weeksMs + daysMs, true);
+ e.setStartTime(start);
+ e.setEndTime(end);
+ } else {
+ e.setStartTime(new Date(from.getTime()));
+ e.setEndTime(new Date(to.getTime()));
+ }
+
+ updateDragPosition(w, dayDiff, weekDiff);
+ }
+
+ private void setDates(CalendarEvent e, Date start, Date end, long shift,
+ boolean isDateTime) {
+ Date currentStart;
+ Date currentEnd;
+ if (isDateTime) {
+ currentStart = e.getStartTime();
+ currentEnd = e.getEndTime();
+ } else {
+ currentStart = e.getStart();
+ currentEnd = e.getEnd();
+ }
+ long duration = currentEnd.getTime() - currentStart.getTime();
+ if (isDateTime) {
+ start.setTime(dndSourceStartDateTime.getTime() + shift);
+ } else {
+ start.setTime(dndSourceDateFrom.getTime() + shift);
+ }
+ end.setTime((start.getTime() + duration));
+ }
+
+ private void eventMoved(CalendarEvent e) {
+ calendar.updateEventToMonthGrid(e);
+ if (calendar.getEventMovedListener() != null) {
+ calendar.getEventMovedListener().eventMoved(e);
+ }
+ }
+
+ public void startCalendarEventDrag(MouseDownEvent event,
+ final MonthEventLabel w) {
+ moveRegistration = addMouseMoveHandler(this);
+ startX = event.getClientX();
+ startY = event.getClientY();
+ startYrelative = event.getRelativeY(w.getParent().getElement())
+ % getHeigth();
+ startXrelative = event.getRelativeX(w.getParent().getElement())
+ % getWidth();
+
+ CalendarEvent e = getEventByWidget(w);
+ dndSourceDateFrom = (Date) e.getStart().clone();
+ dndSourceDateTo = (Date) e.getEnd().clone();
+
+ dndSourceStartDateTime = (Date) e.getStartTime().clone();
+ dndSourceEndDateTime = (Date) e.getEndTime().clone();
+
+ Event.setCapture(getElement());
+ keyDownHandler = addKeyDownHandler(new KeyDownHandler() {
+
+ @Override
+ public void onKeyDown(KeyDownEvent event) {
+ if (event.getNativeKeyCode() == KeyCodes.KEY_ESCAPE) {
+ cancelEventDrag(w);
+ }
+ }
+
+ });
+
+ focus();
+
+ GWT.log("Start drag");
+ }
+
+ protected void cancelEventDrag(MonthEventLabel w) {
+ if (moveRegistration != null) {
+ // reset position
+ if (moveEvent == null) {
+ moveEvent = getEventByWidget(w);
+ }
+
+ moveEvent.setStart(dndSourceDateFrom);
+ moveEvent.setEnd(dndSourceDateTo);
+ moveEvent.setStartTime(dndSourceStartDateTime);
+ moveEvent.setEndTime(dndSourceEndDateTime);
+ calendar.updateEventToMonthGrid(moveEvent);
+
+ // reset drag-related properties
+ Event.releaseCapture(getElement());
+ moveRegistration.removeHandler();
+ moveRegistration = null;
+ keyDownHandler.removeHandler();
+ keyDownHandler = null;
+ setFocus(false);
+ monthEventMouseDown = false;
+ startY = -1;
+ startX = -1;
+ moveEvent = null;
+ labelMouseDown = false;
+ clickedWidget = null;
+ }
+ }
+
+ public void updateDragPosition(MonthEventLabel w, int dayDiff,
+ int weekDiff) {
+ // Draw event to its new position only when position has changed
+ if (dayDiff == prevDayDiff && weekDiff == prevWeekDiff) {
+ return;
+ }
+
+ prevDayDiff = dayDiff;
+ prevWeekDiff = weekDiff;
+
+ if (moveEvent == null) {
+ moveEvent = getEventByWidget(w);
+ }
+
+ calendar.updateEventToMonthGrid(moveEvent);
+ }
+
+ public int getRow() {
+ return row;
+ }
+
+ public int getCell() {
+ return cell;
+ }
+
+ public int getHeigth() {
+ return intHeight + BORDERPADDINGSIZE;
+ }
+
+ public int getWidth() {
+ return getOffsetWidth() - BORDERPADDINGSIZE;
+ }
+
+ public void setToday(boolean today) {
+ if (today) {
+ addStyleDependentName("today");
+ } else {
+ removeStyleDependentName("today");
+ }
+ }
+
+ public boolean removeEvent(CalendarEvent targetEvent,
+ boolean reDrawImmediately) {
+ int slot = targetEvent.getSlotIndex();
+ if (slot < 0) {
+ return false;
+ }
+
+ CalendarEvent e = getCalendarEvent(slot);
+ if (targetEvent.equals(e)) {
+ events[slot] = null;
+ eventCount--;
+ if (reDrawImmediately) {
+ reDraw(moveEvent == null);
+ }
+ return true;
+ }
+ return false;
+ }
+
+ private CalendarEvent getEventByWidget(MonthEventLabel eventWidget) {
+ int index = getWidgetIndex(eventWidget);
+ return getCalendarEvent(index - 1);
+ }
+
+ public CalendarEvent getCalendarEvent(int i) {
+ return events[i];
+ }
+
+ public CalendarEvent[] getEvents() {
+ return events;
+ }
+
+ public int getEventCount() {
+ return eventCount;
+ }
+
+ public CalendarEvent getMoveEvent() {
+ return moveEvent;
+ }
+
+ public void addEmphasisStyle() {
+ addStyleDependentName("dragemphasis");
+ }
+
+ public void removeEmphasisStyle() {
+ removeStyleDependentName("dragemphasis");
+ }
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/SimpleDayToolbar.java b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/SimpleDayToolbar.java
new file mode 100644
index 0000000000..e83a2cce3a
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/SimpleDayToolbar.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2000-2016 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.client.ui.calendar.schedule;
+
+import com.google.gwt.user.client.ui.HorizontalPanel;
+import com.google.gwt.user.client.ui.Label;
+import com.google.gwt.user.client.ui.Widget;
+
+/**
+ *
+ * @since 7.1.0
+ * @author Vaadin Ltd.
+ *
+ */
+public class SimpleDayToolbar extends HorizontalPanel {
+ private int width = 0;
+ private boolean isWidthUndefined = false;
+
+ public SimpleDayToolbar() {
+ setStylePrimaryName("v-calendar-header-month");
+ }
+
+ public void setDayNames(String[] dayNames) {
+ clear();
+ for (int i = 0; i < dayNames.length; i++) {
+ Label l = new Label(dayNames[i]);
+ l.setStylePrimaryName("v-calendar-header-day");
+ add(l);
+ }
+ updateCellWidth();
+ }
+
+ public void setWidthPX(int width) {
+ this.width = width;
+
+ setWidthUndefined(width == -1);
+
+ if (!isWidthUndefined()) {
+ super.setWidth(this.width + "px");
+ if (getWidgetCount() == 0) {
+ return;
+ }
+ }
+ updateCellWidth();
+ }
+
+ private boolean isWidthUndefined() {
+ return isWidthUndefined;
+ }
+
+ private void setWidthUndefined(boolean isWidthUndefined) {
+ this.isWidthUndefined = isWidthUndefined;
+
+ if (isWidthUndefined) {
+ addStyleDependentName("Hsized");
+
+ } else {
+ removeStyleDependentName("Hsized");
+ }
+ }
+
+ private void updateCellWidth() {
+ int cellw = -1;
+ int widgetCount = getWidgetCount();
+ if (widgetCount <= 0) {
+ return;
+ }
+ if (isWidthUndefined()) {
+ Widget widget = getWidget(0);
+ String w = widget.getElement().getStyle().getWidth();
+ if (w.length() > 2) {
+ cellw = Integer.parseInt(w.substring(0, w.length() - 2));
+ }
+ } else {
+ cellw = width / getWidgetCount();
+ }
+ if (cellw > 0) {
+ for (int i = 0; i < getWidgetCount(); i++) {
+ Widget widget = getWidget(i);
+ setCellWidth(widget, cellw + "px");
+ }
+ }
+ }
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/SimpleWeekToolbar.java b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/SimpleWeekToolbar.java
new file mode 100644
index 0000000000..30c52e1059
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/SimpleWeekToolbar.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright 2000-2016 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.client.ui.calendar.schedule;
+
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.user.client.ui.FlexTable;
+import com.vaadin.client.ui.VCalendar;
+
+/**
+ *
+ * @since 7.1
+ * @author Vaadin Ltd.
+ *
+ */
+public class SimpleWeekToolbar extends FlexTable implements ClickHandler {
+ private int height;
+ private VCalendar calendar;
+ private boolean isHeightUndefined;
+
+ public SimpleWeekToolbar(VCalendar parent) {
+ calendar = parent;
+ setCellSpacing(0);
+ setCellPadding(0);
+ setStyleName("v-calendar-week-numbers");
+ }
+
+ public void addWeek(int week, int year) {
+ WeekLabel l = new WeekLabel(week + "", week, year);
+ l.addClickHandler(this);
+ int rowCount = getRowCount();
+ insertRow(rowCount);
+ setWidget(rowCount, 0, l);
+ updateCellHeights();
+ }
+
+ public void updateCellHeights() {
+ if (!isHeightUndefined()) {
+ int rowCount = getRowCount();
+ if (rowCount == 0) {
+ return;
+ }
+ int cellheight = (height / rowCount) - 1;
+ int remainder = height % rowCount;
+ if (cellheight < 0) {
+ cellheight = 0;
+ }
+ for (int i = 0; i < rowCount; i++) {
+ if (remainder > 0) {
+ getWidget(i, 0).setHeight(cellheight + 1 + "px");
+ } else {
+ getWidget(i, 0).setHeight(cellheight + "px");
+ }
+ getWidget(i, 0).getElement().getStyle()
+ .setProperty("lineHeight", cellheight + "px");
+ remainder--;
+ }
+ } else {
+ for (int i = 0; i < getRowCount(); i++) {
+ getWidget(i, 0).setHeight("");
+ getWidget(i, 0).getElement().getStyle()
+ .setProperty("lineHeight", "");
+ }
+ }
+ }
+
+ public void setHeightPX(int intHeight) {
+ setHeightUndefined(intHeight == -1);
+ height = intHeight;
+ updateCellHeights();
+ }
+
+ public boolean isHeightUndefined() {
+ return isHeightUndefined;
+ }
+
+ public void setHeightUndefined(boolean isHeightUndefined) {
+ this.isHeightUndefined = isHeightUndefined;
+
+ if (isHeightUndefined) {
+ addStyleDependentName("Vsized");
+
+ } else {
+ removeStyleDependentName("Vsized");
+ }
+ }
+
+ @Override
+ public void onClick(ClickEvent event) {
+ WeekLabel wl = (WeekLabel) event.getSource();
+ if (calendar.getWeekClickListener() != null) {
+ calendar.getWeekClickListener()
+ .weekClick(wl.getYear() + "w" + wl.getWeek());
+ }
+ }
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/WeekGrid.java b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/WeekGrid.java
new file mode 100644
index 0000000000..cfc9d6231a
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/WeekGrid.java
@@ -0,0 +1,692 @@
+/*
+ * Copyright 2000-2016 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.client.ui.calendar.schedule;
+
+import java.util.Arrays;
+import java.util.Date;
+
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.dom.client.Style;
+import com.google.gwt.dom.client.Style.Unit;
+import com.google.gwt.event.dom.client.ScrollEvent;
+import com.google.gwt.event.dom.client.ScrollHandler;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.ui.HTML;
+import com.google.gwt.user.client.ui.HorizontalPanel;
+import com.google.gwt.user.client.ui.Panel;
+import com.google.gwt.user.client.ui.ScrollPanel;
+import com.google.gwt.user.client.ui.SimplePanel;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.client.DateTimeService;
+import com.vaadin.client.WidgetUtil;
+import com.vaadin.client.ui.VCalendar;
+import com.vaadin.shared.ui.calendar.DateConstants;
+
+/**
+ *
+ * @since 7.1
+ * @author Vaadin Ltd.
+ *
+ */
+public class WeekGrid extends SimplePanel {
+
+ int width = 0;
+ private int height = 0;
+ final HorizontalPanel content;
+ private VCalendar calendar;
+ private boolean disabled;
+ final Timebar timebar;
+ private Panel wrapper;
+ private boolean verticalScrollEnabled;
+ private boolean horizontalScrollEnabled;
+ private int[] cellHeights;
+ private final int slotInMinutes = 30;
+ private int dateCellBorder;
+ private DateCell dateCellOfToday;
+ private int[] cellWidths;
+ private int firstHour;
+ private int lastHour;
+
+ public WeekGrid(VCalendar parent, boolean format24h) {
+ setCalendar(parent);
+ content = new HorizontalPanel();
+ timebar = new Timebar(format24h);
+ content.add(timebar);
+
+ wrapper = new SimplePanel();
+ wrapper.setStylePrimaryName("v-calendar-week-wrapper");
+ wrapper.add(content);
+
+ setWidget(wrapper);
+ }
+
+ private void setVerticalScroll(boolean isVerticalScrollEnabled) {
+ if (isVerticalScrollEnabled && !(isVerticalScrollable())) {
+ verticalScrollEnabled = true;
+ horizontalScrollEnabled = false;
+ wrapper.remove(content);
+
+ final ScrollPanel scrollPanel = new ScrollPanel();
+ scrollPanel.setStylePrimaryName("v-calendar-week-wrapper");
+ scrollPanel.setWidget(content);
+
+ scrollPanel.addScrollHandler(new ScrollHandler() {
+ @Override
+ public void onScroll(ScrollEvent event) {
+ if (calendar.getScrollListener() != null) {
+ calendar.getScrollListener().scroll(
+ scrollPanel.getVerticalScrollPosition());
+ }
+ }
+ });
+
+ setWidget(scrollPanel);
+ wrapper = scrollPanel;
+
+ } else if (!isVerticalScrollEnabled && (isVerticalScrollable())) {
+ verticalScrollEnabled = false;
+ horizontalScrollEnabled = false;
+ wrapper.remove(content);
+
+ SimplePanel simplePanel = new SimplePanel();
+ simplePanel.setStylePrimaryName("v-calendar-week-wrapper");
+ simplePanel.setWidget(content);
+
+ setWidget(simplePanel);
+ wrapper = simplePanel;
+ }
+ }
+
+ public void setVerticalScrollPosition(int verticalScrollPosition) {
+ if (isVerticalScrollable()) {
+ ((ScrollPanel) wrapper)
+ .setVerticalScrollPosition(verticalScrollPosition);
+ }
+ }
+
+ public int getInternalWidth() {
+ return width;
+ }
+
+ public void addDate(Date d) {
+ final DateCell dc = new DateCell(this, d);
+ dc.setDisabled(isDisabled());
+ dc.setHorizontalSized(isHorizontalScrollable() || width < 0);
+ dc.setVerticalSized(isVerticalScrollable());
+ content.add(dc);
+ }
+
+ /**
+ * @param dateCell
+ * @return get the index of the given date cell in this week, starting from
+ * 0
+ */
+ public int getDateCellIndex(DateCell dateCell) {
+ return content.getWidgetIndex(dateCell) - 1;
+ }
+
+ /**
+ * @return get the slot border in pixels
+ */
+ public int getDateSlotBorder() {
+ return ((DateCell) content.getWidget(1)).getSlotBorder();
+ }
+
+ private boolean isVerticalScrollable() {
+ return verticalScrollEnabled;
+ }
+
+ private boolean isHorizontalScrollable() {
+ return horizontalScrollEnabled;
+ }
+
+ public void setWidthPX(int width) {
+ if (isHorizontalScrollable()) {
+ updateCellWidths();
+
+ // Otherwise the scroll wrapper is somehow too narrow = horizontal
+ // scroll
+ wrapper.setWidth(content.getOffsetWidth()
+ + WidgetUtil.getNativeScrollbarSize() + "px");
+
+ this.width = content.getOffsetWidth() - timebar.getOffsetWidth();
+
+ } else {
+ this.width = (width == -1) ? width
+ : width - timebar.getOffsetWidth();
+
+ if (isVerticalScrollable() && width != -1) {
+ this.width = this.width - WidgetUtil.getNativeScrollbarSize();
+ }
+ updateCellWidths();
+ }
+ }
+
+ public void setHeightPX(int intHeight) {
+ height = intHeight;
+
+ setVerticalScroll(height <= -1);
+
+ // if not scrollable, use any height given
+ if (!isVerticalScrollable() && height > 0) {
+
+ content.setHeight(height + "px");
+ setHeight(height + "px");
+ wrapper.setHeight(height + "px");
+ wrapper.removeStyleDependentName("Vsized");
+ updateCellHeights();
+ timebar.setCellHeights(cellHeights);
+ timebar.setHeightPX(height);
+
+ } else if (isVerticalScrollable()) {
+ updateCellHeights();
+ wrapper.addStyleDependentName("Vsized");
+ timebar.setCellHeights(cellHeights);
+ timebar.setHeightPX(height);
+ }
+ }
+
+ public void clearDates() {
+ while (content.getWidgetCount() > 1) {
+ content.remove(1);
+ }
+
+ dateCellOfToday = null;
+ }
+
+ /**
+ * @return true if this weekgrid contains a date that is today
+ */
+ public boolean hasToday() {
+ return dateCellOfToday != null;
+ }
+
+ public void updateCellWidths() {
+ if (!isHorizontalScrollable() && width != -1) {
+ int count = content.getWidgetCount();
+ int datesWidth = width;
+ if (datesWidth > 0 && count > 1) {
+ cellWidths = VCalendar.distributeSize(datesWidth, count - 1,
+ -1);
+
+ for (int i = 1; i < count; i++) {
+ DateCell dc = (DateCell) content.getWidget(i);
+ dc.setHorizontalSized(
+ isHorizontalScrollable() || width < 0);
+ dc.setWidthPX(cellWidths[i - 1]);
+ if (dc.isToday()) {
+ dc.setTimeBarWidth(getOffsetWidth());
+ }
+ }
+ }
+
+ } else {
+ int count = content.getWidgetCount();
+ if (count > 1) {
+ for (int i = 1; i < count; i++) {
+ DateCell dc = (DateCell) content.getWidget(i);
+ dc.setHorizontalSized(
+ isHorizontalScrollable() || width < 0);
+ }
+ }
+ }
+ }
+
+ /**
+ * @return an int-array containing the widths of the cells (days)
+ */
+ public int[] getDateCellWidths() {
+ return cellWidths;
+ }
+
+ public void updateCellHeights() {
+ if (!isVerticalScrollable()) {
+ int count = content.getWidgetCount();
+ if (count > 1) {
+ DateCell first = (DateCell) content.getWidget(1);
+ dateCellBorder = first.getSlotBorder();
+ cellHeights = VCalendar.distributeSize(height,
+ first.getNumberOfSlots(), -dateCellBorder);
+ for (int i = 1; i < count; i++) {
+ DateCell dc = (DateCell) content.getWidget(i);
+ dc.setHeightPX(height, cellHeights);
+ }
+ }
+
+ } else {
+ int count = content.getWidgetCount();
+ if (count > 1) {
+ DateCell first = (DateCell) content.getWidget(1);
+ dateCellBorder = first.getSlotBorder();
+ int dateHeight = (first.getOffsetHeight()
+ / first.getNumberOfSlots()) - dateCellBorder;
+ cellHeights = new int[48];
+ Arrays.fill(cellHeights, dateHeight);
+
+ for (int i = 1; i < count; i++) {
+ DateCell dc = (DateCell) content.getWidget(i);
+ dc.setVerticalSized(isVerticalScrollable());
+ }
+ }
+ }
+ }
+
+ public void addEvent(CalendarEvent e) {
+ int dateCount = content.getWidgetCount();
+ Date from = e.getStart();
+ Date toTime = e.getEndTime();
+ for (int i = 1; i < dateCount; i++) {
+ DateCell dc = (DateCell) content.getWidget(i);
+ Date dcDate = dc.getDate();
+ int comp = dcDate.compareTo(from);
+ int comp2 = dcDate.compareTo(toTime);
+ if (comp >= 0 && comp2 < 0 || (comp == 0 && comp2 == 0
+ && VCalendar.isZeroLengthMidnightEvent(e))) {
+ // Same event may be over two DateCells if event's date
+ // range floats over one day. It can't float over two days,
+ // because event which range is over 24 hours, will be handled
+ // as a "fullDay" event.
+ dc.addEvent(dcDate, e);
+ }
+ }
+ }
+
+ public int getPixelLengthFor(int startFromMinutes, int durationInMinutes) {
+ int pixelLength = 0;
+ int currentSlot = 0;
+
+ int firstHourInMinutes = firstHour * DateConstants.HOURINMINUTES;
+ int endHourInMinutes = lastHour * DateConstants.HOURINMINUTES;
+
+ if (firstHourInMinutes > startFromMinutes) {
+ durationInMinutes = durationInMinutes
+ - (firstHourInMinutes - startFromMinutes);
+ startFromMinutes = 0;
+ } else {
+ startFromMinutes -= firstHourInMinutes;
+ }
+
+ int shownHeightInMinutes = endHourInMinutes - firstHourInMinutes
+ + DateConstants.HOURINMINUTES;
+
+ durationInMinutes = Math.min(durationInMinutes,
+ shownHeightInMinutes - startFromMinutes);
+
+ // calculate full slots to event
+ int slotsTillEvent = startFromMinutes / slotInMinutes;
+ int startOverFlowTime = slotInMinutes
+ - (startFromMinutes % slotInMinutes);
+ if (startOverFlowTime == slotInMinutes) {
+ startOverFlowTime = 0;
+ currentSlot = slotsTillEvent;
+ } else {
+ currentSlot = slotsTillEvent + 1;
+ }
+
+ int durationInSlots = 0;
+ int endOverFlowTime = 0;
+
+ if (startOverFlowTime > 0) {
+ durationInSlots = (durationInMinutes - startOverFlowTime)
+ / slotInMinutes;
+ endOverFlowTime = (durationInMinutes - startOverFlowTime)
+ % slotInMinutes;
+
+ } else {
+ durationInSlots = durationInMinutes / slotInMinutes;
+ endOverFlowTime = durationInMinutes % slotInMinutes;
+ }
+
+ // calculate slot overflow at start
+ if (startOverFlowTime > 0 && currentSlot < cellHeights.length) {
+ int lastSlotHeight = cellHeights[currentSlot] + dateCellBorder;
+ pixelLength += (int) (((double) lastSlotHeight
+ / (double) slotInMinutes) * startOverFlowTime);
+ }
+
+ // calculate length in full slots
+ int lastFullSlot = currentSlot + durationInSlots;
+ for (; currentSlot < lastFullSlot
+ && currentSlot < cellHeights.length; currentSlot++) {
+ pixelLength += cellHeights[currentSlot] + dateCellBorder;
+ }
+
+ // calculate overflow at end
+ if (endOverFlowTime > 0 && currentSlot < cellHeights.length) {
+ int lastSlotHeight = cellHeights[currentSlot] + dateCellBorder;
+ pixelLength += (int) (((double) lastSlotHeight
+ / (double) slotInMinutes) * endOverFlowTime);
+ }
+
+ // reduce possible underflow at end
+ if (endOverFlowTime < 0) {
+ int lastSlotHeight = cellHeights[currentSlot] + dateCellBorder;
+ pixelLength += (int) (((double) lastSlotHeight
+ / (double) slotInMinutes) * endOverFlowTime);
+ }
+
+ return pixelLength;
+ }
+
+ public int getPixelTopFor(int startFromMinutes) {
+ int pixelsToTop = 0;
+ int slotIndex = 0;
+
+ int firstHourInMinutes = firstHour * 60;
+
+ if (firstHourInMinutes > startFromMinutes) {
+ startFromMinutes = 0;
+ } else {
+ startFromMinutes -= firstHourInMinutes;
+ }
+
+ // calculate full slots to event
+ int slotsTillEvent = startFromMinutes / slotInMinutes;
+ int overFlowTime = startFromMinutes % slotInMinutes;
+ if (slotsTillEvent > 0) {
+ for (slotIndex = 0; slotIndex < slotsTillEvent; slotIndex++) {
+ pixelsToTop += cellHeights[slotIndex] + dateCellBorder;
+ }
+ }
+
+ // calculate lengths less than one slot
+ if (overFlowTime > 0) {
+ int lastSlotHeight = cellHeights[slotIndex] + dateCellBorder;
+ pixelsToTop += ((double) lastSlotHeight / (double) slotInMinutes)
+ * overFlowTime;
+ }
+
+ return pixelsToTop;
+ }
+
+ public void eventMoved(DateCellDayEvent dayEvent) {
+ Style s = dayEvent.getElement().getStyle();
+ int left = Integer
+ .parseInt(s.getLeft().substring(0, s.getLeft().length() - 2));
+ DateCell previousParent = (DateCell) dayEvent.getParent();
+ DateCell newParent = (DateCell) content
+ .getWidget((left / getDateCellWidth()) + 1);
+ CalendarEvent se = dayEvent.getCalendarEvent();
+ previousParent.removeEvent(dayEvent);
+ newParent.addEvent(dayEvent);
+ if (!previousParent.equals(newParent)) {
+ previousParent.recalculateEventWidths();
+ }
+ newParent.recalculateEventWidths();
+ if (calendar.getEventMovedListener() != null) {
+ calendar.getEventMovedListener().eventMoved(se);
+ }
+ }
+
+ public void setToday(Date todayDate, Date todayTimestamp) {
+ int count = content.getWidgetCount();
+ if (count > 1) {
+ for (int i = 1; i < count; i++) {
+ DateCell dc = (DateCell) content.getWidget(i);
+ if (dc.getDate().getTime() == todayDate.getTime()) {
+ if (isVerticalScrollable()) {
+ dc.setToday(todayTimestamp, -1);
+ } else {
+ dc.setToday(todayTimestamp, getOffsetWidth());
+ }
+ }
+ dateCellOfToday = dc;
+ }
+ }
+ }
+
+ public DateCell getDateCellOfToday() {
+ return dateCellOfToday;
+ }
+
+ public void setDisabled(boolean disabled) {
+ this.disabled = disabled;
+ }
+
+ public boolean isDisabled() {
+ return disabled;
+ }
+
+ public Timebar getTimeBar() {
+ return timebar;
+ }
+
+ public void setDateColor(Date when, Date to, String styleName) {
+ int dateCount = content.getWidgetCount();
+ for (int i = 1; i < dateCount; i++) {
+ DateCell dc = (DateCell) content.getWidget(i);
+ Date dcDate = dc.getDate();
+ int comp = dcDate.compareTo(when);
+ int comp2 = dcDate.compareTo(to);
+ if (comp >= 0 && comp2 <= 0) {
+ dc.setDateColor(styleName);
+ }
+ }
+ }
+
+ /**
+ * @param calendar
+ * the calendar to set
+ */
+ public void setCalendar(VCalendar calendar) {
+ this.calendar = calendar;
+ }
+
+ /**
+ * @return the calendar
+ */
+ public VCalendar getCalendar() {
+ return calendar;
+ }
+
+ /**
+ * Get width of the single date cell
+ *
+ * @return Date cell width
+ */
+ public int getDateCellWidth() {
+ int count = content.getWidgetCount() - 1;
+ int cellWidth = -1;
+ if (count <= 0) {
+ return cellWidth;
+ }
+
+ if (width == -1) {
+ Widget firstWidget = content.getWidget(1);
+ cellWidth = firstWidget.getElement().getOffsetWidth();
+ } else {
+ cellWidth = getInternalWidth() / count;
+ }
+ return cellWidth;
+ }
+
+ /**
+ * @return the number of day cells in this week
+ */
+ public int getDateCellCount() {
+ return content.getWidgetCount() - 1;
+ }
+
+ public void setFirstHour(int firstHour) {
+ this.firstHour = firstHour;
+ timebar.setFirstHour(firstHour);
+ }
+
+ public void setLastHour(int lastHour) {
+ this.lastHour = lastHour;
+ timebar.setLastHour(lastHour);
+ }
+
+ public int getFirstHour() {
+ return firstHour;
+ }
+
+ public int getLastHour() {
+ return lastHour;
+ }
+
+ public static class Timebar extends HTML {
+
+ private static final int[] timesFor12h = { 12, 1, 2, 3, 4, 5, 6, 7, 8,
+ 9, 10, 11 };
+
+ private int height;
+
+ private final int verticalPadding = 7; // FIXME measure this from DOM
+
+ private int[] slotCellHeights;
+
+ private int firstHour;
+
+ private int lastHour;
+
+ public Timebar(boolean format24h) {
+ createTimeBar(format24h);
+ }
+
+ public void setLastHour(int lastHour) {
+ this.lastHour = lastHour;
+ }
+
+ public void setFirstHour(int firstHour) {
+ this.firstHour = firstHour;
+
+ }
+
+ public void setCellHeights(int[] cellHeights) {
+ slotCellHeights = cellHeights;
+ }
+
+ private void createTimeBar(boolean format24h) {
+ setStylePrimaryName("v-calendar-times");
+
+ // Fist "time" is empty
+ Element e = DOM.createDiv();
+ setStyleName(e, "v-calendar-time");
+ e.setInnerText("");
+ getElement().appendChild(e);
+
+ DateTimeService dts = new DateTimeService();
+
+ if (format24h) {
+ for (int i = firstHour + 1; i <= lastHour; i++) {
+ e = DOM.createDiv();
+ setStyleName(e, "v-calendar-time");
+ String delimiter = dts.getClockDelimeter();
+ e.setInnerHTML("<span>" + i + "</span>" + delimiter + "00");
+ getElement().appendChild(e);
+ }
+ } else {
+ // FIXME Use dts.getAmPmStrings(); and make sure that
+ // DateTimeService has a some Locale set.
+ String[] ampm = new String[] { "AM", "PM" };
+
+ int amStop = (lastHour < 11) ? lastHour : 11;
+ int pmStart = (firstHour > 11) ? firstHour % 11 : 0;
+
+ if (firstHour < 12) {
+ for (int i = firstHour + 1; i <= amStop; i++) {
+ e = DOM.createDiv();
+ setStyleName(e, "v-calendar-time");
+ e.setInnerHTML("<span>" + timesFor12h[i] + "</span>"
+ + " " + ampm[0]);
+ getElement().appendChild(e);
+ }
+ }
+
+ if (lastHour > 11) {
+ for (int i = pmStart; i < lastHour - 11; i++) {
+ e = DOM.createDiv();
+ setStyleName(e, "v-calendar-time");
+ e.setInnerHTML("<span>" + timesFor12h[i] + "</span>"
+ + " " + ampm[1]);
+ getElement().appendChild(e);
+ }
+ }
+ }
+ }
+
+ public void updateTimeBar(boolean format24h) {
+ clear();
+ createTimeBar(format24h);
+ }
+
+ private void clear() {
+ while (getElement().getChildCount() > 0) {
+ getElement().removeChild(getElement().getChild(0));
+ }
+ }
+
+ public void setHeightPX(int pixelHeight) {
+ height = pixelHeight;
+
+ if (pixelHeight > -1) {
+ // as the negative margins on children pulls the whole element
+ // upwards, we must compensate. otherwise the element would be
+ // too short
+ super.setHeight((height + verticalPadding) + "px");
+ removeStyleDependentName("Vsized");
+ updateChildHeights();
+
+ } else {
+ addStyleDependentName("Vsized");
+ updateChildHeights();
+ }
+ }
+
+ private void updateChildHeights() {
+ int childCount = getElement().getChildCount();
+
+ if (height != -1) {
+
+ // 23 hours + first is empty
+ // we try to adjust the height of time labels to the distributed
+ // heights of the time slots
+ int hoursPerDay = lastHour - firstHour + 1;
+
+ int slotsPerHour = slotCellHeights.length / hoursPerDay;
+ int[] cellHeights = new int[slotCellHeights.length
+ / slotsPerHour];
+
+ int slotHeightPosition = 0;
+ for (int i = 0; i < cellHeights.length; i++) {
+ for (int j = slotHeightPosition; j < slotHeightPosition
+ + slotsPerHour; j++) {
+ cellHeights[i] += slotCellHeights[j] + 1;
+ // 1px more for borders
+ // FIXME measure from DOM
+ }
+ slotHeightPosition += slotsPerHour;
+ }
+
+ for (int i = 0; i < childCount; i++) {
+ Element e = (Element) getElement().getChild(i);
+ e.getStyle().setHeight(cellHeights[i], Unit.PX);
+ }
+
+ } else {
+ for (int i = 0; i < childCount; i++) {
+ Element e = (Element) getElement().getChild(i);
+ e.getStyle().setProperty("height", "");
+ }
+ }
+ }
+ }
+
+ public VCalendar getParentCalendar() {
+ return calendar;
+ }
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/WeekGridMinuteTimeRange.java b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/WeekGridMinuteTimeRange.java
new file mode 100644
index 0000000000..984d3d48dc
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/WeekGridMinuteTimeRange.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2000-2016 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.client.ui.calendar.schedule;
+
+import java.util.Date;
+
+/**
+ * Internally used by the calendar
+ *
+ * @since 7.1
+ */
+public class WeekGridMinuteTimeRange {
+ private final Date start;
+ private final Date end;
+
+ /**
+ * Creates a Date time range between start and end date. Drops seconds from
+ * the range.
+ *
+ * @param start
+ * Start time of the range
+ * @param end
+ * End time of the range
+ * @param clearSeconds
+ * Boolean Indicates, if seconds should be dropped from the range
+ * start and end
+ */
+ public WeekGridMinuteTimeRange(Date start, Date end) {
+ this.start = new Date(start.getTime());
+ this.end = new Date(end.getTime());
+ this.start.setSeconds(0);
+ this.end.setSeconds(0);
+ }
+
+ public Date getStart() {
+ return start;
+ }
+
+ public Date getEnd() {
+ return end;
+ }
+
+ public static boolean doesOverlap(WeekGridMinuteTimeRange a,
+ WeekGridMinuteTimeRange b) {
+ boolean overlaps = a.getStart().compareTo(b.getEnd()) < 0
+ && a.getEnd().compareTo(b.getStart()) > 0;
+ return overlaps;
+ }
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/WeekLabel.java b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/WeekLabel.java
new file mode 100644
index 0000000000..ae7001cb21
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/WeekLabel.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2000-2016 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.client.ui.calendar.schedule;
+
+import com.google.gwt.user.client.ui.Label;
+
+/**
+ * A label in the {@link SimpleWeekToolbar}
+ *
+ * @since 7.1
+ */
+public class WeekLabel extends Label {
+ private int week;
+ private int year;
+
+ public WeekLabel(String string, int week2, int year2) {
+ super(string);
+ setStylePrimaryName("v-calendar-week-number");
+ week = week2;
+ year = year2;
+ }
+
+ public int getWeek() {
+ return week;
+ }
+
+ public void setWeek(int week) {
+ this.week = week;
+ }
+
+ public int getYear() {
+ return year;
+ }
+
+ public void setYear(int year) {
+ this.year = year;
+ }
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/WeeklyLongEvents.java b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/WeeklyLongEvents.java
new file mode 100644
index 0000000000..fe1f3e181e
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/WeeklyLongEvents.java
@@ -0,0 +1,189 @@
+/*
+ * Copyright 2000-2016 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.client.ui.calendar.schedule;
+
+import java.util.Date;
+import java.util.List;
+
+import com.google.gwt.user.client.ui.HorizontalPanel;
+import com.vaadin.client.ui.VCalendar;
+
+/**
+ *
+ * @since 7.1
+ * @author Vaadin Ltd.
+ *
+ */
+public class WeeklyLongEvents extends HorizontalPanel implements HasTooltipKey {
+
+ public static final int EVENT_HEIGTH = 15;
+
+ public static final int EVENT_MARGIN = 1;
+
+ private int rowCount = 0;
+
+ private VCalendar calendar;
+
+ private boolean undefinedWidth;
+
+ public WeeklyLongEvents(VCalendar calendar) {
+ setStylePrimaryName("v-calendar-weekly-longevents");
+ this.calendar = calendar;
+ }
+
+ public void addDate(Date d) {
+ DateCellContainer dcc = new DateCellContainer();
+ dcc.setDate(d);
+ dcc.setCalendar(calendar);
+ add(dcc);
+ }
+
+ public void setWidthPX(int width) {
+ if (getWidgetCount() == 0) {
+ return;
+ }
+ undefinedWidth = (width < 0);
+
+ updateCellWidths();
+ }
+
+ public void addEvents(List<CalendarEvent> events) {
+ for (CalendarEvent e : events) {
+ addEvent(e);
+ }
+ }
+
+ public void addEvent(CalendarEvent calendarEvent) {
+ updateEventSlot(calendarEvent);
+
+ int dateCount = getWidgetCount();
+ Date from = calendarEvent.getStart();
+ Date to = calendarEvent.getEnd();
+ boolean started = false;
+ for (int i = 0; i < dateCount; i++) {
+ DateCellContainer dc = (DateCellContainer) getWidget(i);
+ Date dcDate = dc.getDate();
+ int comp = dcDate.compareTo(from);
+ int comp2 = dcDate.compareTo(to);
+ WeeklyLongEventsDateCell eventLabel = dc
+ .getDateCell(calendarEvent.getSlotIndex());
+ eventLabel.setStylePrimaryName("v-calendar-event");
+ if (comp >= 0 && comp2 <= 0) {
+ eventLabel.setEvent(calendarEvent);
+ eventLabel.setCalendar(calendar);
+
+ eventLabel.addStyleDependentName("all-day");
+ if (comp == 0) {
+ eventLabel.addStyleDependentName("start");
+ }
+ if (comp2 == 0) {
+ eventLabel.addStyleDependentName("end");
+ }
+ if (!started && comp > 0 && comp2 <= 0) {
+ eventLabel.addStyleDependentName("continued-from");
+ } else if (i == (dateCount - 1)) {
+ eventLabel.addStyleDependentName("continued-to");
+ }
+ final String extraStyle = calendarEvent.getStyleName();
+ if (extraStyle != null && extraStyle.length() > 0) {
+ eventLabel.addStyleDependentName(extraStyle + "-all-day");
+ }
+ if (!started) {
+ if (calendar.isEventCaptionAsHtml()) {
+ eventLabel.setHTML(calendarEvent.getCaption());
+ } else {
+ eventLabel.setText(calendarEvent.getCaption());
+ }
+ started = true;
+ }
+ }
+ }
+ }
+
+ private void updateEventSlot(CalendarEvent e) {
+ boolean foundFreeSlot = false;
+ int slot = 0;
+ while (!foundFreeSlot) {
+ if (isSlotFree(slot, e.getStart(), e.getEnd())) {
+ e.setSlotIndex(slot);
+ foundFreeSlot = true;
+
+ } else {
+ slot++;
+ }
+ }
+ }
+
+ private boolean isSlotFree(int slot, Date start, Date end) {
+ int dateCount = getWidgetCount();
+
+ // Go over all dates this week
+ for (int i = 0; i < dateCount; i++) {
+ DateCellContainer dc = (DateCellContainer) getWidget(i);
+ Date dcDate = dc.getDate();
+ int comp = dcDate.compareTo(start);
+ int comp2 = dcDate.compareTo(end);
+
+ // check if the date is in the range we need
+ if (comp >= 0 && comp2 <= 0) {
+
+ // check if the slot is taken
+ if (dc.hasEvent(slot)) {
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+
+ public int getRowCount() {
+ return rowCount;
+ }
+
+ public void updateCellWidths() {
+ int cells = getWidgetCount();
+ if (cells <= 0) {
+ return;
+ }
+
+ int cellWidth = -1;
+
+ // if width is undefined, use the width of the first cell
+ // otherwise use distributed sizes
+ if (undefinedWidth) {
+ cellWidth = calendar.getWeekGrid().getDateCellWidth()
+ - calendar.getWeekGrid().getDateSlotBorder();
+ }
+
+ for (int i = 0; i < cells; i++) {
+ DateCellContainer dc = (DateCellContainer) getWidget(i);
+
+ if (undefinedWidth) {
+ dc.setWidth(cellWidth + "px");
+
+ } else {
+ dc.setWidth(
+ calendar.getWeekGrid().getDateCellWidths()[i] + "px");
+ }
+ }
+ }
+
+ @Override
+ public String getTooltipKey() {
+ return null;
+ }
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/WeeklyLongEventsDateCell.java b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/WeeklyLongEventsDateCell.java
new file mode 100644
index 0000000000..a098ab9c1a
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/WeeklyLongEventsDateCell.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2000-2016 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.client.ui.calendar.schedule;
+
+import java.util.Date;
+
+import com.google.gwt.user.client.ui.HTML;
+import com.vaadin.client.ui.VCalendar;
+
+/**
+ * Represents a cell used in {@link WeeklyLongEvents}
+ *
+ * @since 7.1
+ */
+public class WeeklyLongEventsDateCell extends HTML implements HasTooltipKey {
+ private Date date;
+ private CalendarEvent calendarEvent;
+ private VCalendar calendar;
+
+ public WeeklyLongEventsDateCell() {
+ }
+
+ public void setDate(Date date) {
+ this.date = date;
+ }
+
+ public Date getDate() {
+ return date;
+ }
+
+ public void setEvent(CalendarEvent event) {
+ calendarEvent = event;
+ }
+
+ public CalendarEvent getEvent() {
+ return calendarEvent;
+ }
+
+ public void setCalendar(VCalendar calendar) {
+ this.calendar = calendar;
+ }
+
+ public VCalendar getCalendar() {
+ return calendar;
+ }
+
+ @Override
+ public Object getTooltipKey() {
+ if (calendarEvent != null) {
+ return calendarEvent.getIndex();
+ }
+ return null;
+ }
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/dd/CalendarDropHandler.java b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/dd/CalendarDropHandler.java
new file mode 100644
index 0000000000..58757b8552
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/dd/CalendarDropHandler.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2000-2016 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.client.ui.calendar.schedule.dd;
+
+import com.vaadin.client.ApplicationConnection;
+import com.vaadin.client.ui.calendar.CalendarConnector;
+import com.vaadin.client.ui.dd.VAbstractDropHandler;
+
+/**
+ * Abstract base class for calendar drop handlers.
+ *
+ * @since 7.1
+ * @author Vaadin Ltd.
+ *
+ */
+public abstract class CalendarDropHandler extends VAbstractDropHandler {
+
+ protected final CalendarConnector calendarConnector;
+
+ /**
+ * Constructor
+ *
+ * @param connector
+ * The connector of the calendar
+ */
+ public CalendarDropHandler(CalendarConnector connector) {
+ calendarConnector = connector;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.terminal.gwt.client.ui.dd.VAbstractDropHandler#getConnector()
+ */
+ @Override
+ public CalendarConnector getConnector() {
+ return calendarConnector;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.terminal.gwt.client.ui.dd.VDropHandler#
+ * getApplicationConnection ()
+ */
+ @Override
+ public ApplicationConnection getApplicationConnection() {
+ return calendarConnector.getClient();
+ }
+
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/dd/CalendarMonthDropHandler.java b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/dd/CalendarMonthDropHandler.java
new file mode 100644
index 0000000000..663ee1eb98
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/dd/CalendarMonthDropHandler.java
@@ -0,0 +1,171 @@
+/*
+ * Copyright 2000-2016 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.client.ui.calendar.schedule.dd;
+
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.user.client.DOM;
+import com.vaadin.client.WidgetUtil;
+import com.vaadin.client.ui.calendar.CalendarConnector;
+import com.vaadin.client.ui.calendar.schedule.SimpleDayCell;
+import com.vaadin.client.ui.dd.VAcceptCallback;
+import com.vaadin.client.ui.dd.VDragEvent;
+
+/**
+ * Handles DD when the monthly view is showing in the Calendar. In the monthly
+ * view, drops are only allowed in the the day cells. Only the day index is
+ * included in the drop details sent to the server.
+ *
+ * @since 7.1
+ * @author Vaadin Ltd.
+ */
+public class CalendarMonthDropHandler extends CalendarDropHandler {
+
+ public CalendarMonthDropHandler(CalendarConnector connector) {
+ super(connector);
+ }
+
+ private Element currentTargetElement;
+ private SimpleDayCell currentTargetDay;
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.terminal.gwt.client.ui.dd.VAbstractDropHandler#dragAccepted
+ * (com.vaadin.terminal.gwt.client.ui.dd.VDragEvent)
+ */
+ @Override
+ protected void dragAccepted(VDragEvent drag) {
+ deEmphasis();
+ currentTargetElement = drag.getElementOver();
+ currentTargetDay = WidgetUtil.findWidget(currentTargetElement,
+ SimpleDayCell.class);
+ emphasis();
+ }
+
+ /**
+ * Removed the emphasis CSS style name from the currently emphasized day
+ */
+ private void deEmphasis() {
+ if (currentTargetElement != null && currentTargetDay != null) {
+ currentTargetDay.removeEmphasisStyle();
+ currentTargetElement = null;
+ }
+ }
+
+ /**
+ * Add CSS style name for the currently emphasized day
+ */
+ private void emphasis() {
+ if (currentTargetElement != null && currentTargetDay != null) {
+ currentTargetDay.addEmphasisStyle();
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.terminal.gwt.client.ui.dd.VAbstractDropHandler#dragOver(com
+ * .vaadin.terminal.gwt.client.ui.dd.VDragEvent)
+ */
+ @Override
+ public void dragOver(final VDragEvent drag) {
+ if (isLocationValid(drag.getElementOver())) {
+ validate(new VAcceptCallback() {
+ @Override
+ public void accepted(VDragEvent event) {
+ dragAccepted(drag);
+ }
+ }, drag);
+ }
+ }
+
+ /**
+ * Checks if the one can perform a drop in a element
+ *
+ * @param elementOver
+ * The element to check
+ * @return
+ */
+ private boolean isLocationValid(Element elementOver) {
+ Element monthGridElement = calendarConnector.getWidget().getMonthGrid()
+ .getElement();
+
+ // drops are not allowed in:
+ // - weekday header
+ // - week number bart
+ return DOM.isOrHasChild(monthGridElement, elementOver);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.terminal.gwt.client.ui.dd.VAbstractDropHandler#dragEnter(com
+ * .vaadin.terminal.gwt.client.ui.dd.VDragEvent)
+ */
+ @Override
+ public void dragEnter(VDragEvent drag) {
+ // NOOP, we determine drag acceptance in dragOver
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.terminal.gwt.client.ui.dd.VAbstractDropHandler#drop(com.vaadin
+ * .terminal.gwt.client.ui.dd.VDragEvent)
+ */
+ @Override
+ public boolean drop(VDragEvent drag) {
+ if (isLocationValid(drag.getElementOver())) {
+ updateDropDetails(drag);
+ deEmphasis();
+ return super.drop(drag);
+
+ } else {
+ deEmphasis();
+ return false;
+ }
+ }
+
+ /**
+ * Updates the drop details sent to the server
+ *
+ * @param drag
+ * The drag event
+ */
+ private void updateDropDetails(VDragEvent drag) {
+ int dayIndex = calendarConnector.getWidget().getMonthGrid()
+ .getDayCellIndex(currentTargetDay);
+
+ drag.getDropDetails().put("dropDayIndex", dayIndex);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.terminal.gwt.client.ui.dd.VAbstractDropHandler#dragLeave(com
+ * .vaadin.terminal.gwt.client.ui.dd.VDragEvent)
+ */
+ @Override
+ public void dragLeave(VDragEvent drag) {
+ deEmphasis();
+ super.dragLeave(drag);
+ }
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/dd/CalendarWeekDropHandler.java b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/dd/CalendarWeekDropHandler.java
new file mode 100644
index 0000000000..c0ad635ef7
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/client/ui/calendar/schedule/dd/CalendarWeekDropHandler.java
@@ -0,0 +1,187 @@
+/*
+ * Copyright 2000-2016 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.client.ui.calendar.schedule.dd;
+
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.user.client.DOM;
+import com.vaadin.client.WidgetUtil;
+import com.vaadin.client.ui.calendar.CalendarConnector;
+import com.vaadin.client.ui.calendar.schedule.DateCell;
+import com.vaadin.client.ui.calendar.schedule.DateCellDayEvent;
+import com.vaadin.client.ui.dd.VAcceptCallback;
+import com.vaadin.client.ui.dd.VDragEvent;
+
+/**
+ * Handles DD when the weekly view is showing in the Calendar. In the weekly
+ * view, drops are only allowed in the the time slots for each day. The slot
+ * index and the day index are included in the drop details sent to the server.
+ *
+ * @since 7.1
+ * @author Vaadin Ltd.
+ */
+public class CalendarWeekDropHandler extends CalendarDropHandler {
+
+ private Element currentTargetElement;
+ private DateCell currentTargetDay;
+
+ public CalendarWeekDropHandler(CalendarConnector connector) {
+ super(connector);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.terminal.gwt.client.ui.dd.VAbstractDropHandler#dragAccepted
+ * (com.vaadin.terminal.gwt.client.ui.dd.VDragEvent)
+ */
+ @Override
+ protected void dragAccepted(VDragEvent drag) {
+ deEmphasis();
+ currentTargetElement = drag.getElementOver();
+ currentTargetDay = WidgetUtil.findWidget(currentTargetElement,
+ DateCell.class);
+ emphasis();
+ }
+
+ /**
+ * Removes the CSS style name from the emphasized element
+ */
+ private void deEmphasis() {
+ if (currentTargetElement != null) {
+ currentTargetDay.removeEmphasisStyle(currentTargetElement);
+ currentTargetElement = null;
+ }
+ }
+
+ /**
+ * Add a CSS stylen name to current target element
+ */
+ private void emphasis() {
+ currentTargetDay.addEmphasisStyle(currentTargetElement);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.terminal.gwt.client.ui.dd.VAbstractDropHandler#dragOver(com
+ * .vaadin.terminal.gwt.client.ui.dd.VDragEvent)
+ */
+ @Override
+ public void dragOver(final VDragEvent drag) {
+ if (isLocationValid(drag.getElementOver())) {
+ validate(new VAcceptCallback() {
+ @Override
+ public void accepted(VDragEvent event) {
+ dragAccepted(drag);
+ }
+ }, drag);
+ }
+ }
+
+ /**
+ * Checks if the location is a valid drop location
+ *
+ * @param elementOver
+ * The element to check
+ * @return
+ */
+ private boolean isLocationValid(Element elementOver) {
+ Element weekGridElement = calendarConnector.getWidget().getWeekGrid()
+ .getElement();
+ Element timeBarElement = calendarConnector.getWidget().getWeekGrid()
+ .getTimeBar().getElement();
+
+ Element todayBarElement = null;
+ if (calendarConnector.getWidget().getWeekGrid().hasToday()) {
+ todayBarElement = calendarConnector.getWidget().getWeekGrid()
+ .getDateCellOfToday().getTodaybarElement();
+ }
+
+ // drops are not allowed in:
+ // - weekday header
+ // - allday event list
+ // - todaybar
+ // - timebar
+ // - events
+ return DOM.isOrHasChild(weekGridElement, elementOver)
+ && !DOM.isOrHasChild(timeBarElement, elementOver)
+ && todayBarElement != elementOver
+ && (WidgetUtil.findWidget(elementOver,
+ DateCellDayEvent.class) == null);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.terminal.gwt.client.ui.dd.VAbstractDropHandler#dragEnter(com
+ * .vaadin.terminal.gwt.client.ui.dd.VDragEvent)
+ */
+ @Override
+ public void dragEnter(VDragEvent drag) {
+ // NOOP, we determine drag acceptance in dragOver
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.terminal.gwt.client.ui.dd.VAbstractDropHandler#drop(com.vaadin
+ * .terminal.gwt.client.ui.dd.VDragEvent)
+ */
+ @Override
+ public boolean drop(VDragEvent drag) {
+ if (isLocationValid(drag.getElementOver())) {
+ updateDropDetails(drag);
+ deEmphasis();
+ return super.drop(drag);
+
+ } else {
+ deEmphasis();
+ return false;
+ }
+ }
+
+ /**
+ * Update the drop details sent to the server
+ *
+ * @param drag
+ * The drag event
+ */
+ private void updateDropDetails(VDragEvent drag) {
+ int slotIndex = currentTargetDay.getSlotIndex(currentTargetElement);
+ int dayIndex = calendarConnector.getWidget().getWeekGrid()
+ .getDateCellIndex(currentTargetDay);
+
+ drag.getDropDetails().put("dropDayIndex", dayIndex);
+ drag.getDropDetails().put("dropSlotIndex", slotIndex);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.terminal.gwt.client.ui.dd.VAbstractDropHandler#dragLeave(com
+ * .vaadin.terminal.gwt.client.ui.dd.VDragEvent)
+ */
+ @Override
+ public void dragLeave(VDragEvent drag) {
+ deEmphasis();
+ super.dragLeave(drag);
+ }
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/client/ui/colorpicker/AbstractColorPickerConnector.java b/compatibility-client/src/main/java/com/vaadin/client/ui/colorpicker/AbstractColorPickerConnector.java
new file mode 100644
index 0000000000..6f480538e0
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/client/ui/colorpicker/AbstractColorPickerConnector.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright 2000-2016 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.client.ui.colorpicker;
+
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.event.dom.client.HasClickHandlers;
+import com.vaadin.client.communication.StateChangeEvent;
+import com.vaadin.client.ui.AbstractComponentConnector;
+import com.vaadin.shared.ui.colorpicker.ColorPickerState;
+
+/**
+ * An abstract class that defines default implementation for a color picker
+ * connector.
+ *
+ * @since 7.0.0
+ */
+public abstract class AbstractColorPickerConnector
+ extends AbstractComponentConnector implements ClickHandler {
+
+ private static final String DEFAULT_WIDTH_STYLE = "v-default-caption-width";
+
+ @Override
+ public ColorPickerState getState() {
+ return (ColorPickerState) super.getState();
+ }
+
+ @Override
+ public boolean delegateCaptionHandling() {
+ return false;
+ }
+
+ @Override
+ public void onStateChanged(StateChangeEvent stateChangeEvent) {
+ // NOTE: this method is called after @DelegateToWidget
+ super.onStateChanged(stateChangeEvent);
+ if (stateChangeEvent.hasPropertyChanged("color")) {
+ refreshColor();
+
+ if (getState().showDefaultCaption && (getState().caption == null
+ || "".equals(getState().caption))) {
+
+ setCaption(getState().color);
+ }
+ }
+ if (stateChangeEvent.hasPropertyChanged("caption")
+ || stateChangeEvent.hasPropertyChanged("htmlContentAllowed")
+ || stateChangeEvent.hasPropertyChanged("showDefaultCaption")) {
+
+ setCaption(getCaption());
+ refreshDefaultCaptionStyle();
+ }
+ }
+
+ @Override
+ public void init() {
+ super.init();
+ if (getWidget() instanceof HasClickHandlers) {
+ ((HasClickHandlers) getWidget()).addClickHandler(this);
+ }
+ }
+
+ /**
+ * Get caption for the color picker widget.
+ *
+ * @return
+ */
+ protected String getCaption() {
+ if (getState().showDefaultCaption && (getState().caption == null
+ || "".equals(getState().caption))) {
+ return getState().color;
+ }
+ return getState().caption;
+ }
+
+ /**
+ * Add/remove default caption style.
+ */
+ protected void refreshDefaultCaptionStyle() {
+ if (getState().showDefaultCaption
+ && (getState().caption == null || getState().caption.isEmpty())
+ && getState().width.isEmpty()) {
+ getWidget().addStyleName(DEFAULT_WIDTH_STYLE);
+ } else {
+ getWidget().removeStyleName(DEFAULT_WIDTH_STYLE);
+ }
+ }
+
+ /**
+ * Set caption of the color picker widget.
+ *
+ * @param caption
+ */
+ protected abstract void setCaption(String caption);
+
+ /**
+ * Update the widget to show the currently selected color.
+ */
+ protected abstract void refreshColor();
+
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/client/ui/colorpicker/ColorPickerAreaConnector.java b/compatibility-client/src/main/java/com/vaadin/client/ui/colorpicker/ColorPickerAreaConnector.java
new file mode 100644
index 0000000000..828cc689c7
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/client/ui/colorpicker/ColorPickerAreaConnector.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2000-2016 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.client.ui.colorpicker;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.client.VCaption;
+import com.vaadin.client.communication.RpcProxy;
+import com.vaadin.client.ui.VColorPickerArea;
+import com.vaadin.shared.ui.Connect;
+import com.vaadin.shared.ui.Connect.LoadStyle;
+import com.vaadin.shared.ui.colorpicker.ColorPickerServerRpc;
+import com.vaadin.ui.ColorPickerArea;
+
+/**
+ * A class that defines an implementation for a color picker connector. Connects
+ * the server side {@link com.vaadin.ui.ColorPickerArea} with the client side
+ * counterpart {@link VColorPickerArea}
+ *
+ * @since 7.0.0
+ */
+@Connect(value = ColorPickerArea.class, loadStyle = LoadStyle.LAZY)
+public class ColorPickerAreaConnector extends AbstractColorPickerConnector {
+
+ private ColorPickerServerRpc rpc = RpcProxy
+ .create(ColorPickerServerRpc.class, this);
+
+ @Override
+ protected Widget createWidget() {
+ return GWT.create(VColorPickerArea.class);
+ }
+
+ @Override
+ public VColorPickerArea getWidget() {
+ return (VColorPickerArea) super.getWidget();
+ }
+
+ @Override
+ public void onClick(ClickEvent event) {
+ rpc.openPopup(getWidget().isOpen());
+ }
+
+ @Override
+ protected void setCaption(String caption) {
+ VCaption.setCaptionText(getWidget(), getState());
+ }
+
+ @Override
+ protected void refreshColor() {
+ getWidget().refreshColor();
+ }
+
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/client/ui/colorpicker/ColorPickerConnector.java b/compatibility-client/src/main/java/com/vaadin/client/ui/colorpicker/ColorPickerConnector.java
new file mode 100644
index 0000000000..6254e7adbe
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/client/ui/colorpicker/ColorPickerConnector.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2000-2016 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.client.ui.colorpicker;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.client.communication.RpcProxy;
+import com.vaadin.client.ui.VColorPicker;
+import com.vaadin.shared.ui.Connect;
+import com.vaadin.shared.ui.Connect.LoadStyle;
+import com.vaadin.shared.ui.colorpicker.ColorPickerServerRpc;
+import com.vaadin.ui.ColorPicker;
+
+/**
+ * A class that defines default implementation for a color picker connector.
+ * Connects the server side {@link com.vaadin.ui.ColorPicker} with the client
+ * side counterpart {@link VColorPicker}
+ *
+ * @since 7.0.0
+ */
+@Connect(value = ColorPicker.class, loadStyle = LoadStyle.LAZY)
+public class ColorPickerConnector extends AbstractColorPickerConnector {
+
+ private ColorPickerServerRpc rpc = RpcProxy
+ .create(ColorPickerServerRpc.class, this);
+
+ @Override
+ protected Widget createWidget() {
+ return GWT.create(VColorPicker.class);
+ }
+
+ @Override
+ public VColorPicker getWidget() {
+ return (VColorPicker) super.getWidget();
+ }
+
+ @Override
+ public void onClick(ClickEvent event) {
+ rpc.openPopup(getWidget().isOpen());
+ }
+
+ @Override
+ protected void setCaption(String caption) {
+ if (getState().captionAsHtml) {
+ getWidget().setHtml(caption);
+ } else {
+ getWidget().setText(caption);
+ }
+ }
+
+ @Override
+ protected void refreshColor() {
+ getWidget().refreshColor();
+ }
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/client/ui/colorpicker/ColorPickerGradientConnector.java b/compatibility-client/src/main/java/com/vaadin/client/ui/colorpicker/ColorPickerGradientConnector.java
new file mode 100644
index 0000000000..c05449d7bd
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/client/ui/colorpicker/ColorPickerGradientConnector.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2000-2016 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.client.ui.colorpicker;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.event.dom.client.MouseUpEvent;
+import com.google.gwt.event.dom.client.MouseUpHandler;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.client.communication.RpcProxy;
+import com.vaadin.client.communication.StateChangeEvent;
+import com.vaadin.client.ui.AbstractComponentConnector;
+import com.vaadin.shared.ui.Connect;
+import com.vaadin.shared.ui.Connect.LoadStyle;
+import com.vaadin.shared.ui.colorpicker.ColorPickerGradientServerRpc;
+import com.vaadin.shared.ui.colorpicker.ColorPickerGradientState;
+import com.vaadin.ui.components.colorpicker.ColorPickerGradient;
+
+/**
+ * A class that defines the default implementation for a color picker gradient
+ * connector. Connects the server side
+ * {@link com.vaadin.ui.components.colorpicker.ColorPickerGradient} with the
+ * client side counterpart {@link VColorPickerGradient}
+ *
+ * @since 7.0.0
+ */
+@Connect(value = ColorPickerGradient.class, loadStyle = LoadStyle.LAZY)
+public class ColorPickerGradientConnector extends AbstractComponentConnector
+ implements MouseUpHandler {
+
+ private ColorPickerGradientServerRpc rpc = RpcProxy
+ .create(ColorPickerGradientServerRpc.class, this);
+
+ @Override
+ protected Widget createWidget() {
+ return GWT.create(VColorPickerGradient.class);
+ }
+
+ @Override
+ public VColorPickerGradient getWidget() {
+ return (VColorPickerGradient) super.getWidget();
+ }
+
+ @Override
+ public ColorPickerGradientState getState() {
+ return (ColorPickerGradientState) super.getState();
+ }
+
+ @Override
+ public void onMouseUp(MouseUpEvent event) {
+ rpc.select(getWidget().getCursorX(), getWidget().getCursorY());
+ }
+
+ @Override
+ public void onStateChanged(StateChangeEvent stateChangeEvent) {
+ super.onStateChanged(stateChangeEvent);
+ if (stateChangeEvent.hasPropertyChanged("cursorX")
+ || stateChangeEvent.hasPropertyChanged("cursorY")) {
+
+ getWidget().setCursor(getState().cursorX, getState().cursorY);
+ }
+ if (stateChangeEvent.hasPropertyChanged("bgColor")) {
+ getWidget().setBGColor(getState().bgColor);
+ }
+ }
+
+ @Override
+ protected void init() {
+ super.init();
+ getWidget().addMouseUpHandler(this);
+ }
+
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/client/ui/colorpicker/ColorPickerGridConnector.java b/compatibility-client/src/main/java/com/vaadin/client/ui/colorpicker/ColorPickerGridConnector.java
new file mode 100644
index 0000000000..cd0a3d1466
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/client/ui/colorpicker/ColorPickerGridConnector.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2000-2016 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.client.ui.colorpicker;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.client.communication.RpcProxy;
+import com.vaadin.client.communication.StateChangeEvent;
+import com.vaadin.client.ui.AbstractComponentConnector;
+import com.vaadin.shared.ui.Connect;
+import com.vaadin.shared.ui.Connect.LoadStyle;
+import com.vaadin.shared.ui.colorpicker.ColorPickerGridServerRpc;
+import com.vaadin.shared.ui.colorpicker.ColorPickerGridState;
+import com.vaadin.ui.components.colorpicker.ColorPickerGrid;
+
+/**
+ * A class that defines the default implementation for a color picker grid
+ * connector. Connects the server side
+ * {@link com.vaadin.ui.components.colorpicker.ColorPickerGrid} with the client
+ * side counterpart {@link VColorPickerGrid}
+ *
+ * @since 7.0.0
+ */
+@Connect(value = ColorPickerGrid.class, loadStyle = LoadStyle.LAZY)
+public class ColorPickerGridConnector extends AbstractComponentConnector
+ implements ClickHandler {
+
+ private ColorPickerGridServerRpc rpc = RpcProxy
+ .create(ColorPickerGridServerRpc.class, this);
+
+ @Override
+ protected Widget createWidget() {
+ return GWT.create(VColorPickerGrid.class);
+ }
+
+ @Override
+ public VColorPickerGrid getWidget() {
+ return (VColorPickerGrid) super.getWidget();
+ }
+
+ @Override
+ public ColorPickerGridState getState() {
+ return (ColorPickerGridState) super.getState();
+ }
+
+ @Override
+ public void onClick(ClickEvent event) {
+ rpc.select(getWidget().getSelectedX(), getWidget().getSelectedY());
+ }
+
+ @Override
+ public void onStateChanged(StateChangeEvent stateChangeEvent) {
+ super.onStateChanged(stateChangeEvent);
+ if (stateChangeEvent.hasPropertyChanged("rowCount")
+ || stateChangeEvent.hasPropertyChanged("columnCount")
+ || stateChangeEvent.hasPropertyChanged("updateGrid")) {
+
+ getWidget().updateGrid(getState().rowCount, getState().columnCount);
+ }
+ if (stateChangeEvent.hasPropertyChanged("changedX")
+ || stateChangeEvent.hasPropertyChanged("changedY")
+ || stateChangeEvent.hasPropertyChanged("changedColor")
+ || stateChangeEvent.hasPropertyChanged("updateColor")) {
+
+ getWidget().updateColor(getState().changedColor,
+ getState().changedX, getState().changedY);
+
+ if (!getWidget().isGridLoaded()) {
+ rpc.refresh();
+ }
+ }
+ }
+
+ @Override
+ protected void init() {
+ super.init();
+ getWidget().addClickHandler(this);
+ }
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/client/ui/colorpicker/VColorPickerGradient.java b/compatibility-client/src/main/java/com/vaadin/client/ui/colorpicker/VColorPickerGradient.java
new file mode 100644
index 0000000000..b75bee23c1
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/client/ui/colorpicker/VColorPickerGradient.java
@@ -0,0 +1,211 @@
+/*
+ * Copyright 2000-2016 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.client.ui.colorpicker;
+
+import com.google.gwt.dom.client.Style.Unit;
+import com.google.gwt.event.dom.client.MouseDownEvent;
+import com.google.gwt.event.dom.client.MouseDownHandler;
+import com.google.gwt.event.dom.client.MouseMoveEvent;
+import com.google.gwt.event.dom.client.MouseMoveHandler;
+import com.google.gwt.event.dom.client.MouseUpEvent;
+import com.google.gwt.event.dom.client.MouseUpHandler;
+import com.google.gwt.user.client.ui.AbsolutePanel;
+import com.google.gwt.user.client.ui.FocusPanel;
+import com.google.gwt.user.client.ui.HTML;
+import com.vaadin.client.ui.SubPartAware;
+
+/**
+ * Client side implementation for ColorPickerGradient.
+ *
+ * @since 7.0.0
+ *
+ */
+public class VColorPickerGradient extends FocusPanel implements
+ MouseDownHandler, MouseUpHandler, MouseMoveHandler, SubPartAware {
+
+ /** Set the CSS class name to allow styling. */
+ public static final String CLASSNAME = "v-colorpicker-gradient";
+ public static final String CLASSNAME_BACKGROUND = CLASSNAME + "-background";
+ public static final String CLASSNAME_FOREGROUND = CLASSNAME + "-foreground";
+ public static final String CLASSNAME_LOWERBOX = CLASSNAME + "-lowerbox";
+ public static final String CLASSNAME_HIGHERBOX = CLASSNAME + "-higherbox";
+ public static final String CLASSNAME_CONTAINER = CLASSNAME + "-container";
+ public static final String CLASSNAME_CLICKLAYER = CLASSNAME + "-clicklayer";
+ private static final String CLICKLAYER_ID = "clicklayer";
+
+ private final HTML background;
+ private final HTML foreground;
+ private final HTML lowercross;
+ private final HTML highercross;
+ private final HTML clicklayer;
+ private final AbsolutePanel container;
+
+ private boolean mouseIsDown = false;
+
+ private int cursorX;
+ private int cursorY;
+
+ private int width = 220;
+ private int height = 220;
+
+ /**
+ * Instantiates the client side component for a color picker gradient.
+ */
+ public VColorPickerGradient() {
+ super();
+
+ setStyleName(CLASSNAME);
+
+ background = new HTML();
+ background.setStyleName(CLASSNAME_BACKGROUND);
+ background.setPixelSize(width, height);
+
+ foreground = new HTML();
+ foreground.setStyleName(CLASSNAME_FOREGROUND);
+ foreground.setPixelSize(width, height);
+
+ clicklayer = new HTML();
+ clicklayer.setStyleName(CLASSNAME_CLICKLAYER);
+ clicklayer.setPixelSize(width, height);
+ clicklayer.addMouseDownHandler(this);
+ clicklayer.addMouseUpHandler(this);
+ clicklayer.addMouseMoveHandler(this);
+
+ lowercross = new HTML();
+ lowercross.setPixelSize(width / 2, height / 2);
+ lowercross.setStyleName(CLASSNAME_LOWERBOX);
+
+ highercross = new HTML();
+ highercross.setPixelSize(width / 2, height / 2);
+ highercross.setStyleName(CLASSNAME_HIGHERBOX);
+
+ container = new AbsolutePanel();
+ container.setStyleName(CLASSNAME_CONTAINER);
+ container.setPixelSize(width, height);
+ container.add(background, 0, 0);
+ container.add(foreground, 0, 0);
+ container.add(lowercross, 0, height / 2);
+ container.add(highercross, width / 2, 0);
+ container.add(clicklayer, 0, 0);
+
+ add(container);
+ }
+
+ /**
+ * Returns the latest x-coordinate for pressed-down mouse cursor.
+ */
+ protected int getCursorX() {
+ return cursorX;
+ }
+
+ /**
+ * Returns the latest y-coordinate for pressed-down mouse cursor.
+ */
+ protected int getCursorY() {
+ return cursorY;
+ }
+
+ /**
+ * Sets the given css color as the background.
+ *
+ * @param bgColor
+ */
+ protected void setBGColor(String bgColor) {
+ if (bgColor == null) {
+ background.getElement().getStyle().clearBackgroundColor();
+ } else {
+ background.getElement().getStyle().setBackgroundColor(bgColor);
+ }
+ }
+
+ @Override
+ public void onMouseDown(MouseDownEvent event) {
+ event.preventDefault();
+
+ mouseIsDown = true;
+ setCursor(event.getX(), event.getY());
+ }
+
+ @Override
+ public void onMouseUp(MouseUpEvent event) {
+ event.preventDefault();
+ mouseIsDown = false;
+ setCursor(event.getX(), event.getY());
+
+ cursorX = event.getX();
+ cursorY = event.getY();
+ }
+
+ @Override
+ public void onMouseMove(MouseMoveEvent event) {
+ event.preventDefault();
+
+ if (mouseIsDown) {
+ setCursor(event.getX(), event.getY());
+ }
+ }
+
+ /**
+ * Sets the latest coordinates for pressed-down mouse cursor and updates the
+ * cross elements.
+ *
+ * @param x
+ * @param y
+ */
+ public void setCursor(int x, int y) {
+ cursorX = x;
+ cursorY = y;
+ if (x >= 0) {
+ lowercross.getElement().getStyle().setWidth(x, Unit.PX);
+ }
+ if (y >= 0) {
+ lowercross.getElement().getStyle().setTop(y, Unit.PX);
+ }
+ if (y >= 0) {
+ lowercross.getElement().getStyle().setHeight(height - y, Unit.PX);
+ }
+
+ if (x >= 0) {
+ highercross.getElement().getStyle().setWidth(width - x, Unit.PX);
+ }
+ if (x >= 0) {
+ highercross.getElement().getStyle().setLeft(x, Unit.PX);
+ }
+ if (y >= 0) {
+ highercross.getElement().getStyle().setHeight(y, Unit.PX);
+ }
+ }
+
+ @Override
+ public com.google.gwt.user.client.Element getSubPartElement(
+ String subPart) {
+ if (subPart.equals(CLICKLAYER_ID)) {
+ return clicklayer.getElement();
+ }
+
+ return null;
+ }
+
+ @Override
+ public String getSubPartName(
+ com.google.gwt.user.client.Element subElement) {
+ if (clicklayer.getElement().isOrHasChild(subElement)) {
+ return CLICKLAYER_ID;
+ }
+
+ return null;
+ }
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/client/ui/colorpicker/VColorPickerGrid.java b/compatibility-client/src/main/java/com/vaadin/client/ui/colorpicker/VColorPickerGrid.java
new file mode 100644
index 0000000000..67f2ce07dd
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/client/ui/colorpicker/VColorPickerGrid.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright 2000-2016 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.client.ui.colorpicker;
+
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.event.dom.client.HasClickHandlers;
+import com.google.gwt.event.shared.HandlerRegistration;
+import com.google.gwt.user.client.ui.AbsolutePanel;
+import com.google.gwt.user.client.ui.Grid;
+import com.google.gwt.user.client.ui.HTMLTable.Cell;
+
+/**
+ * Client side implementation for ColorPickerGrid.
+ *
+ * @since 7.0.0
+ *
+ */
+public class VColorPickerGrid extends AbsolutePanel
+ implements ClickHandler, HasClickHandlers {
+
+ private int rows = 1;
+ private int columns = 1;
+
+ private Grid grid;
+
+ private boolean gridLoaded = false;
+
+ private int selectedX;
+ private int selectedY;
+
+ /**
+ * Instantiates the client side component for a color picker grid.
+ */
+ public VColorPickerGrid() {
+ super();
+
+ this.add(createGrid(), 0, 0);
+ }
+
+ /**
+ * Creates a grid according to the current row and column count information.
+ *
+ * @return grid
+ */
+ private Grid createGrid() {
+ grid = new Grid(rows, columns);
+ grid.setWidth("100%");
+ grid.setHeight("100%");
+ grid.addClickHandler(this);
+ return grid;
+ }
+
+ /**
+ * Updates the row and column count and creates a new grid based on them.
+ * The new grid replaces the old grid if one existed.
+ *
+ * @param rowCount
+ * @param columnCount
+ */
+ protected void updateGrid(int rowCount, int columnCount) {
+ rows = rowCount;
+ columns = columnCount;
+ this.remove(grid);
+ this.add(createGrid(), 0, 0);
+ }
+
+ /**
+ * Updates the changed colors within the grid based on the given x- and
+ * y-coordinates. Nothing happens if any of the parameters is null or the
+ * parameter lengths don't match.
+ *
+ * @param changedColor
+ * @param changedX
+ * @param changedY
+ */
+ protected void updateColor(String[] changedColor, String[] changedX,
+ String[] changedY) {
+ if (changedColor != null && changedX != null && changedY != null) {
+ if (changedColor.length == changedX.length
+ && changedX.length == changedY.length) {
+ for (int c = 0; c < changedColor.length; c++) {
+ Element element = grid.getCellFormatter().getElement(
+ Integer.parseInt(changedX[c]),
+ Integer.parseInt(changedY[c]));
+ element.getStyle().setProperty("background",
+ changedColor[c]);
+ }
+ }
+
+ gridLoaded = true;
+ }
+ }
+
+ /**
+ * Returns currently selected x-coordinate of the grid.
+ */
+ protected int getSelectedX() {
+ return selectedX;
+ }
+
+ /**
+ * Returns currently selected y-coordinate of the grid.
+ */
+ protected int getSelectedY() {
+ return selectedY;
+ }
+
+ /**
+ * Returns true if the colors have been successfully updated at least once,
+ * false otherwise.
+ */
+ protected boolean isGridLoaded() {
+ return gridLoaded;
+ }
+
+ @Override
+ public void onClick(ClickEvent event) {
+ Cell cell = grid.getCellForEvent(event);
+ if (cell == null) {
+ return;
+ }
+
+ selectedY = cell.getRowIndex();
+ selectedX = cell.getCellIndex();
+ }
+
+ @Override
+ public HandlerRegistration addClickHandler(ClickHandler handler) {
+ return addDomHandler(handler, ClickEvent.getType());
+ }
+
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/client/ui/combobox/ComboBoxConnector.java b/compatibility-client/src/main/java/com/vaadin/client/ui/combobox/ComboBoxConnector.java
new file mode 100644
index 0000000000..f6dff893f5
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/client/ui/combobox/ComboBoxConnector.java
@@ -0,0 +1,378 @@
+/*
+ * Copyright 2000-2016 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.client.ui.combobox;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+import com.vaadin.client.ApplicationConnection;
+import com.vaadin.client.Paintable;
+import com.vaadin.client.Profiler;
+import com.vaadin.client.UIDL;
+import com.vaadin.client.communication.RpcProxy;
+import com.vaadin.client.communication.StateChangeEvent;
+import com.vaadin.client.ui.AbstractFieldConnector;
+import com.vaadin.client.ui.SimpleManagedLayout;
+import com.vaadin.client.ui.VFilterSelect;
+import com.vaadin.client.ui.VFilterSelect.DataReceivedHandler;
+import com.vaadin.client.ui.VFilterSelect.FilterSelectSuggestion;
+import com.vaadin.shared.EventId;
+import com.vaadin.shared.communication.FieldRpc.FocusAndBlurServerRpc;
+import com.vaadin.shared.ui.Connect;
+import com.vaadin.shared.ui.combobox.ComboBoxServerRpc;
+import com.vaadin.shared.ui.combobox.ComboBoxState;
+import com.vaadin.ui.ComboBox;
+
+@Connect(ComboBox.class)
+public class ComboBoxConnector extends AbstractFieldConnector
+ implements Paintable, SimpleManagedLayout {
+
+ protected ComboBoxServerRpc rpc = RpcProxy.create(ComboBoxServerRpc.class,
+ this);
+
+ protected FocusAndBlurServerRpc focusAndBlurRpc = RpcProxy
+ .create(FocusAndBlurServerRpc.class, this);
+
+ @Override
+ protected void init() {
+ super.init();
+ getWidget().connector = this;
+ }
+
+ @Override
+ public void onStateChanged(StateChangeEvent stateChangeEvent) {
+ super.onStateChanged(stateChangeEvent);
+
+ Profiler.enter("ComboBoxConnector.onStateChanged update content");
+
+ getWidget().readonly = isReadOnly();
+ getWidget().updateReadOnly();
+
+ getWidget().setTextInputEnabled(getState().textInputAllowed);
+
+ if (getState().inputPrompt != null) {
+ getWidget().inputPrompt = getState().inputPrompt;
+ } else {
+ getWidget().inputPrompt = "";
+ }
+
+ getWidget().pageLength = getState().pageLength;
+
+ getWidget().filteringmode = getState().filteringMode;
+
+ getWidget().suggestionPopupWidth = getState().suggestionPopupWidth;
+
+ Profiler.leave("ComboBoxConnector.onStateChanged update content");
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.client.Paintable#updateFromUIDL(com.vaadin.client.UIDL,
+ * com.vaadin.client.ApplicationConnection)
+ */
+ @Override
+ public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+ if (!isRealUpdate(uidl)) {
+ return;
+ }
+
+ // not a FocusWidget -> needs own tabindex handling
+ getWidget().tb.setTabIndex(getState().tabIndex);
+
+ getWidget().nullSelectionAllowed = uidl.hasAttribute("nullselect");
+
+ getWidget().nullSelectItem = uidl.hasAttribute("nullselectitem")
+ && uidl.getBooleanAttribute("nullselectitem");
+
+ getWidget().currentPage = uidl.getIntVariable("page");
+
+ getWidget().suggestionPopup.updateStyleNames(getState());
+
+ getWidget().allowNewItem = uidl.hasAttribute("allownewitem");
+ getWidget().lastNewItemString = null;
+
+ final UIDL options = uidl.getChildUIDL(0);
+ if (uidl.hasAttribute("totalMatches")) {
+ getWidget().totalMatches = uidl.getIntAttribute("totalMatches");
+ } else {
+ getWidget().totalMatches = 0;
+ }
+
+ List<FilterSelectSuggestion> newSuggestions = new ArrayList<FilterSelectSuggestion>();
+
+ for (final Iterator<?> i = options.getChildIterator(); i.hasNext();) {
+ final UIDL optionUidl = (UIDL) i.next();
+ String key = optionUidl.getStringAttribute("key");
+ String caption = optionUidl.getStringAttribute("caption");
+ String style = optionUidl.getStringAttribute("style");
+
+ String untranslatedIconUri = null;
+ if (optionUidl.hasAttribute("icon")) {
+ untranslatedIconUri = optionUidl.getStringAttribute("icon");
+ }
+
+ final FilterSelectSuggestion suggestion = getWidget().new FilterSelectSuggestion(
+ key, caption, style, untranslatedIconUri);
+ newSuggestions.add(suggestion);
+ }
+
+ // only close the popup if the suggestions list has actually changed
+ boolean suggestionsChanged = !getWidget().initDone
+ || !newSuggestions.equals(getWidget().currentSuggestions);
+
+ // An ItemSetChangeEvent on server side clears the current suggestion
+ // popup. Popup needs to be repopulated with suggestions from UIDL.
+ boolean popupOpenAndCleared = false;
+
+ // oldSuggestionTextMatchTheOldSelection is used to detect when it's
+ // safe to update textbox text by a changed item caption.
+ boolean oldSuggestionTextMatchesTheOldSelection = false;
+
+ if (suggestionsChanged) {
+ oldSuggestionTextMatchesTheOldSelection = isWidgetsCurrentSelectionTextInTextBox();
+ getWidget().currentSuggestions.clear();
+
+ if (!getDataReceivedHandler().isWaitingForFilteringResponse()) {
+ /*
+ * Clear the current suggestions as the server response always
+ * includes the new ones. Exception is when filtering, then we
+ * need to retain the value if the user does not select any of
+ * the options matching the filter.
+ */
+ getWidget().currentSuggestion = null;
+ /*
+ * Also ensure no old items in menu. Unless cleared the old
+ * values may cause odd effects on blur events. Suggestions in
+ * menu might not necessary exist in select at all anymore.
+ */
+ getWidget().suggestionPopup.menu.clearItems();
+ popupOpenAndCleared = getWidget().suggestionPopup.isAttached();
+
+ }
+
+ for (FilterSelectSuggestion suggestion : newSuggestions) {
+ getWidget().currentSuggestions.add(suggestion);
+ }
+ }
+
+ // handle selection (null or a single value)
+ if (uidl.hasVariable("selected")
+
+ // In case we're switching page no need to update the selection as the
+ // selection process didn't finish.
+ // && getWidget().selectPopupItemWhenResponseIsReceived ==
+ // VFilterSelect.Select.NONE
+ //
+ ) {
+
+ // single selected key (can be empty string) or empty array for null
+ // selection
+ String[] selectedKeys = uidl.getStringArrayVariable("selected");
+ String selectedKey = null;
+ if (selectedKeys.length == 1) {
+ selectedKey = selectedKeys[0];
+ }
+ // selected item caption in case it is not on the current page
+ String selectedCaption = null;
+ if (uidl.hasAttribute("selectedCaption")) {
+ selectedCaption = uidl.getStringAttribute("selectedCaption");
+ }
+
+ getDataReceivedHandler().updateSelectionFromServer(selectedKey,
+ selectedCaption, oldSuggestionTextMatchesTheOldSelection);
+ }
+
+ // TODO even this condition should probably be moved to the handler
+ if ((getDataReceivedHandler().isWaitingForFilteringResponse()
+ && getWidget().lastFilter.toLowerCase()
+ .equals(uidl.getStringVariable("filter")))
+ || popupOpenAndCleared) {
+ getDataReceivedHandler().dataReceived();
+ }
+
+ // Calculate minimum textarea width
+ getWidget().updateSuggestionPopupMinWidth();
+
+ /*
+ * if this is our first time we need to recalculate the root width.
+ */
+ if (!getWidget().initDone) {
+
+ getWidget().updateRootWidth();
+ }
+
+ // Focus dependent style names are lost during the update, so we add
+ // them here back again
+ if (getWidget().focused) {
+ getWidget().addStyleDependentName("focus");
+ }
+
+ getWidget().initDone = true;
+
+ // TODO this should perhaps be moved to be a part of dataReceived()
+ getDataReceivedHandler().serverReplyHandled();
+ }
+
+ private boolean isWidgetsCurrentSelectionTextInTextBox() {
+ return getWidget().currentSuggestion != null
+ && getWidget().currentSuggestion.getReplacementString()
+ .equals(getWidget().tb.getText());
+ }
+
+ @Override
+ public VFilterSelect getWidget() {
+ return (VFilterSelect) super.getWidget();
+ }
+
+ private DataReceivedHandler getDataReceivedHandler() {
+ return getWidget().getDataReceivedHandler();
+ }
+
+ @Override
+ public ComboBoxState getState() {
+ return (ComboBoxState) super.getState();
+ }
+
+ @Override
+ public void layout() {
+ VFilterSelect widget = getWidget();
+ if (widget.initDone) {
+ widget.updateRootWidth();
+ }
+ }
+
+ @Override
+ public void setWidgetEnabled(boolean widgetEnabled) {
+ super.setWidgetEnabled(widgetEnabled);
+ getWidget().enabled = widgetEnabled;
+ getWidget().tb.setEnabled(widgetEnabled);
+ }
+
+ /*
+ * These methods exist to move communications out of VFilterSelect, and may
+ * be refactored/removed in the future
+ */
+
+ /**
+ * Send a message about a newly created item to the server.
+ *
+ * This method is for internal use only and may be removed in future
+ * versions.
+ *
+ * @since
+ * @param itemValue
+ * user entered string value for the new item
+ */
+ public void sendNewItem(String itemValue) {
+ rpc.createNewItem(itemValue);
+ afterSendRequestToServer();
+ }
+
+ /**
+ * Send a message to the server to request the first page of items without
+ * filtering or selection.
+ *
+ * This method is for internal use only and may be removed in future
+ * versions.
+ *
+ * @since
+ */
+ public void requestFirstPage() {
+ sendSelection(null);
+ requestPage("", 0);
+ }
+
+ /**
+ * Send a message to the server to request a page of items with a given
+ * filter.
+ *
+ * This method is for internal use only and may be removed in future
+ * versions.
+ *
+ * @since
+ * @param filter
+ * the current filter string
+ * @param page
+ * the page number to get
+ */
+ public void requestPage(String filter, int page) {
+ rpc.requestPage(filter, page);
+ afterSendRequestToServer();
+ }
+
+ /**
+ * Send a message to the server updating the current selection.
+ *
+ * This method is for internal use only and may be removed in future
+ * versions.
+ *
+ * @since
+ * @param selection
+ * the current selection
+ */
+ public void sendSelection(String selection) {
+ rpc.setSelectedItem(selection);
+ afterSendRequestToServer();
+ }
+
+ /**
+ * Notify the server that the combo box received focus.
+ *
+ * For timing reasons, ConnectorFocusAndBlurHandler is not used at the
+ * moment.
+ *
+ * This method is for internal use only and may be removed in future
+ * versions.
+ *
+ * @since
+ */
+ public void sendFocusEvent() {
+ boolean registeredListeners = hasEventListener(EventId.FOCUS);
+ if (registeredListeners) {
+ focusAndBlurRpc.focus();
+ afterSendRequestToServer();
+ }
+ }
+
+ /**
+ * Notify the server that the combo box lost focus.
+ *
+ * For timing reasons, ConnectorFocusAndBlurHandler is not used at the
+ * moment.
+ *
+ * This method is for internal use only and may be removed in future
+ * versions.
+ *
+ * @since
+ */
+ public void sendBlurEvent() {
+ boolean registeredListeners = hasEventListener(EventId.BLUR);
+ if (registeredListeners) {
+ focusAndBlurRpc.blur();
+ afterSendRequestToServer();
+ }
+ }
+
+ /*
+ * Called after any request to server.
+ */
+ private void afterSendRequestToServer() {
+ getDataReceivedHandler().anyRequestSentToServer();
+ }
+
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/client/ui/dd/VIsOverId.java b/compatibility-client/src/main/java/com/vaadin/client/ui/dd/VIsOverId.java
new file mode 100644
index 0000000000..b02f8df147
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/client/ui/dd/VIsOverId.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2000-2016 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+/**
+ *
+ */
+package com.vaadin.client.ui.dd;
+
+import com.vaadin.client.ComponentConnector;
+import com.vaadin.client.UIDL;
+import com.vaadin.shared.ui.dd.AcceptCriterion;
+import com.vaadin.ui.AbstractSelect;
+
+@AcceptCriterion(AbstractSelect.TargetItemIs.class)
+final public class VIsOverId extends VAcceptCriterion {
+
+ @Override
+ protected boolean accept(VDragEvent drag, UIDL configuration) {
+ try {
+
+ String pid = configuration.getStringAttribute("s");
+ VDropHandler currentDropHandler = VDragAndDropManager.get()
+ .getCurrentDropHandler();
+ ComponentConnector dropHandlerConnector = currentDropHandler
+ .getConnector();
+
+ String pid2 = dropHandlerConnector.getConnectorId();
+ if (pid2.equals(pid)) {
+ Object searchedId = drag.getDropDetails().get("itemIdOver");
+ String[] stringArrayAttribute = configuration
+ .getStringArrayAttribute("keys");
+ for (String string : stringArrayAttribute) {
+ if (string.equals(searchedId)) {
+ return true;
+ }
+ }
+ }
+ } catch (Exception e) {
+ }
+ return false;
+ }
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/client/ui/dd/VItemIdIs.java b/compatibility-client/src/main/java/com/vaadin/client/ui/dd/VItemIdIs.java
new file mode 100644
index 0000000000..6992ebcaad
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/client/ui/dd/VItemIdIs.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2000-2016 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+/**
+ *
+ */
+package com.vaadin.client.ui.dd;
+
+import com.vaadin.client.ComponentConnector;
+import com.vaadin.client.UIDL;
+import com.vaadin.shared.ui.dd.AcceptCriterion;
+import com.vaadin.ui.AbstractSelect;
+
+@AcceptCriterion(AbstractSelect.AcceptItem.class)
+final public class VItemIdIs extends VAcceptCriterion {
+
+ @Override
+ protected boolean accept(VDragEvent drag, UIDL configuration) {
+ try {
+ String pid = configuration.getStringAttribute("s");
+ ComponentConnector dragSource = drag.getTransferable()
+ .getDragSource();
+ String pid2 = dragSource.getConnectorId();
+ if (pid2.equals(pid)) {
+ Object searchedId = drag.getTransferable().getData("itemId");
+ String[] stringArrayAttribute = configuration
+ .getStringArrayAttribute("keys");
+ for (String string : stringArrayAttribute) {
+ if (string.equals(searchedId)) {
+ return true;
+ }
+ }
+ }
+ } catch (Exception e) {
+ }
+ return false;
+ }
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/client/ui/tree/TreeConnector.java b/compatibility-client/src/main/java/com/vaadin/client/ui/tree/TreeConnector.java
new file mode 100644
index 0000000000..c28a58aaad
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/client/ui/tree/TreeConnector.java
@@ -0,0 +1,392 @@
+/*
+ * Copyright 2000-2016 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.client.ui.tree;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+
+import com.google.gwt.aria.client.Roles;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.dom.client.EventTarget;
+import com.vaadin.client.ApplicationConnection;
+import com.vaadin.client.BrowserInfo;
+import com.vaadin.client.Paintable;
+import com.vaadin.client.TooltipInfo;
+import com.vaadin.client.UIDL;
+import com.vaadin.client.VConsole;
+import com.vaadin.client.WidgetUtil;
+import com.vaadin.client.communication.StateChangeEvent;
+import com.vaadin.client.ui.AbstractComponentConnector;
+import com.vaadin.client.ui.VTree;
+import com.vaadin.client.ui.VTree.TreeNode;
+import com.vaadin.shared.MouseEventDetails;
+import com.vaadin.shared.ui.Connect;
+import com.vaadin.shared.ui.MultiSelectMode;
+import com.vaadin.shared.ui.tree.TreeConstants;
+import com.vaadin.shared.ui.tree.TreeServerRpc;
+import com.vaadin.shared.ui.tree.TreeState;
+import com.vaadin.ui.Tree;
+
+@Connect(Tree.class)
+public class TreeConnector extends AbstractComponentConnector
+ implements Paintable {
+
+ protected final Map<TreeNode, TooltipInfo> tooltipMap = new HashMap<TreeNode, TooltipInfo>();
+
+ @Override
+ protected void init() {
+ getWidget().connector = this;
+ }
+
+ @Override
+ public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+ if (!isRealUpdate(uidl)) {
+ return;
+ }
+
+ getWidget().rendering = true;
+
+ getWidget().client = client;
+
+ if (uidl.hasAttribute("partialUpdate")) {
+ handleUpdate(uidl);
+
+ getWidget().rendering = false;
+ return;
+ }
+
+ getWidget().paintableId = uidl.getId();
+
+ getWidget().immediate = getState().immediate;
+
+ getWidget().disabled = !isEnabled();
+ getWidget().readonly = isReadOnly();
+
+ getWidget().dragMode = uidl.hasAttribute("dragMode")
+ ? uidl.getIntAttribute("dragMode") : 0;
+
+ getWidget().isNullSelectionAllowed = uidl
+ .getBooleanAttribute("nullselect");
+ getWidget().isHtmlContentAllowed = uidl
+ .getBooleanAttribute(TreeConstants.ATTRIBUTE_HTML_ALLOWED);
+
+ if (uidl.hasAttribute("alb")) {
+ getWidget().bodyActionKeys = uidl.getStringArrayAttribute("alb");
+ }
+
+ getWidget().body.clear();
+ // clear out any references to nodes that no longer are attached
+ getWidget().clearNodeToKeyMap();
+ tooltipMap.clear();
+
+ TreeNode childTree = null;
+ UIDL childUidl = null;
+ for (final Iterator<?> i = uidl.getChildIterator(); i.hasNext();) {
+ childUidl = (UIDL) i.next();
+ if ("actions".equals(childUidl.getTag())) {
+ updateActionMap(childUidl);
+ continue;
+ } else if ("-ac".equals(childUidl.getTag())) {
+ getWidget().updateDropHandler(childUidl);
+ continue;
+ }
+ childTree = getWidget().new TreeNode();
+ getConnection().getVTooltip().connectHandlersToWidget(childTree);
+ updateNodeFromUIDL(childTree, childUidl, 1);
+ getWidget().body.add(childTree);
+ childTree.addStyleDependentName("root");
+ childTree.childNodeContainer.addStyleDependentName("root");
+ }
+ if (childTree != null && childUidl != null) {
+ boolean leaf = !childUidl.getTag().equals("node");
+ childTree.addStyleDependentName(leaf ? "leaf-last" : "last");
+ childTree.childNodeContainer.addStyleDependentName("last");
+ }
+ final String selectMode = uidl.getStringAttribute("selectmode");
+ getWidget().selectable = !"none".equals(selectMode);
+ getWidget().isMultiselect = "multi".equals(selectMode);
+
+ if (getWidget().isMultiselect) {
+ Roles.getTreeRole().setAriaMultiselectableProperty(
+ getWidget().getElement(), true);
+
+ if (BrowserInfo.get().isTouchDevice()) {
+ // Always use the simple mode for touch devices that do not have
+ // shift/ctrl keys (#8595)
+ getWidget().multiSelectMode = MultiSelectMode.SIMPLE;
+ } else {
+ getWidget().multiSelectMode = MultiSelectMode
+ .valueOf(uidl.getStringAttribute("multiselectmode"));
+ }
+ } else {
+ Roles.getTreeRole().setAriaMultiselectableProperty(
+ getWidget().getElement(), false);
+ }
+
+ getWidget().selectedIds = uidl.getStringArrayVariableAsSet("selected");
+
+ // Update lastSelection and focusedNode to point to *actual* nodes again
+ // after the old ones have been cleared from the body. This fixes focus
+ // and keyboard navigation issues as described in #7057 and other
+ // tickets.
+ if (getWidget().lastSelection != null) {
+ getWidget().lastSelection = getWidget()
+ .getNodeByKey(getWidget().lastSelection.key);
+ }
+
+ if (getWidget().focusedNode != null) {
+
+ Set<String> selectedIds = getWidget().selectedIds;
+
+ // If the focused node is not between the selected nodes, we need to
+ // refresh the focused node to prevent an undesired scroll. #12618.
+ if (!selectedIds.isEmpty()
+ && !selectedIds.contains(getWidget().focusedNode.key)) {
+ String keySelectedId = selectedIds.iterator().next();
+
+ TreeNode nodeToSelect = getWidget().getNodeByKey(keySelectedId);
+
+ getWidget().setFocusedNode(nodeToSelect);
+ } else {
+ getWidget().setFocusedNode(
+ getWidget().getNodeByKey(getWidget().focusedNode.key));
+ }
+ }
+
+ if (getWidget().lastSelection == null && getWidget().focusedNode == null
+ && !getWidget().selectedIds.isEmpty()) {
+ getWidget().setFocusedNode(getWidget()
+ .getNodeByKey(getWidget().selectedIds.iterator().next()));
+ getWidget().focusedNode.setFocused(false);
+ }
+
+ getWidget().rendering = false;
+
+ }
+
+ @Override
+ public void onStateChanged(StateChangeEvent stateChangeEvent) {
+ super.onStateChanged(stateChangeEvent);
+ // VTree does not implement Focusable
+ getWidget().setTabIndex(getState().tabIndex);
+ }
+
+ @Override
+ public VTree getWidget() {
+ return (VTree) super.getWidget();
+ }
+
+ private void handleUpdate(UIDL uidl) {
+ final TreeNode rootNode = getWidget()
+ .getNodeByKey(uidl.getStringAttribute("rootKey"));
+ if (rootNode != null) {
+ if (!rootNode.getState()) {
+ // expanding node happened server side
+ rootNode.setState(true, false);
+ }
+ String levelPropertyString = Roles.getTreeitemRole()
+ .getAriaLevelProperty(rootNode.getElement());
+ int levelProperty;
+ try {
+ levelProperty = Integer.valueOf(levelPropertyString);
+ } catch (NumberFormatException e) {
+ levelProperty = 1;
+ VConsole.error(e);
+ }
+
+ renderChildNodes(rootNode, (Iterator) uidl.getChildIterator(),
+ levelProperty + 1);
+ }
+ }
+
+ /**
+ * Registers action for the root and also for individual nodes
+ *
+ * @param uidl
+ */
+ private void updateActionMap(UIDL uidl) {
+ final Iterator<?> it = uidl.getChildIterator();
+ while (it.hasNext()) {
+ final UIDL action = (UIDL) it.next();
+ final String key = action.getStringAttribute("key");
+ final String caption = action
+ .getStringAttribute(TreeConstants.ATTRIBUTE_ACTION_CAPTION);
+ String iconUrl = null;
+ if (action.hasAttribute(TreeConstants.ATTRIBUTE_ACTION_ICON)) {
+ iconUrl = getConnection()
+ .translateVaadinUri(action.getStringAttribute(
+ TreeConstants.ATTRIBUTE_ACTION_ICON));
+ }
+ getWidget().registerAction(key, caption, iconUrl);
+ }
+
+ }
+
+ public void updateNodeFromUIDL(TreeNode treeNode, UIDL uidl, int level) {
+ Roles.getTreeitemRole().setAriaLevelProperty(treeNode.getElement(),
+ level);
+
+ String nodeKey = uidl.getStringAttribute("key");
+ String caption = uidl
+ .getStringAttribute(TreeConstants.ATTRIBUTE_NODE_CAPTION);
+ if (getWidget().isHtmlContentAllowed) {
+ treeNode.setHtml(caption);
+ } else {
+ treeNode.setText(caption);
+ }
+ treeNode.key = nodeKey;
+
+ getWidget().registerNode(treeNode);
+
+ if (uidl.hasAttribute("al")) {
+ treeNode.actionKeys = uidl.getStringArrayAttribute("al");
+ }
+
+ if (uidl.getTag().equals("node")) {
+ if (uidl.getChildCount() == 0) {
+ treeNode.childNodeContainer.setVisible(false);
+ } else {
+ renderChildNodes(treeNode, (Iterator) uidl.getChildIterator(),
+ level + 1);
+ treeNode.childrenLoaded = true;
+ }
+ } else {
+ treeNode.addStyleName(TreeNode.CLASSNAME + "-leaf");
+ }
+ if (uidl.hasAttribute(TreeConstants.ATTRIBUTE_NODE_STYLE)) {
+ treeNode.setNodeStyleName(uidl
+ .getStringAttribute(TreeConstants.ATTRIBUTE_NODE_STYLE));
+ }
+
+ String description = uidl.getStringAttribute("descr");
+ if (description != null) {
+ tooltipMap.put(treeNode,
+ new TooltipInfo(description, null, treeNode));
+ }
+
+ if (uidl.getBooleanAttribute("expanded") && !treeNode.getState()) {
+ treeNode.setState(true, false);
+ }
+
+ if (uidl.getBooleanAttribute("selected")) {
+ treeNode.setSelected(true);
+ // ensure that identifier is in selectedIds array (this may be a
+ // partial update)
+ getWidget().selectedIds.add(nodeKey);
+ }
+
+ String iconUrl = uidl
+ .getStringAttribute(TreeConstants.ATTRIBUTE_NODE_ICON);
+ String iconAltText = uidl
+ .getStringAttribute(TreeConstants.ATTRIBUTE_NODE_ICON_ALT);
+ treeNode.setIcon(iconUrl, iconAltText);
+ }
+
+ void renderChildNodes(TreeNode containerNode, Iterator<UIDL> i, int level) {
+ containerNode.childNodeContainer.clear();
+ containerNode.childNodeContainer.setVisible(true);
+ while (i.hasNext()) {
+ final UIDL childUidl = i.next();
+ // actions are in bit weird place, don't mix them with children,
+ // but current node's actions
+ if ("actions".equals(childUidl.getTag())) {
+ updateActionMap(childUidl);
+ continue;
+ }
+ final TreeNode childTree = getWidget().new TreeNode();
+ getConnection().getVTooltip().connectHandlersToWidget(childTree);
+ updateNodeFromUIDL(childTree, childUidl, level);
+ containerNode.childNodeContainer.add(childTree);
+ if (!i.hasNext()) {
+ childTree.addStyleDependentName(
+ childTree.isLeaf() ? "leaf-last" : "last");
+ childTree.childNodeContainer.addStyleDependentName("last");
+ }
+ }
+ containerNode.childrenLoaded = true;
+ }
+
+ @Override
+ public boolean isReadOnly() {
+ return super.isReadOnly() || getState().propertyReadOnly;
+ }
+
+ @Override
+ public TreeState getState() {
+ return (TreeState) super.getState();
+ }
+
+ @Override
+ public TooltipInfo getTooltipInfo(Element element) {
+
+ TooltipInfo info = null;
+
+ // Try to find a tooltip for a node
+ if (element != getWidget().getElement()) {
+ Object node = WidgetUtil.findWidget(element, TreeNode.class);
+
+ if (node != null) {
+ TreeNode tnode = (TreeNode) node;
+ if (tnode.isCaptionElement(element)) {
+ info = tooltipMap.get(tnode);
+ }
+ }
+ }
+
+ // If no tooltip found for the node or if the target was not a node, use
+ // the default tooltip
+ if (info == null) {
+ info = super.getTooltipInfo(element);
+ }
+
+ return info;
+ }
+
+ @Override
+ public boolean hasTooltip() {
+ /*
+ * Item tooltips are not processed until updateFromUIDL, so we can't be
+ * sure that there are no tooltips during onStateChange when this method
+ * is used.
+ */
+ return true;
+ }
+
+ @Override
+ protected void sendContextClickEvent(MouseEventDetails details,
+ EventTarget eventTarget) {
+ if (!Element.is(eventTarget)) {
+ return;
+ }
+
+ Element e = Element.as(eventTarget);
+ String key = null;
+
+ if (getWidget().body.getElement().isOrHasChild(e)) {
+ TreeNode t = WidgetUtil.findWidget(e, TreeNode.class);
+ if (t != null) {
+ key = t.key;
+ }
+ }
+
+ getRpcProxy(TreeServerRpc.class).contextClick(key, details);
+
+ WidgetUtil.clearTextSelection();
+ }
+}
+
diff --git a/compatibility-client/src/main/java/com/vaadin/client/ui/tree/VTargetInSubtree.java b/compatibility-client/src/main/java/com/vaadin/client/ui/tree/VTargetInSubtree.java
new file mode 100644
index 0000000000..34b8428582
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/client/ui/tree/VTargetInSubtree.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2000-2016 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+/**
+ *
+ */
+package com.vaadin.client.ui.tree;
+
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.client.UIDL;
+import com.vaadin.client.ui.VTree;
+import com.vaadin.client.ui.VTree.TreeNode;
+import com.vaadin.client.ui.dd.VAcceptCriterion;
+import com.vaadin.client.ui.dd.VDragAndDropManager;
+import com.vaadin.client.ui.dd.VDragEvent;
+import com.vaadin.shared.ui.dd.AcceptCriterion;
+import com.vaadin.ui.Tree;
+
+@AcceptCriterion(Tree.TargetInSubtree.class)
+final public class VTargetInSubtree extends VAcceptCriterion {
+
+ @Override
+ protected boolean accept(VDragEvent drag, UIDL configuration) {
+
+ VTree tree = (VTree) VDragAndDropManager.get().getCurrentDropHandler()
+ .getConnector().getWidget();
+ TreeNode treeNode = tree
+ .getNodeByKey((String) drag.getDropDetails().get("itemIdOver"));
+ if (treeNode != null) {
+ Widget parent2 = treeNode;
+ int depth = configuration.getIntAttribute("depth");
+ if (depth < 0) {
+ depth = Integer.MAX_VALUE;
+ }
+ final String searchedKey = configuration.getStringAttribute("key");
+ for (int i = 0; i <= depth && parent2 instanceof TreeNode; i++) {
+ if (searchedKey.equals(((TreeNode) parent2).key)) {
+ return true;
+ }
+ // panel -> next level node
+ parent2 = parent2.getParent().getParent();
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/compatibility-client/src/main/java/com/vaadin/client/ui/tree/VTreeLazyInitItemIdentifiers.java b/compatibility-client/src/main/java/com/vaadin/client/ui/tree/VTreeLazyInitItemIdentifiers.java
new file mode 100644
index 0000000000..46089af16e
--- /dev/null
+++ b/compatibility-client/src/main/java/com/vaadin/client/ui/tree/VTreeLazyInitItemIdentifiers.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2000-2016 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.client.ui.tree;
+
+import com.vaadin.client.ui.dd.VLazyInitItemIdentifiers;
+import com.vaadin.shared.ui.dd.AcceptCriterion;
+import com.vaadin.ui.Tree;
+
+@AcceptCriterion(Tree.TreeDropCriterion.class)
+public final class VTreeLazyInitItemIdentifiers
+ extends VLazyInitItemIdentifiers {
+ // all logic in superclass
+}