aboutsummaryrefslogtreecommitdiffstats
path: root/compatibility-server/src/main/java/com/vaadin/v7/ui
diff options
context:
space:
mode:
authorArtur Signell <artur@vaadin.com>2016-08-18 23:19:41 +0300
committerArtur Signell <artur@vaadin.com>2016-08-22 15:59:51 +0300
commitc6b44ac8adc9b2ffd6290c98643a633f405dd6c6 (patch)
tree634982e9a789452ab8046e660a9170cc8b25623f /compatibility-server/src/main/java/com/vaadin/v7/ui
parentec8904f6b0ab77231d567daa35c9cc7138b6fe59 (diff)
downloadvaadin-framework-c6b44ac8adc9b2ffd6290c98643a633f405dd6c6.tar.gz
vaadin-framework-c6b44ac8adc9b2ffd6290c98643a633f405dd6c6.zip
Move and rename server classes which go into the compatibility package
* Use com.vaadin.v7 * Use the same class name as in Vaadin 7 * Use a "vaadin7-" declarative prefix for Vaadin 7 components Change-Id: I19a27f3835b18980b91a4f8f9464b2adde1a5fd5
Diffstat (limited to 'compatibility-server/src/main/java/com/vaadin/v7/ui')
-rw-r--r--compatibility-server/src/main/java/com/vaadin/v7/ui/AbstractColorPicker.java590
-rw-r--r--compatibility-server/src/main/java/com/vaadin/v7/ui/AbstractSelect.java2355
-rw-r--r--compatibility-server/src/main/java/com/vaadin/v7/ui/Calendar.java2031
-rw-r--r--compatibility-server/src/main/java/com/vaadin/v7/ui/ColorPicker.java67
-rw-r--r--compatibility-server/src/main/java/com/vaadin/v7/ui/ColorPickerArea.java77
-rw-r--r--compatibility-server/src/main/java/com/vaadin/v7/ui/ComboBox.java926
-rw-r--r--compatibility-server/src/main/java/com/vaadin/v7/ui/DateField.java (renamed from compatibility-server/src/main/java/com/vaadin/v7/ui/LegacyDateField.java)38
-rw-r--r--compatibility-server/src/main/java/com/vaadin/v7/ui/DefaultFieldFactory.java111
-rw-r--r--compatibility-server/src/main/java/com/vaadin/v7/ui/Grid.java7355
-rw-r--r--compatibility-server/src/main/java/com/vaadin/v7/ui/InlineDateField.java (renamed from compatibility-server/src/main/java/com/vaadin/v7/ui/LegacyInlineDateField.java)18
-rw-r--r--compatibility-server/src/main/java/com/vaadin/v7/ui/ListSelect.java80
-rw-r--r--compatibility-server/src/main/java/com/vaadin/v7/ui/NativeSelect.java108
-rw-r--r--compatibility-server/src/main/java/com/vaadin/v7/ui/OptionGroup.java253
-rw-r--r--compatibility-server/src/main/java/com/vaadin/v7/ui/PopupDateField.java (renamed from compatibility-server/src/main/java/com/vaadin/v7/ui/LegacyPopupDateField.java)18
-rw-r--r--compatibility-server/src/main/java/com/vaadin/v7/ui/RichTextArea.java317
-rw-r--r--compatibility-server/src/main/java/com/vaadin/v7/ui/Select.java60
-rw-r--r--compatibility-server/src/main/java/com/vaadin/v7/ui/Table.java6536
-rw-r--r--compatibility-server/src/main/java/com/vaadin/v7/ui/TableFieldFactory.java56
-rw-r--r--compatibility-server/src/main/java/com/vaadin/v7/ui/TextArea.java170
-rw-r--r--compatibility-server/src/main/java/com/vaadin/v7/ui/Tree.java1985
-rw-r--r--compatibility-server/src/main/java/com/vaadin/v7/ui/TreeTable.java985
-rw-r--r--compatibility-server/src/main/java/com/vaadin/v7/ui/TwinColSelect.java169
-rw-r--r--compatibility-server/src/main/java/com/vaadin/v7/ui/components/calendar/CalendarComponentEvent.java51
-rw-r--r--compatibility-server/src/main/java/com/vaadin/v7/ui/components/calendar/CalendarComponentEvents.java603
-rw-r--r--compatibility-server/src/main/java/com/vaadin/v7/ui/components/calendar/CalendarDateRange.java97
-rw-r--r--compatibility-server/src/main/java/com/vaadin/v7/ui/components/calendar/CalendarTargetDetails.java80
-rw-r--r--compatibility-server/src/main/java/com/vaadin/v7/ui/components/calendar/ContainerEventProvider.java566
-rw-r--r--compatibility-server/src/main/java/com/vaadin/v7/ui/components/calendar/event/BasicEvent.java265
-rw-r--r--compatibility-server/src/main/java/com/vaadin/v7/ui/components/calendar/event/BasicEventProvider.java177
-rw-r--r--compatibility-server/src/main/java/com/vaadin/v7/ui/components/calendar/event/CalendarEditableEventProvider.java42
-rw-r--r--compatibility-server/src/main/java/com/vaadin/v7/ui/components/calendar/event/CalendarEvent.java147
-rw-r--r--compatibility-server/src/main/java/com/vaadin/v7/ui/components/calendar/event/CalendarEventProvider.java112
-rw-r--r--compatibility-server/src/main/java/com/vaadin/v7/ui/components/calendar/event/EditableCalendarEvent.java91
-rw-r--r--compatibility-server/src/main/java/com/vaadin/v7/ui/components/calendar/handler/BasicBackwardHandler.java96
-rw-r--r--compatibility-server/src/main/java/com/vaadin/v7/ui/components/calendar/handler/BasicDateClickHandler.java70
-rw-r--r--compatibility-server/src/main/java/com/vaadin/v7/ui/components/calendar/handler/BasicEventMoveHandler.java74
-rw-r--r--compatibility-server/src/main/java/com/vaadin/v7/ui/components/calendar/handler/BasicEventResizeHandler.java70
-rw-r--r--compatibility-server/src/main/java/com/vaadin/v7/ui/components/calendar/handler/BasicForwardHandler.java94
-rw-r--r--compatibility-server/src/main/java/com/vaadin/v7/ui/components/calendar/handler/BasicWeekClickHandler.java82
-rw-r--r--compatibility-server/src/main/java/com/vaadin/v7/ui/components/colorpicker/ColorChangeEvent.java43
-rw-r--r--compatibility-server/src/main/java/com/vaadin/v7/ui/components/colorpicker/ColorChangeListener.java42
-rw-r--r--compatibility-server/src/main/java/com/vaadin/v7/ui/components/colorpicker/ColorPickerGradient.java144
-rw-r--r--compatibility-server/src/main/java/com/vaadin/v7/ui/components/colorpicker/ColorPickerGrid.java258
-rw-r--r--compatibility-server/src/main/java/com/vaadin/v7/ui/components/colorpicker/ColorPickerHistory.java217
-rw-r--r--compatibility-server/src/main/java/com/vaadin/v7/ui/components/colorpicker/ColorPickerPopup.java759
-rw-r--r--compatibility-server/src/main/java/com/vaadin/v7/ui/components/colorpicker/ColorPickerPreview.java198
-rw-r--r--compatibility-server/src/main/java/com/vaadin/v7/ui/components/colorpicker/ColorPickerSelect.java235
-rw-r--r--compatibility-server/src/main/java/com/vaadin/v7/ui/components/colorpicker/ColorSelector.java43
-rw-r--r--compatibility-server/src/main/java/com/vaadin/v7/ui/components/colorpicker/HasColorChangeListener.java36
-rw-r--r--compatibility-server/src/main/java/com/vaadin/v7/ui/renderers/AbstractJavaScriptRenderer.java175
-rw-r--r--compatibility-server/src/main/java/com/vaadin/v7/ui/renderers/ButtonRenderer.java74
-rw-r--r--compatibility-server/src/main/java/com/vaadin/v7/ui/renderers/ClickableRenderer.java143
-rw-r--r--compatibility-server/src/main/java/com/vaadin/v7/ui/renderers/DateRenderer.java240
-rw-r--r--compatibility-server/src/main/java/com/vaadin/v7/ui/renderers/HtmlRenderer.java48
-rw-r--r--compatibility-server/src/main/java/com/vaadin/v7/ui/renderers/ImageRenderer.java68
-rw-r--r--compatibility-server/src/main/java/com/vaadin/v7/ui/renderers/NumberRenderer.java207
-rw-r--r--compatibility-server/src/main/java/com/vaadin/v7/ui/renderers/ProgressBarRenderer.java46
-rw-r--r--compatibility-server/src/main/java/com/vaadin/v7/ui/renderers/Renderer.java69
-rw-r--r--compatibility-server/src/main/java/com/vaadin/v7/ui/renderers/TextRenderer.java49
59 files changed, 30079 insertions, 37 deletions
diff --git a/compatibility-server/src/main/java/com/vaadin/v7/ui/AbstractColorPicker.java b/compatibility-server/src/main/java/com/vaadin/v7/ui/AbstractColorPicker.java
new file mode 100644
index 0000000000..da03136593
--- /dev/null
+++ b/compatibility-server/src/main/java/com/vaadin/v7/ui/AbstractColorPicker.java
@@ -0,0 +1,590 @@
+/*
+ * 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.v7.ui;
+
+import java.io.Serializable;
+import java.lang.reflect.Method;
+import java.util.Collection;
+
+import org.jsoup.nodes.Attributes;
+import org.jsoup.nodes.Element;
+
+import com.vaadin.shared.ui.colorpicker.Color;
+import com.vaadin.shared.ui.colorpicker.ColorPickerServerRpc;
+import com.vaadin.shared.ui.colorpicker.ColorPickerState;
+import com.vaadin.ui.AbstractComponent;
+import com.vaadin.ui.UI;
+import com.vaadin.ui.Window.CloseEvent;
+import com.vaadin.ui.Window.CloseListener;
+import com.vaadin.ui.declarative.DesignAttributeHandler;
+import com.vaadin.ui.declarative.DesignContext;
+import com.vaadin.v7.ui.components.colorpicker.ColorChangeEvent;
+import com.vaadin.v7.ui.components.colorpicker.ColorChangeListener;
+import com.vaadin.v7.ui.components.colorpicker.ColorPickerPopup;
+import com.vaadin.v7.ui.components.colorpicker.ColorSelector;
+
+/**
+ * An abstract class that defines default implementation for a color picker
+ * component.
+ *
+ * @since 7.0.0
+ */
+public abstract class AbstractColorPicker extends AbstractComponent
+ implements CloseListener, ColorSelector {
+ private static final Method COLOR_CHANGE_METHOD;
+ static {
+ try {
+ COLOR_CHANGE_METHOD = ColorChangeListener.class.getDeclaredMethod(
+ "colorChanged", new Class[] { ColorChangeEvent.class });
+ } catch (final java.lang.NoSuchMethodException e) {
+ // This should never happen
+ throw new java.lang.RuntimeException(
+ "Internal error finding methods in ColorPicker");
+ }
+ }
+
+ /**
+ * Interface for converting 2d-coordinates to a Color
+ */
+ public interface Coordinates2Color extends Serializable {
+
+ /**
+ * Calculate color from coordinates
+ *
+ * @param x
+ * the x-coordinate
+ * @param y
+ * the y-coordinate
+ *
+ * @return the color
+ */
+ public Color calculate(int x, int y);
+
+ /**
+ * Calculate coordinates from color
+ *
+ * @param c
+ * the c
+ *
+ * @return the integer array with the coordinates
+ */
+ public int[] calculate(Color c);
+ }
+
+ public enum PopupStyle {
+ POPUP_NORMAL("normal"), POPUP_SIMPLE("simple");
+
+ private String style;
+
+ PopupStyle(String styleName) {
+ style = styleName;
+ }
+
+ @Override
+ public String toString() {
+ return style;
+ }
+ }
+
+ private ColorPickerServerRpc rpc = new ColorPickerServerRpc() {
+
+ @Override
+ public void openPopup(boolean open) {
+ showPopup(open);
+ }
+ };
+
+ protected static final String STYLENAME_DEFAULT = "v-colorpicker";
+ protected static final String STYLENAME_BUTTON = "v-button";
+ protected static final String STYLENAME_AREA = "v-colorpicker-area";
+
+ protected PopupStyle popupStyle = PopupStyle.POPUP_NORMAL;
+
+ /** The popup window. */
+ private ColorPickerPopup window;
+
+ /** The color. */
+ protected Color color;
+
+ /** The UI. */
+ private UI parent;
+
+ protected String popupCaption = null;
+ private int positionX = 0;
+ private int positionY = 0;
+
+ protected boolean rgbVisible = true;
+ protected boolean hsvVisible = true;
+ protected boolean swatchesVisible = true;
+ protected boolean historyVisible = true;
+ protected boolean textfieldVisible = true;
+
+ /**
+ * Instantiates a new color picker.
+ */
+ public AbstractColorPicker() {
+ this("Colors", Color.WHITE);
+ }
+
+ /**
+ * Instantiates a new color picker.
+ *
+ * @param popupCaption
+ * the caption of the popup window
+ */
+ public AbstractColorPicker(String popupCaption) {
+ this(popupCaption, Color.WHITE);
+ }
+
+ /**
+ * Instantiates a new color picker.
+ *
+ * @param popupCaption
+ * the caption of the popup window
+ * @param initialColor
+ * the initial color
+ */
+ public AbstractColorPicker(String popupCaption, Color initialColor) {
+ super();
+ registerRpc(rpc);
+ setColor(initialColor);
+ this.popupCaption = popupCaption;
+ setDefaultStyles();
+ setCaption("");
+ }
+
+ @Override
+ public void setColor(Color color) {
+ this.color = color;
+
+ if (window != null) {
+ window.setColor(color);
+ }
+ getState().color = color.getCSS();
+ }
+
+ @Override
+ public Color getColor() {
+ return color;
+ }
+
+ /**
+ * Set true if the component should show a default caption (css-code for the
+ * currently selected color, e.g. #ffffff) when no other caption is
+ * available.
+ *
+ * @param enabled
+ */
+ public void setDefaultCaptionEnabled(boolean enabled) {
+ getState().showDefaultCaption = enabled;
+ }
+
+ /**
+ * Returns true if the component shows the default caption (css-code for the
+ * currently selected color, e.g. #ffffff) if no other caption is available.
+ */
+ public boolean isDefaultCaptionEnabled() {
+ return getState(false).showDefaultCaption;
+ }
+
+ /**
+ * Sets the position of the popup window
+ *
+ * @param x
+ * the x-coordinate
+ * @param y
+ * the y-coordinate
+ */
+ public void setPosition(int x, int y) {
+ positionX = x;
+ positionY = y;
+
+ if (window != null) {
+ window.setPositionX(x);
+ window.setPositionY(y);
+ }
+ }
+
+ @Override
+ public void addColorChangeListener(ColorChangeListener listener) {
+ addListener(ColorChangeEvent.class, listener, COLOR_CHANGE_METHOD);
+ }
+
+ @Override
+ public void removeColorChangeListener(ColorChangeListener listener) {
+ removeListener(ColorChangeEvent.class, listener);
+ }
+
+ @Override
+ public void windowClose(CloseEvent e) {
+ if (e.getWindow() == window) {
+ getState().popupVisible = false;
+ }
+ }
+
+ /**
+ * Fired when a color change event occurs
+ *
+ * @param event
+ * The color change event
+ */
+ protected void colorChanged(ColorChangeEvent event) {
+ setColor(event.getColor());
+ fireColorChanged();
+ }
+
+ /**
+ * Notifies the listeners that the selected color has changed
+ */
+ public void fireColorChanged() {
+ fireEvent(new ColorChangeEvent(this, color));
+ }
+
+ /**
+ * The style for the popup window
+ *
+ * @param style
+ * The style
+ */
+ public void setPopupStyle(PopupStyle style) {
+ popupStyle = style;
+
+ switch (style) {
+ case POPUP_NORMAL: {
+ setRGBVisibility(true);
+ setHSVVisibility(true);
+ setSwatchesVisibility(true);
+ setHistoryVisibility(true);
+ setTextfieldVisibility(true);
+ break;
+ }
+
+ case POPUP_SIMPLE: {
+ setRGBVisibility(false);
+ setHSVVisibility(false);
+ setSwatchesVisibility(true);
+ setHistoryVisibility(false);
+ setTextfieldVisibility(false);
+ break;
+ }
+ }
+ }
+
+ /**
+ * Gets the style for the popup window
+ *
+ * @since 7.5.0
+ * @return popup window style
+ */
+ public PopupStyle getPopupStyle() {
+ return popupStyle;
+ }
+
+ /**
+ * Set the visibility of the RGB Tab
+ *
+ * @param visible
+ * The visibility
+ */
+ public void setRGBVisibility(boolean visible) {
+
+ if (!visible && !hsvVisible && !swatchesVisible) {
+ throw new IllegalArgumentException("Cannot hide all tabs.");
+ }
+
+ rgbVisible = visible;
+ if (window != null) {
+ window.setRGBTabVisible(visible);
+ }
+ }
+
+ /**
+ * Gets the visibility of the RGB Tab
+ *
+ * @since 7.5.0
+ * @return visibility of the RGB tab
+ */
+ public boolean getRGBVisibility() {
+ return rgbVisible;
+ }
+
+ /**
+ * Set the visibility of the HSV Tab
+ *
+ * @param visible
+ * The visibility
+ */
+ public void setHSVVisibility(boolean visible) {
+ if (!visible && !rgbVisible && !swatchesVisible) {
+ throw new IllegalArgumentException("Cannot hide all tabs.");
+ }
+
+ hsvVisible = visible;
+ if (window != null) {
+ window.setHSVTabVisible(visible);
+ }
+ }
+
+ /**
+ * Gets the visibility of the HSV Tab
+ *
+ * @since 7.5.0
+ * @return visibility of the HSV tab
+ */
+ public boolean getHSVVisibility() {
+ return hsvVisible;
+ }
+
+ /**
+ * Set the visibility of the Swatches Tab
+ *
+ * @param visible
+ * The visibility
+ */
+ public void setSwatchesVisibility(boolean visible) {
+ if (!visible && !hsvVisible && !rgbVisible) {
+ throw new IllegalArgumentException("Cannot hide all tabs.");
+ }
+
+ swatchesVisible = visible;
+ if (window != null) {
+ window.setSwatchesTabVisible(visible);
+ }
+ }
+
+ /**
+ * Gets the visibility of the Swatches Tab
+ *
+ * @since 7.5.0
+ * @return visibility of the swatches tab
+ */
+ public boolean getSwatchesVisibility() {
+ return swatchesVisible;
+ }
+
+ /**
+ * Sets the visibility of the Color History
+ *
+ * @param visible
+ * The visibility
+ */
+ public void setHistoryVisibility(boolean visible) {
+ historyVisible = visible;
+ if (window != null) {
+ window.setHistoryVisible(visible);
+ }
+ }
+
+ /**
+ * Gets the visibility of the Color History
+ *
+ * @since 7.5.0
+ * @return visibility of color history
+ */
+ public boolean getHistoryVisibility() {
+ return historyVisible;
+ }
+
+ /**
+ * Sets the visibility of the CSS color code text field
+ *
+ * @param visible
+ * The visibility
+ */
+ public void setTextfieldVisibility(boolean visible) {
+ textfieldVisible = visible;
+ if (window != null) {
+ window.setPreviewVisible(visible);
+ }
+ }
+
+ /**
+ * Gets the visibility of CSS color code text field
+ *
+ * @since 7.5.0
+ * @return visibility of css color code text field
+ */
+ public boolean getTextfieldVisibility() {
+ return textfieldVisible;
+ }
+
+ @Override
+ protected ColorPickerState getState() {
+ return (ColorPickerState) super.getState();
+ }
+
+ @Override
+ protected ColorPickerState getState(boolean markAsDirty) {
+ return (ColorPickerState) super.getState(markAsDirty);
+ }
+
+ /**
+ * Sets the default styles of the component
+ *
+ */
+ abstract protected void setDefaultStyles();
+
+ /**
+ * Shows a popup-window for color selection.
+ */
+ public void showPopup() {
+ showPopup(true);
+ }
+
+ /**
+ * Hides a popup-window for color selection.
+ */
+ public void hidePopup() {
+ showPopup(false);
+ }
+
+ /**
+ * Shows or hides popup-window depending on the given parameter. If there is
+ * no such window yet, one is created.
+ *
+ * @param open
+ */
+ protected void showPopup(boolean open) {
+ if (open && !isReadOnly()) {
+ if (parent == null) {
+ parent = getUI();
+ }
+
+ if (window == null) {
+
+ // Create the popup
+ window = new ColorPickerPopup(color);
+ window.setCaption(popupCaption);
+
+ window.setRGBTabVisible(rgbVisible);
+ window.setHSVTabVisible(hsvVisible);
+ window.setSwatchesTabVisible(swatchesVisible);
+ window.setHistoryVisible(historyVisible);
+ window.setPreviewVisible(textfieldVisible);
+
+ window.setImmediate(true);
+ window.addCloseListener(this);
+ window.addColorChangeListener(new ColorChangeListener() {
+ @Override
+ public void colorChanged(ColorChangeEvent event) {
+ AbstractColorPicker.this.colorChanged(event);
+ }
+ });
+
+ window.getHistory().setColor(color);
+ parent.addWindow(window);
+ window.setVisible(true);
+ window.setPositionX(positionX);
+ window.setPositionY(positionY);
+
+ } else if (!parent.equals(window.getParent())) {
+
+ window.setRGBTabVisible(rgbVisible);
+ window.setHSVTabVisible(hsvVisible);
+ window.setSwatchesTabVisible(swatchesVisible);
+ window.setHistoryVisible(historyVisible);
+ window.setPreviewVisible(textfieldVisible);
+
+ window.setColor(color);
+ window.getHistory().setColor(color);
+ window.setVisible(true);
+ parent.addWindow(window);
+ }
+
+ } else if (window != null) {
+ window.setVisible(false);
+ parent.removeWindow(window);
+ }
+ getState().popupVisible = open;
+ }
+
+ /**
+ * Set whether the caption text is rendered as HTML or not. You might need
+ * to re-theme component to allow higher content than the original text
+ * style.
+ *
+ * If set to true, the captions are passed to the browser as html and the
+ * developer is responsible for ensuring no harmful html is used. If set to
+ * false, the content is passed to the browser as plain text.
+ *
+ * @param htmlContentAllowed
+ * <code>true</code> if caption is rendered as HTML,
+ * <code>false</code> otherwise
+ * @deprecated as of , use {@link #setCaptionAsHtml(boolean)} instead
+ */
+ @Deprecated
+ public void setHtmlContentAllowed(boolean htmlContentAllowed) {
+ setCaptionAsHtml(htmlContentAllowed);
+ }
+
+ /**
+ * Return HTML rendering setting
+ *
+ * @return <code>true</code> if the caption text is to be rendered as HTML,
+ * <code>false</code> otherwise
+ * @deprecated as of , use {@link #isCaptionAsHtml()} instead
+ */
+ @Deprecated
+ public boolean isHtmlContentAllowed() {
+ return isCaptionAsHtml();
+ }
+
+ @Override
+ public void readDesign(Element design, DesignContext designContext) {
+ super.readDesign(design, designContext);
+
+ Attributes attributes = design.attributes();
+ if (design.hasAttr("color")) {
+ // Ignore the # character
+ String hexColor = DesignAttributeHandler
+ .readAttribute("color", attributes, String.class)
+ .substring(1);
+ setColor(new Color(Integer.parseInt(hexColor, 16)));
+ }
+ if (design.hasAttr("popup-style")) {
+ setPopupStyle(PopupStyle.valueOf(
+ "POPUP_" + attributes.get("popup-style").toUpperCase()));
+ }
+ if (design.hasAttr("position")) {
+ String[] position = attributes.get("position").split(",");
+ setPosition(Integer.parseInt(position[0]),
+ Integer.parseInt(position[1]));
+ }
+ }
+
+ @Override
+ public void writeDesign(Element design, DesignContext designContext) {
+ super.writeDesign(design, designContext);
+
+ Attributes attribute = design.attributes();
+ DesignAttributeHandler.writeAttribute("color", attribute,
+ color.getCSS(), Color.WHITE.getCSS(), String.class);
+ DesignAttributeHandler.writeAttribute("popup-style", attribute,
+ (popupStyle == PopupStyle.POPUP_NORMAL ? "normal" : "simple"),
+ "normal", String.class);
+ DesignAttributeHandler.writeAttribute("position", attribute,
+ positionX + "," + positionY, "0,0", String.class);
+ }
+
+ @Override
+ protected Collection<String> getCustomAttributes() {
+ Collection<String> result = super.getCustomAttributes();
+ result.add("color");
+ result.add("position");
+ result.add("popup-style");
+ return result;
+ }
+}
diff --git a/compatibility-server/src/main/java/com/vaadin/v7/ui/AbstractSelect.java b/compatibility-server/src/main/java/com/vaadin/v7/ui/AbstractSelect.java
new file mode 100644
index 0000000000..b7b241598b
--- /dev/null
+++ b/compatibility-server/src/main/java/com/vaadin/v7/ui/AbstractSelect.java
@@ -0,0 +1,2355 @@
+/*
+ * 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.v7.ui;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.EventObject;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.jsoup.nodes.Element;
+
+import com.vaadin.event.DataBoundTransferable;
+import com.vaadin.event.Transferable;
+import com.vaadin.event.dd.DragAndDropEvent;
+import com.vaadin.event.dd.DropTarget;
+import com.vaadin.event.dd.TargetDetailsImpl;
+import com.vaadin.event.dd.acceptcriteria.ClientSideCriterion;
+import com.vaadin.event.dd.acceptcriteria.ContainsDataFlavor;
+import com.vaadin.event.dd.acceptcriteria.TargetDetailIs;
+import com.vaadin.server.KeyMapper;
+import com.vaadin.server.PaintException;
+import com.vaadin.server.PaintTarget;
+import com.vaadin.server.Resource;
+import com.vaadin.server.VaadinSession;
+import com.vaadin.shared.ui.combobox.FilteringMode;
+import com.vaadin.shared.ui.dd.VerticalDropLocation;
+import com.vaadin.shared.ui.select.AbstractSelectState;
+import com.vaadin.ui.Component;
+import com.vaadin.ui.LegacyComponent;
+import com.vaadin.ui.declarative.DesignAttributeHandler;
+import com.vaadin.ui.declarative.DesignContext;
+import com.vaadin.ui.declarative.DesignException;
+import com.vaadin.ui.declarative.DesignFormatter;
+import com.vaadin.v7.data.Container;
+import com.vaadin.v7.data.Item;
+import com.vaadin.v7.data.Property;
+import com.vaadin.v7.data.Validator.InvalidValueException;
+import com.vaadin.v7.data.util.IndexedContainer;
+import com.vaadin.v7.data.util.converter.Converter;
+import com.vaadin.v7.data.util.converter.Converter.ConversionException;
+import com.vaadin.v7.data.util.converter.ConverterUtil;
+
+/**
+ * <p>
+ * A class representing a selection of items the user has selected in a UI. The
+ * set of choices is presented as a set of {@link com.com.vaadin.v7.data.Item}s
+ * in a {@link com.com.vaadin.v7.data.Container}.
+ * </p>
+ *
+ * <p>
+ * A <code>Select</code> component may be in single- or multiselect mode.
+ * Multiselect mode means that more than one item can be selected
+ * simultaneously.
+ * </p>
+ *
+ * @author Vaadin Ltd.
+ * @since 5.0
+ */
+@SuppressWarnings("serial")
+// TODO currently cannot specify type more precisely in case of multi-select
+public abstract class AbstractSelect extends AbstractField<Object>
+ implements Container, Container.Viewer,
+ Container.PropertySetChangeListener,
+ Container.PropertySetChangeNotifier, Container.ItemSetChangeNotifier,
+ Container.ItemSetChangeListener, LegacyComponent {
+
+ public enum ItemCaptionMode {
+ /**
+ * Item caption mode: Item's ID converted to a String using
+ * {@link VaadinSession#getConverterFactory()} is used as caption.
+ */
+ ID,
+ /**
+ * Item caption mode: Item's ID's <code>String</code> representation is
+ * used as caption.
+ *
+ * @since 7.5.6
+ */
+ ID_TOSTRING,
+ /**
+ * Item caption mode: Item's <code>String</code> representation is used
+ * as caption.
+ */
+ ITEM,
+ /**
+ * Item caption mode: Index of the item is used as caption. The index
+ * mode can only be used with the containers implementing the
+ * {@link com.vaadin.data.Container.Indexed} interface.
+ */
+ INDEX,
+ /**
+ * Item caption mode: If an Item has a caption it's used, if not, Item's
+ * ID converted to a String using
+ * {@link VaadinSession#getConverterFactory()} is used as caption.
+ * <b>This is the default</b>.
+ */
+ EXPLICIT_DEFAULTS_ID,
+ /**
+ * Item caption mode: Captions must be explicitly specified.
+ */
+ EXPLICIT,
+ /**
+ * Item caption mode: Only icons are shown, captions are hidden.
+ */
+ ICON_ONLY,
+ /**
+ * Item caption mode: Item captions are read from property specified
+ * with <code>setItemCaptionPropertyId</code>.
+ */
+ PROPERTY;
+ }
+
+ /**
+ * @deprecated As of 7.0, use {@link ItemCaptionMode#ID} instead
+ */
+ @Deprecated
+ public static final ItemCaptionMode ITEM_CAPTION_MODE_ID = ItemCaptionMode.ID;
+
+ /**
+ * @deprecated As of 7.0, use {@link ItemCaptionMode#ITEM} instead
+ */
+ @Deprecated
+ public static final ItemCaptionMode ITEM_CAPTION_MODE_ITEM = ItemCaptionMode.ITEM;
+
+ /**
+ * @deprecated As of 7.0, use {@link ItemCaptionMode#INDEX} instead
+ */
+ @Deprecated
+ public static final ItemCaptionMode ITEM_CAPTION_MODE_INDEX = ItemCaptionMode.INDEX;
+
+ /**
+ * @deprecated As of 7.0, use {@link ItemCaptionMode#EXPLICIT_DEFAULTS_ID}
+ * instead
+ */
+ @Deprecated
+ public static final ItemCaptionMode ITEM_CAPTION_MODE_EXPLICIT_DEFAULTS_ID = ItemCaptionMode.EXPLICIT_DEFAULTS_ID;
+
+ /**
+ * @deprecated As of 7.0, use {@link ItemCaptionMode#EXPLICIT} instead
+ */
+ @Deprecated
+ public static final ItemCaptionMode ITEM_CAPTION_MODE_EXPLICIT = ItemCaptionMode.EXPLICIT;
+
+ /**
+ * @deprecated As of 7.0, use {@link ItemCaptionMode#ICON_ONLY} instead
+ */
+ @Deprecated
+ public static final ItemCaptionMode ITEM_CAPTION_MODE_ICON_ONLY = ItemCaptionMode.ICON_ONLY;
+
+ /**
+ * @deprecated As of 7.0, use {@link ItemCaptionMode#PROPERTY} instead
+ */
+ @Deprecated
+ public static final ItemCaptionMode ITEM_CAPTION_MODE_PROPERTY = ItemCaptionMode.PROPERTY;
+
+ /**
+ * Interface for option filtering, used to filter options based on user
+ * entered value. The value is matched to the item caption.
+ * <code>FilteringMode.OFF</code> (0) turns the filtering off.
+ * <code>FilteringMode.STARTSWITH</code> (1) matches from the start of the
+ * caption. <code>FilteringMode.CONTAINS</code> (1) matches anywhere in the
+ * caption.
+ */
+ public interface Filtering extends Serializable {
+
+ /**
+ * @deprecated As of 7.0, use {@link FilteringMode#OFF} instead
+ */
+ @Deprecated
+ public static final FilteringMode FILTERINGMODE_OFF = FilteringMode.OFF;
+ /**
+ * @deprecated As of 7.0, use {@link FilteringMode#STARTSWITH} instead
+ */
+ @Deprecated
+ public static final FilteringMode FILTERINGMODE_STARTSWITH = FilteringMode.STARTSWITH;
+ /**
+ * @deprecated As of 7.0, use {@link FilteringMode#CONTAINS} instead
+ */
+ @Deprecated
+ public static final FilteringMode FILTERINGMODE_CONTAINS = FilteringMode.CONTAINS;
+
+ /**
+ * Sets the option filtering mode.
+ *
+ * @param filteringMode
+ * the filtering mode to use
+ */
+ public void setFilteringMode(FilteringMode filteringMode);
+
+ /**
+ * Gets the current filtering mode.
+ *
+ * @return the filtering mode in use
+ */
+ public FilteringMode getFilteringMode();
+
+ }
+
+ /**
+ * Select options.
+ */
+ protected Container items;
+
+ /**
+ * Is the user allowed to add new options?
+ */
+ private boolean allowNewOptions;
+
+ /**
+ * Keymapper used to map key values.
+ */
+ protected KeyMapper<Object> itemIdMapper = new KeyMapper<Object>();
+
+ /**
+ * Item icons.
+ */
+ private final HashMap<Object, Resource> itemIcons = new HashMap<Object, Resource>();
+
+ /**
+ * Item captions.
+ */
+ private final HashMap<Object, String> itemCaptions = new HashMap<Object, String>();
+
+ /**
+ * Item caption mode.
+ */
+ private ItemCaptionMode itemCaptionMode = ItemCaptionMode.EXPLICIT_DEFAULTS_ID;
+
+ /**
+ * Item caption source property id.
+ */
+ private Object itemCaptionPropertyId = null;
+
+ /**
+ * Item icon source property id.
+ */
+ private Object itemIconPropertyId = null;
+
+ /**
+ * List of property set change event listeners.
+ */
+ private Set<Container.PropertySetChangeListener> propertySetEventListeners = null;
+
+ /**
+ * List of item set change event listeners.
+ */
+ private Set<Container.ItemSetChangeListener> itemSetEventListeners = null;
+
+ /**
+ * Item id that represents null selection of this select.
+ *
+ * <p>
+ * Data interface does not support nulls as item ids. Selecting the item
+ * identified by this id is the same as selecting no items at all. This
+ * setting only affects the single select mode.
+ * </p>
+ */
+ private Object nullSelectionItemId = null;
+
+ // Null (empty) selection is enabled by default
+ private boolean nullSelectionAllowed = true;
+ private NewItemHandler newItemHandler;
+
+ // Caption (Item / Property) change listeners
+ CaptionChangeListener captionChangeListener;
+
+ /* Constructors */
+
+ /**
+ * Creates an empty Select. The caption is not used.
+ */
+ public AbstractSelect() {
+ setContainerDataSource(new IndexedContainer());
+ }
+
+ /**
+ * Creates an empty Select with caption.
+ */
+ public AbstractSelect(String caption) {
+ setContainerDataSource(new IndexedContainer());
+ setCaption(caption);
+ }
+
+ /**
+ * Creates a new select that is connected to a data-source.
+ *
+ * @param caption
+ * the Caption of the component.
+ * @param dataSource
+ * the Container datasource to be selected from by this select.
+ */
+ public AbstractSelect(String caption, Container dataSource) {
+ setCaption(caption);
+ setContainerDataSource(dataSource);
+ }
+
+ /**
+ * Creates a new select that is filled from a collection of option values.
+ *
+ * @param caption
+ * the Caption of this field.
+ * @param options
+ * the Collection containing the options.
+ */
+ public AbstractSelect(String caption, Collection<?> options) {
+
+ // Creates the options container and add given options to it
+ final Container c = new IndexedContainer();
+ if (options != null) {
+ for (final Iterator<?> i = options.iterator(); i.hasNext();) {
+ c.addItem(i.next());
+ }
+ }
+
+ setCaption(caption);
+ setContainerDataSource(c);
+ }
+
+ /* Component methods */
+
+ /**
+ * Paints the content of this component.
+ *
+ * @param target
+ * the Paint Event.
+ * @throws PaintException
+ * if the paint operation failed.
+ */
+ @Override
+ public void paintContent(PaintTarget target) throws PaintException {
+
+ // Paints select attributes
+ if (isNewItemsAllowed()) {
+ target.addAttribute("allownewitem", true);
+ }
+ if (isNullSelectionAllowed()) {
+ target.addAttribute("nullselect", true);
+ if (getNullSelectionItemId() != null) {
+ target.addAttribute("nullselectitem", true);
+ }
+ }
+
+ // Constructs selected keys array
+ String[] selectedKeys;
+ if (isMultiSelect()) {
+ selectedKeys = new String[((Set<?>) getValue()).size()];
+ } else {
+ selectedKeys = new String[(getValue() == null
+ && getNullSelectionItemId() == null ? 0 : 1)];
+ }
+
+ // ==
+ // first remove all previous item/property listeners
+ getCaptionChangeListener().clear();
+ // Paints the options and create array of selected id keys
+
+ target.startTag("options");
+ int keyIndex = 0;
+ // Support for external null selection item id
+ final Collection<?> ids = getItemIds();
+ if (isNullSelectionAllowed() && getNullSelectionItemId() != null
+ && !ids.contains(getNullSelectionItemId())) {
+ final Object id = getNullSelectionItemId();
+ // Paints option
+ target.startTag("so");
+ paintItem(target, id);
+ if (isSelected(id)) {
+ selectedKeys[keyIndex++] = itemIdMapper.key(id);
+ }
+ target.endTag("so");
+ }
+
+ final Iterator<?> i = getItemIds().iterator();
+ // Paints the available selection options from data source
+ while (i.hasNext()) {
+ // Gets the option attribute values
+ final Object id = i.next();
+ if (!isNullSelectionAllowed() && id != null
+ && id.equals(getNullSelectionItemId())) {
+ // Remove item if it's the null selection item but null
+ // selection is not allowed
+ continue;
+ }
+ final String key = itemIdMapper.key(id);
+ // add listener for each item, to cause repaint if an item changes
+ getCaptionChangeListener().addNotifierForItem(id);
+ target.startTag("so");
+ paintItem(target, id);
+ if (isSelected(id) && keyIndex < selectedKeys.length) {
+ selectedKeys[keyIndex++] = key;
+ }
+ target.endTag("so");
+ }
+ target.endTag("options");
+ // ==
+
+ // Paint variables
+ target.addVariable(this, "selected", selectedKeys);
+ if (isNewItemsAllowed()) {
+ target.addVariable(this, "newitem", "");
+ }
+
+ }
+
+ protected void paintItem(PaintTarget target, Object itemId)
+ throws PaintException {
+ final String key = itemIdMapper.key(itemId);
+ final String caption = getItemCaption(itemId);
+ final Resource icon = getItemIcon(itemId);
+ if (icon != null) {
+ target.addAttribute("icon", icon);
+ }
+ target.addAttribute("caption", caption);
+ if (itemId != null && itemId.equals(getNullSelectionItemId())) {
+ target.addAttribute("nullselection", true);
+ }
+ target.addAttribute("key", key);
+ if (isSelected(itemId)) {
+ target.addAttribute("selected", true);
+ }
+ }
+
+ /**
+ * Invoked when the value of a variable has changed.
+ *
+ * @see com.vaadin.ui.AbstractComponent#changeVariables(java.lang.Object,
+ * java.util.Map)
+ */
+ @Override
+ public void changeVariables(Object source, Map<String, Object> variables) {
+
+ // New option entered (and it is allowed)
+ if (isNewItemsAllowed()) {
+ final String newitem = (String) variables.get("newitem");
+ if (newitem != null && newitem.length() > 0) {
+ getNewItemHandler().addNewItem(newitem);
+ }
+ }
+
+ // Selection change
+ if (variables.containsKey("selected")) {
+ final String[] clientSideSelectedKeys = (String[]) variables
+ .get("selected");
+
+ // Multiselect mode
+ if (isMultiSelect()) {
+
+ // TODO Optimize by adding repaintNotNeeded when applicable
+
+ // Converts the key-array to id-set
+ final LinkedList<Object> acceptedSelections = new LinkedList<Object>();
+ for (int i = 0; i < clientSideSelectedKeys.length; i++) {
+ final Object id = itemIdMapper
+ .get(clientSideSelectedKeys[i]);
+ if (!isNullSelectionAllowed()
+ && (id == null || id == getNullSelectionItemId())) {
+ // skip empty selection if nullselection is not allowed
+ markAsDirty();
+ } else if (id != null && containsId(id)) {
+ acceptedSelections.add(id);
+ }
+ }
+
+ if (!isNullSelectionAllowed()
+ && acceptedSelections.size() < 1) {
+ // empty selection not allowed, keep old value
+ markAsDirty();
+ return;
+ }
+
+ // Limits the deselection to the set of visible items
+ // (non-visible items can not be deselected)
+ Collection<?> visibleNotSelected = getVisibleItemIds();
+ if (visibleNotSelected != null) {
+ visibleNotSelected = new HashSet<Object>(
+ visibleNotSelected);
+ // Don't remove those that will be added to preserve order
+ visibleNotSelected.removeAll(acceptedSelections);
+
+ @SuppressWarnings("unchecked")
+ Set<Object> newsel = (Set<Object>) getValue();
+ if (newsel == null) {
+ newsel = new LinkedHashSet<Object>();
+ } else {
+ newsel = new LinkedHashSet<Object>(newsel);
+ }
+ newsel.removeAll(visibleNotSelected);
+ newsel.addAll(acceptedSelections);
+ setValue(newsel, true);
+ }
+ } else {
+ // Single select mode
+ if (!isNullSelectionAllowed()
+ && (clientSideSelectedKeys.length == 0
+ || clientSideSelectedKeys[0] == null
+ || clientSideSelectedKeys[0] == getNullSelectionItemId())) {
+ markAsDirty();
+ return;
+ }
+ if (clientSideSelectedKeys.length == 0) {
+ // Allows deselection only if the deselected item is
+ // visible
+ final Object current = getValue();
+ final Collection<?> visible = getVisibleItemIds();
+ if (visible != null && visible.contains(current)) {
+ setValue(null, true);
+ }
+ } else {
+ String clientSelectedKey = clientSideSelectedKeys[0];
+ if ("null".equals(clientSelectedKey)
+ || itemIdMapper.containsKey(clientSelectedKey)) {
+ // Happens to work for nullselection
+ // (get ("null") -> null))
+ final Object id = itemIdMapper.get(clientSelectedKey);
+
+ if (!isNullSelectionAllowed() && id == null) {
+ markAsDirty();
+ } else if (id != null
+ && id.equals(getNullSelectionItemId())) {
+ setValue(null, true);
+ } else {
+ setValue(id, true);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * TODO refine doc Setter for new item handler that is called when user adds
+ * new item in newItemAllowed mode.
+ *
+ * @param newItemHandler
+ */
+ public void setNewItemHandler(NewItemHandler newItemHandler) {
+ this.newItemHandler = newItemHandler;
+ }
+
+ /**
+ * TODO refine doc
+ *
+ * @return
+ */
+ public NewItemHandler getNewItemHandler() {
+ if (newItemHandler == null) {
+ newItemHandler = new DefaultNewItemHandler();
+ }
+ return newItemHandler;
+ }
+
+ public interface NewItemHandler extends Serializable {
+ void addNewItem(String newItemCaption);
+ }
+
+ /**
+ * TODO refine doc
+ *
+ * This is a default class that handles adding new items that are typed by
+ * user to selects container.
+ *
+ * By extending this class one may implement some logic on new item addition
+ * like database inserts.
+ *
+ */
+ public class DefaultNewItemHandler implements NewItemHandler {
+ @Override
+ public void addNewItem(String newItemCaption) {
+ // Checks for readonly
+ if (isReadOnly()) {
+ throw new Property.ReadOnlyException();
+ }
+
+ // Adds new option
+ if (addItem(newItemCaption) != null) {
+
+ // Sets the caption property, if used
+ if (getItemCaptionPropertyId() != null) {
+ getContainerProperty(newItemCaption,
+ getItemCaptionPropertyId())
+ .setValue(newItemCaption);
+ }
+ if (isMultiSelect()) {
+ Set values = new HashSet((Collection) getValue());
+ values.add(newItemCaption);
+ setValue(values);
+ } else {
+ setValue(newItemCaption);
+ }
+ }
+ }
+ }
+
+ /**
+ * Gets the visible item ids. In Select, this returns list of all item ids,
+ * but can be overriden in subclasses if they paint only part of the items
+ * to the terminal or null if no items is visible.
+ */
+ public Collection<?> getVisibleItemIds() {
+ return getItemIds();
+ }
+
+ /* Property methods */
+
+ /**
+ * Returns the type of the property. <code>getValue</code> and
+ * <code>setValue</code> methods must be compatible with this type: one can
+ * safely cast <code>getValue</code> to given type and pass any variable
+ * assignable to this type as a parameter to <code>setValue</code>.
+ *
+ * @return the Type of the property.
+ */
+ @Override
+ public Class<?> getType() {
+ if (isMultiSelect()) {
+ return Set.class;
+ } else {
+ return Object.class;
+ }
+ }
+
+ /**
+ * Gets the selected item id or in multiselect mode a set of selected ids.
+ *
+ * @see com.vaadin.v7.ui.AbstractField#getValue()
+ */
+ @Override
+ public Object getValue() {
+ final Object retValue = super.getValue();
+
+ if (isMultiSelect()) {
+
+ // If the return value is not a set
+ if (retValue == null) {
+ return new HashSet<Object>();
+ }
+ if (retValue instanceof Set) {
+ return Collections.unmodifiableSet((Set<?>) retValue);
+ } else if (retValue instanceof Collection) {
+ return new HashSet<Object>((Collection<?>) retValue);
+ } else {
+ final Set<Object> s = new HashSet<Object>();
+ if (items.containsId(retValue)) {
+ s.add(retValue);
+ }
+ return s;
+ }
+
+ } else {
+ return retValue;
+ }
+ }
+
+ /**
+ * Sets the visible value of the property.
+ *
+ * <p>
+ * The value of the select is the selected item id. If the select is in
+ * multiselect-mode, the value is a set of selected item keys. In
+ * multiselect mode all collections of id:s can be assigned.
+ * </p>
+ *
+ * @param newValue
+ * the New selected item or collection of selected items.
+ * @see com.vaadin.v7.ui.AbstractField#setValue(java.lang.Object)
+ */
+ @Override
+ public void setValue(Object newValue) throws Property.ReadOnlyException {
+ if (newValue == getNullSelectionItemId()) {
+ newValue = null;
+ }
+
+ setValue(newValue, false);
+ }
+
+ /**
+ * Sets the visible value of the property.
+ *
+ * <p>
+ * The value of the select is the selected item id. If the select is in
+ * multiselect-mode, the value is a set of selected item keys. In
+ * multiselect mode all collections of id:s can be assigned.
+ * </p>
+ *
+ * @since 7.5.7
+ * @param newValue
+ * the New selected item or collection of selected items.
+ * @param repaintIsNotNeeded
+ * True if caller is sure that repaint is not needed.
+ * @param ignoreReadOnly
+ * True if read-only check should be omitted.
+ * @see com.vaadin.v7.ui.AbstractField#setValue(java.lang.Object,
+ * java.lang.Boolean)
+ */
+ @Override
+ protected void setValue(Object newFieldValue, boolean repaintIsNotNeeded,
+ boolean ignoreReadOnly)
+ throws com.vaadin.v7.data.Property.ReadOnlyException,
+ ConversionException, InvalidValueException {
+ if (isMultiSelect()) {
+ if (newFieldValue == null) {
+ super.setValue(new LinkedHashSet<Object>(), repaintIsNotNeeded,
+ ignoreReadOnly);
+ } else if (Collection.class
+ .isAssignableFrom(newFieldValue.getClass())) {
+ super.setValue(
+ new LinkedHashSet<Object>(
+ (Collection<?>) newFieldValue),
+ repaintIsNotNeeded, ignoreReadOnly);
+ }
+ } else if (newFieldValue == null || items.containsId(newFieldValue)) {
+ super.setValue(newFieldValue, repaintIsNotNeeded, ignoreReadOnly);
+ }
+ }
+
+ /* Container methods */
+
+ /**
+ * Gets the item from the container with given id. If the container does not
+ * contain the requested item, null is returned.
+ *
+ * @param itemId
+ * the item id.
+ * @return the item from the container.
+ */
+ @Override
+ public Item getItem(Object itemId) {
+ return items.getItem(itemId);
+ }
+
+ /**
+ * Gets the item Id collection from the container.
+ *
+ * @return the Collection of item ids.
+ */
+ @Override
+ public Collection<?> getItemIds() {
+ return items.getItemIds();
+ }
+
+ /**
+ * Gets the property Id collection from the container.
+ *
+ * @return the Collection of property ids.
+ */
+ @Override
+ public Collection<?> getContainerPropertyIds() {
+ return items.getContainerPropertyIds();
+ }
+
+ /**
+ * Gets the property type.
+ *
+ * @param propertyId
+ * the Id identifying the property.
+ * @see com.com.vaadin.v7.data.Container#getType(java.lang.Object)
+ */
+ @Override
+ public Class<?> getType(Object propertyId) {
+ return items.getType(propertyId);
+ }
+
+ /*
+ * Gets the number of items in the container.
+ *
+ * @return the Number of items in the container.
+ *
+ * @see com.vaadin.data.Container#size()
+ */
+ @Override
+ public int size() {
+ int size = items.size();
+ assert size >= 0;
+ return size;
+ }
+
+ /**
+ * Tests, if the collection contains an item with given id.
+ *
+ * @param itemId
+ * the Id the of item to be tested.
+ */
+ @Override
+ public boolean containsId(Object itemId) {
+ if (itemId != null) {
+ return items.containsId(itemId);
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Gets the Property identified by the given itemId and propertyId from the
+ * Container
+ *
+ * @see com.com.vaadin.v7.data.Container#getContainerProperty(Object,
+ * Object)
+ */
+ @Override
+ public Property getContainerProperty(Object itemId, Object propertyId) {
+ return items.getContainerProperty(itemId, propertyId);
+ }
+
+ /**
+ * Adds the new property to all items. Adds a property with given id, type
+ * and default value to all items in the container.
+ *
+ * This functionality is optional. If the function is unsupported, it always
+ * returns false.
+ *
+ * @return True if the operation succeeded.
+ * @see com.com.vaadin.v7.data.Container#addContainerProperty(java.lang.Object,
+ * java.lang.Class, java.lang.Object)
+ */
+ @Override
+ public boolean addContainerProperty(Object propertyId, Class<?> type,
+ Object defaultValue) throws UnsupportedOperationException {
+
+ final boolean retval = items.addContainerProperty(propertyId, type,
+ defaultValue);
+ if (retval && !(items instanceof Container.PropertySetChangeNotifier)) {
+ firePropertySetChange();
+ }
+ return retval;
+ }
+
+ /**
+ * Removes all items from the container.
+ *
+ * This functionality is optional. If the function is unsupported, it always
+ * returns false.
+ *
+ * @return True if the operation succeeded.
+ * @see com.com.vaadin.v7.data.Container#removeAllItems()
+ */
+ @Override
+ public boolean removeAllItems() throws UnsupportedOperationException {
+
+ final boolean retval = items.removeAllItems();
+ itemIdMapper.removeAll();
+ if (retval) {
+ setValue(null);
+ if (!(items instanceof Container.ItemSetChangeNotifier)) {
+ fireItemSetChange();
+ }
+ }
+ return retval;
+ }
+
+ /**
+ * Creates a new item into container with container managed id. The id of
+ * the created new item is returned. The item can be fetched with getItem()
+ * method. if the creation fails, null is returned.
+ *
+ * @return the Id of the created item or null in case of failure.
+ * @see com.com.vaadin.v7.data.Container#addItem()
+ */
+ @Override
+ public Object addItem() throws UnsupportedOperationException {
+
+ final Object retval = items.addItem();
+ if (retval != null
+ && !(items instanceof Container.ItemSetChangeNotifier)) {
+ fireItemSetChange();
+ }
+ return retval;
+ }
+
+ /**
+ * Create a new item into container. The created new item is returned and
+ * ready for setting property values. if the creation fails, null is
+ * returned. In case the container already contains the item, null is
+ * returned.
+ *
+ * This functionality is optional. If the function is unsupported, it always
+ * returns null.
+ *
+ * @param itemId
+ * the Identification of the item to be created.
+ * @return the Created item with the given id, or null in case of failure.
+ * @see com.com.vaadin.v7.data.Container#addItem(java.lang.Object)
+ */
+ @Override
+ public Item addItem(Object itemId) throws UnsupportedOperationException {
+
+ final Item retval = items.addItem(itemId);
+ if (retval != null
+ && !(items instanceof Container.ItemSetChangeNotifier)) {
+ fireItemSetChange();
+ }
+ return retval;
+ }
+
+ /**
+ * Adds given items with given item ids to container.
+ *
+ * @since 7.2
+ * @param itemId
+ * item identifiers to be added to underlying container
+ * @throws UnsupportedOperationException
+ * if the underlying container don't support adding items with
+ * identifiers
+ */
+ public void addItems(Object... itemId)
+ throws UnsupportedOperationException {
+ for (Object id : itemId) {
+ addItem(id);
+ }
+ }
+
+ /**
+ * Adds given items with given item ids to container.
+ *
+ * @since 7.2
+ * @param itemIds
+ * item identifiers to be added to underlying container
+ * @throws UnsupportedOperationException
+ * if the underlying container don't support adding items with
+ * identifiers
+ */
+ public void addItems(Collection<?> itemIds)
+ throws UnsupportedOperationException {
+ addItems(itemIds.toArray());
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container#removeItem(java.lang.Object)
+ */
+ @Override
+ public boolean removeItem(Object itemId)
+ throws UnsupportedOperationException {
+
+ unselect(itemId);
+ final boolean retval = items.removeItem(itemId);
+ itemIdMapper.remove(itemId);
+ if (retval && !(items instanceof Container.ItemSetChangeNotifier)) {
+ fireItemSetChange();
+ }
+ return retval;
+ }
+
+ /**
+ * Checks that the current selection is valid, i.e. the selected item ids
+ * exist in the container. Updates the selection if one or several selected
+ * item ids are no longer available in the container.
+ */
+ @SuppressWarnings("unchecked")
+ public void sanitizeSelection() {
+ Object value = getValue();
+ if (value == null) {
+ return;
+ }
+
+ boolean changed = false;
+
+ if (isMultiSelect()) {
+ Collection<Object> valueAsCollection = (Collection<Object>) value;
+ List<Object> newSelection = new ArrayList<Object>(
+ valueAsCollection.size());
+ for (Object subValue : valueAsCollection) {
+ if (containsId(subValue)) {
+ newSelection.add(subValue);
+ } else {
+ changed = true;
+ }
+ }
+ if (changed) {
+ setValue(newSelection);
+ }
+ } else {
+ if (!containsId(value)) {
+ setValue(null);
+ }
+ }
+
+ }
+
+ /**
+ * Removes the property from all items. Removes a property with given id
+ * from all the items in the container.
+ *
+ * This functionality is optional. If the function is unsupported, it always
+ * returns false.
+ *
+ * @return True if the operation succeeded.
+ * @see com.com.vaadin.v7.data.Container#removeContainerProperty(java.lang.Object)
+ */
+ @Override
+ public boolean removeContainerProperty(Object propertyId)
+ throws UnsupportedOperationException {
+
+ final boolean retval = items.removeContainerProperty(propertyId);
+ if (retval && !(items instanceof Container.PropertySetChangeNotifier)) {
+ firePropertySetChange();
+ }
+ return retval;
+ }
+
+ /* Container.Viewer methods */
+
+ /**
+ * Sets the Container that serves as the data source of the viewer.
+ *
+ * As a side-effect the fields value (selection) is set to null due old
+ * selection not necessary exists in new Container.
+ *
+ * @see com.com.vaadin.v7.data.Container.Viewer#setContainerDataSource(Container)
+ *
+ * @param newDataSource
+ * the new data source.
+ */
+ @Override
+ public void setContainerDataSource(Container newDataSource) {
+ if (newDataSource == null) {
+ newDataSource = new IndexedContainer();
+ }
+
+ getCaptionChangeListener().clear();
+
+ if (items != newDataSource) {
+
+ // Removes listeners from the old datasource
+ if (items != null) {
+ if (items instanceof Container.ItemSetChangeNotifier) {
+ ((Container.ItemSetChangeNotifier) items)
+ .removeItemSetChangeListener(this);
+ }
+ if (items instanceof Container.PropertySetChangeNotifier) {
+ ((Container.PropertySetChangeNotifier) items)
+ .removePropertySetChangeListener(this);
+ }
+ }
+
+ // Assigns new data source
+ items = newDataSource;
+
+ // Clears itemIdMapper also
+ itemIdMapper.removeAll();
+
+ // Adds listeners
+ if (items != null) {
+ if (items instanceof Container.ItemSetChangeNotifier) {
+ ((Container.ItemSetChangeNotifier) items)
+ .addItemSetChangeListener(this);
+ }
+ if (items instanceof Container.PropertySetChangeNotifier) {
+ ((Container.PropertySetChangeNotifier) items)
+ .addPropertySetChangeListener(this);
+ }
+ }
+
+ /*
+ * We expect changing the data source should also clean value. See
+ * #810, #4607, #5281
+ */
+ setValue(null);
+
+ markAsDirty();
+
+ }
+ }
+
+ /**
+ * Gets the viewing data-source container.
+ *
+ * @see com.com.vaadin.v7.data.Container.Viewer#getContainerDataSource()
+ */
+ @Override
+ public Container getContainerDataSource() {
+ return items;
+ }
+
+ /* Select attributes */
+
+ /**
+ * Is the select in multiselect mode? In multiselect mode
+ *
+ * @return the Value of property multiSelect.
+ */
+ public boolean isMultiSelect() {
+ return getState(false).multiSelect;
+ }
+
+ /**
+ * Sets the multiselect mode. Setting multiselect mode false may lose
+ * selection information: if selected items set contains one or more
+ * selected items, only one of the selected items is kept as selected.
+ *
+ * Subclasses of AbstractSelect can choose not to support changing the
+ * multiselect mode, and may throw {@link UnsupportedOperationException}.
+ *
+ * @param multiSelect
+ * the New value of property multiSelect.
+ */
+ public void setMultiSelect(boolean multiSelect) {
+ if (multiSelect && getNullSelectionItemId() != null) {
+ throw new IllegalStateException(
+ "Multiselect and NullSelectionItemId can not be set at the same time.");
+ }
+ if (multiSelect != getState(false).multiSelect) {
+
+ // Selection before mode change
+ final Object oldValue = getValue();
+
+ getState().multiSelect = multiSelect;
+
+ // Convert the value type
+ if (multiSelect) {
+ final Set<Object> s = new HashSet<Object>();
+ if (oldValue != null) {
+ s.add(oldValue);
+ }
+ setValue(s);
+ } else {
+ final Set<?> s = (Set<?>) oldValue;
+ if (s == null || s.isEmpty()) {
+ setValue(null);
+ } else {
+ // Set the single select to contain only the first
+ // selected value in the multiselect
+ setValue(s.iterator().next());
+ }
+ }
+
+ markAsDirty();
+ }
+ }
+
+ /**
+ * Does the select allow adding new options by the user. If true, the new
+ * options can be added to the Container. The text entered by the user is
+ * used as id. Note that data-source must allow adding new items.
+ *
+ * @return True if additions are allowed.
+ */
+ public boolean isNewItemsAllowed() {
+ return allowNewOptions;
+ }
+
+ /**
+ * Enables or disables possibility to add new options by the user.
+ *
+ * @param allowNewOptions
+ * the New value of property allowNewOptions.
+ */
+ public void setNewItemsAllowed(boolean allowNewOptions) {
+
+ // Only handle change requests
+ if (this.allowNewOptions != allowNewOptions) {
+
+ this.allowNewOptions = allowNewOptions;
+
+ markAsDirty();
+ }
+ }
+
+ /**
+ * Override the caption of an item. Setting caption explicitly overrides id,
+ * item and index captions.
+ *
+ * @param itemId
+ * the id of the item to be recaptioned.
+ * @param caption
+ * the New caption.
+ */
+ public void setItemCaption(Object itemId, String caption) {
+ if (itemId != null) {
+ itemCaptions.put(itemId, caption);
+ markAsDirty();
+ }
+ }
+
+ /**
+ * Gets the caption of an item. The caption is generated as specified by the
+ * item caption mode. See <code>setItemCaptionMode()</code> for more
+ * details.
+ *
+ * @param itemId
+ * the id of the item to be queried.
+ * @return the caption for specified item.
+ */
+ public String getItemCaption(Object itemId) {
+
+ // Null items can not be found
+ if (itemId == null) {
+ return null;
+ }
+
+ String caption = null;
+
+ switch (getItemCaptionMode()) {
+
+ case ID:
+ caption = idToCaption(itemId);
+ break;
+ case ID_TOSTRING:
+ caption = itemId.toString();
+ break;
+ case INDEX:
+ if (items instanceof Container.Indexed) {
+ caption = String
+ .valueOf(((Container.Indexed) items).indexOfId(itemId));
+ } else {
+ caption = "ERROR: Container is not indexed";
+ }
+ break;
+
+ case ITEM:
+ final Item i = getItem(itemId);
+ if (i != null) {
+ caption = i.toString();
+ }
+ break;
+
+ case EXPLICIT:
+ caption = itemCaptions.get(itemId);
+ break;
+
+ case EXPLICIT_DEFAULTS_ID:
+ caption = itemCaptions.get(itemId);
+ if (caption == null) {
+ caption = idToCaption(itemId);
+ }
+ break;
+
+ case PROPERTY:
+ final Property<?> p = getContainerProperty(itemId,
+ getItemCaptionPropertyId());
+ if (p != null) {
+ Object value = p.getValue();
+ if (value != null) {
+ caption = value.toString();
+ }
+ }
+ break;
+ }
+
+ // All items must have some captions
+ return caption != null ? caption : "";
+ }
+
+ private String idToCaption(Object itemId) {
+ try {
+ Converter<String, Object> c = (Converter<String, Object>) ConverterUtil
+ .getConverter(String.class, itemId.getClass(),
+ getSession());
+ return ConverterUtil.convertFromModel(itemId, String.class, c,
+ getLocale());
+ } catch (Exception e) {
+ return itemId.toString();
+ }
+ }
+
+ /**
+ * Sets the icon for an item.
+ *
+ * @param itemId
+ * the id of the item to be assigned an icon.
+ * @param icon
+ * the icon to use or null.
+ */
+ public void setItemIcon(Object itemId, Resource icon) {
+ if (itemId != null) {
+ if (icon == null) {
+ itemIcons.remove(itemId);
+ } else {
+ itemIcons.put(itemId, icon);
+ }
+ markAsDirty();
+ }
+ }
+
+ /**
+ * Gets the item icon.
+ *
+ * @param itemId
+ * the id of the item to be assigned an icon.
+ * @return the icon for the item or null, if not specified.
+ */
+ public Resource getItemIcon(Object itemId) {
+ final Resource explicit = itemIcons.get(itemId);
+ if (explicit != null) {
+ return explicit;
+ }
+
+ if (getItemIconPropertyId() == null) {
+ return null;
+ }
+
+ final Property<?> ip = getContainerProperty(itemId,
+ getItemIconPropertyId());
+ if (ip == null) {
+ return null;
+ }
+ final Object icon = ip.getValue();
+ if (icon instanceof Resource) {
+ return (Resource) icon;
+ }
+
+ return null;
+ }
+
+ /**
+ * Sets the item caption mode.
+ *
+ * See {@link ItemCaptionMode} for a description of the modes.
+ * <p>
+ * {@link ItemCaptionMode#EXPLICIT_DEFAULTS_ID} is the default mode.
+ * </p>
+ *
+ * @param mode
+ * the One of the modes listed above.
+ */
+ public void setItemCaptionMode(ItemCaptionMode mode) {
+ if (mode != null) {
+ itemCaptionMode = mode;
+ markAsDirty();
+ }
+ }
+
+ /**
+ * Gets the item caption mode.
+ *
+ * <p>
+ * The mode can be one of the following ones:
+ * <ul>
+ * <li><code>ITEM_CAPTION_MODE_EXPLICIT_DEFAULTS_ID</code> : Items
+ * Id-objects <code>toString</code> is used as item caption. If caption is
+ * explicitly specified, it overrides the id-caption.
+ * <li><code>ITEM_CAPTION_MODE_ID</code> : Items Id-objects
+ * <code>toString</code> is used as item caption.</li>
+ * <li><code>ITEM_CAPTION_MODE_ITEM</code> : Item-objects
+ * <code>toString</code> is used as item caption.</li>
+ * <li><code>ITEM_CAPTION_MODE_INDEX</code> : The index of the item is used
+ * as item caption. The index mode can only be used with the containers
+ * implementing <code>Container.Indexed</code> interface.</li>
+ * <li><code>ITEM_CAPTION_MODE_EXPLICIT</code> : The item captions must be
+ * explicitly specified.</li>
+ * <li><code>ITEM_CAPTION_MODE_PROPERTY</code> : The item captions are read
+ * from property, that must be specified with
+ * <code>setItemCaptionPropertyId</code>.</li>
+ * </ul>
+ * The <code>ITEM_CAPTION_MODE_EXPLICIT_DEFAULTS_ID</code> is the default
+ * mode.
+ * </p>
+ *
+ * @return the One of the modes listed above.
+ */
+ public ItemCaptionMode getItemCaptionMode() {
+ return itemCaptionMode;
+ }
+
+ /**
+ * Sets the item caption property.
+ *
+ * <p>
+ * Setting the id to a existing property implicitly sets the item caption
+ * mode to <code>ITEM_CAPTION_MODE_PROPERTY</code>. If the object is in
+ * <code>ITEM_CAPTION_MODE_PROPERTY</code> mode, setting caption property id
+ * null resets the item caption mode to
+ * <code>ITEM_CAPTION_EXPLICIT_DEFAULTS_ID</code>.
+ * </p>
+ * <p>
+ * Note that the type of the property used for caption must be String
+ * </p>
+ * <p>
+ * Setting the property id to null disables this feature. The id is null by
+ * default
+ * </p>
+ * .
+ *
+ * @param propertyId
+ * the id of the property.
+ *
+ */
+ public void setItemCaptionPropertyId(Object propertyId) {
+ if (propertyId != null) {
+ itemCaptionPropertyId = propertyId;
+ setItemCaptionMode(ITEM_CAPTION_MODE_PROPERTY);
+ markAsDirty();
+ } else {
+ itemCaptionPropertyId = null;
+ if (getItemCaptionMode() == ITEM_CAPTION_MODE_PROPERTY) {
+ setItemCaptionMode(ITEM_CAPTION_MODE_EXPLICIT_DEFAULTS_ID);
+ }
+ markAsDirty();
+ }
+ }
+
+ /**
+ * Gets the item caption property.
+ *
+ * @return the Id of the property used as item caption source.
+ */
+ public Object getItemCaptionPropertyId() {
+ return itemCaptionPropertyId;
+ }
+
+ /**
+ * Sets the item icon property.
+ *
+ * <p>
+ * If the property id is set to a valid value, each item is given an icon
+ * got from the given property of the items. The type of the property must
+ * be assignable to Resource.
+ * </p>
+ *
+ * <p>
+ * Note : The icons set with <code>setItemIcon</code> function override the
+ * icons from the property.
+ * </p>
+ *
+ * <p>
+ * Setting the property id to null disables this feature. The id is null by
+ * default
+ * </p>
+ * .
+ *
+ * @param propertyId
+ * the id of the property that specifies icons for items or null
+ * @throws IllegalArgumentException
+ * If the propertyId is not in the container or is not of a
+ * valid type
+ */
+ public void setItemIconPropertyId(Object propertyId)
+ throws IllegalArgumentException {
+ if (propertyId == null) {
+ itemIconPropertyId = null;
+ } else if (!getContainerPropertyIds().contains(propertyId)) {
+ throw new IllegalArgumentException(
+ "Property id not found in the container");
+ } else if (Resource.class.isAssignableFrom(getType(propertyId))) {
+ itemIconPropertyId = propertyId;
+ } else {
+ throw new IllegalArgumentException(
+ "Property type must be assignable to Resource");
+ }
+ markAsDirty();
+ }
+
+ /**
+ * Gets the item icon property.
+ *
+ * <p>
+ * If the property id is set to a valid value, each item is given an icon
+ * got from the given property of the items. The type of the property must
+ * be assignable to Icon.
+ * </p>
+ *
+ * <p>
+ * Note : The icons set with <code>setItemIcon</code> function override the
+ * icons from the property.
+ * </p>
+ *
+ * <p>
+ * Setting the property id to null disables this feature. The id is null by
+ * default
+ * </p>
+ * .
+ *
+ * @return the Id of the property containing the item icons.
+ */
+ public Object getItemIconPropertyId() {
+ return itemIconPropertyId;
+ }
+
+ /**
+ * Tests if an item is selected.
+ *
+ * <p>
+ * In single select mode testing selection status of the item identified by
+ * {@link #getNullSelectionItemId()} returns true if the value of the
+ * property is null.
+ * </p>
+ *
+ * @param itemId
+ * the Id the of the item to be tested.
+ * @see #getNullSelectionItemId()
+ * @see #setNullSelectionItemId(Object)
+ *
+ */
+ public boolean isSelected(Object itemId) {
+ if (itemId == null) {
+ return false;
+ }
+ if (isMultiSelect()) {
+ return ((Set<?>) getValue()).contains(itemId);
+ } else {
+ final Object value = getValue();
+ return itemId
+ .equals(value == null ? getNullSelectionItemId() : value);
+ }
+ }
+
+ /**
+ * Selects an item.
+ *
+ * <p>
+ * In single select mode selecting item identified by
+ * {@link #getNullSelectionItemId()} sets the value of the property to null.
+ * </p>
+ *
+ * @param itemId
+ * the identifier of Item to be selected.
+ * @see #getNullSelectionItemId()
+ * @see #setNullSelectionItemId(Object)
+ *
+ */
+ public void select(Object itemId) {
+ if (!isMultiSelect()) {
+ setValue(itemId);
+ } else if (!isSelected(itemId) && itemId != null
+ && items.containsId(itemId)) {
+ final Set<Object> s = new HashSet<Object>((Set<?>) getValue());
+ s.add(itemId);
+ setValue(s);
+ }
+ }
+
+ /**
+ * Unselects an item.
+ *
+ * @param itemId
+ * the identifier of the Item to be unselected.
+ * @see #getNullSelectionItemId()
+ * @see #setNullSelectionItemId(Object)
+ *
+ */
+ public void unselect(Object itemId) {
+ if (isSelected(itemId)) {
+ if (isMultiSelect()) {
+ final Set<Object> s = new HashSet<Object>((Set<?>) getValue());
+ s.remove(itemId);
+ setValue(s);
+ } else {
+ setValue(null);
+ }
+ }
+ }
+
+ /**
+ * Notifies this listener that the Containers contents has changed.
+ *
+ * @see com.com.vaadin.v7.data.Container.PropertySetChangeListener#containerPropertySetChange(com.com.vaadin.v7.data.Container.PropertySetChangeEvent)
+ */
+ @Override
+ public void containerPropertySetChange(
+ Container.PropertySetChangeEvent event) {
+ firePropertySetChange();
+ }
+
+ /**
+ * Adds a new Property set change listener for this Container.
+ *
+ * @see com.com.vaadin.v7.data.Container.PropertySetChangeNotifier#addListener(com.com.vaadin.v7.data.Container.PropertySetChangeListener)
+ */
+ @Override
+ public void addPropertySetChangeListener(
+ Container.PropertySetChangeListener listener) {
+ if (propertySetEventListeners == null) {
+ propertySetEventListeners = new LinkedHashSet<Container.PropertySetChangeListener>();
+ }
+ propertySetEventListeners.add(listener);
+ }
+
+ /**
+ * @deprecated As of 7.0, replaced by
+ * {@link #addPropertySetChangeListener(com.com.vaadin.v7.data.Container.PropertySetChangeListener)}
+ **/
+ @Override
+ @Deprecated
+ public void addListener(Container.PropertySetChangeListener listener) {
+ addPropertySetChangeListener(listener);
+ }
+
+ /**
+ * Removes a previously registered Property set change listener.
+ *
+ * @see com.com.vaadin.v7.data.Container.PropertySetChangeNotifier#removeListener(com.com.vaadin.v7.data.Container.PropertySetChangeListener)
+ */
+ @Override
+ public void removePropertySetChangeListener(
+ Container.PropertySetChangeListener listener) {
+ if (propertySetEventListeners != null) {
+ propertySetEventListeners.remove(listener);
+ if (propertySetEventListeners.isEmpty()) {
+ propertySetEventListeners = null;
+ }
+ }
+ }
+
+ /**
+ * @deprecated As of 7.0, replaced by
+ * {@link #removePropertySetChangeListener(com.com.vaadin.v7.data.Container.PropertySetChangeListener)}
+ **/
+ @Override
+ @Deprecated
+ public void removeListener(Container.PropertySetChangeListener listener) {
+ removePropertySetChangeListener(listener);
+ }
+
+ /**
+ * Adds an Item set change listener for the object.
+ *
+ * @see com.com.vaadin.v7.data.Container.ItemSetChangeNotifier#addListener(com.com.vaadin.v7.data.Container.ItemSetChangeListener)
+ */
+ @Override
+ public void addItemSetChangeListener(
+ Container.ItemSetChangeListener listener) {
+ if (itemSetEventListeners == null) {
+ itemSetEventListeners = new LinkedHashSet<Container.ItemSetChangeListener>();
+ }
+ itemSetEventListeners.add(listener);
+ }
+
+ /**
+ * @deprecated As of 7.0, replaced by
+ * {@link #addItemSetChangeListener(com.com.vaadin.v7.data.Container.ItemSetChangeListener)}
+ **/
+ @Override
+ @Deprecated
+ public void addListener(Container.ItemSetChangeListener listener) {
+ addItemSetChangeListener(listener);
+ }
+
+ /**
+ * Removes the Item set change listener from the object.
+ *
+ * @see com.com.vaadin.v7.data.Container.ItemSetChangeNotifier#removeListener(com.com.vaadin.v7.data.Container.ItemSetChangeListener)
+ */
+ @Override
+ public void removeItemSetChangeListener(
+ Container.ItemSetChangeListener listener) {
+ if (itemSetEventListeners != null) {
+ itemSetEventListeners.remove(listener);
+ if (itemSetEventListeners.isEmpty()) {
+ itemSetEventListeners = null;
+ }
+ }
+ }
+
+ /**
+ * @deprecated As of 7.0, replaced by
+ * {@link #removeItemSetChangeListener(com.com.vaadin.v7.data.Container.ItemSetChangeListener)}
+ **/
+ @Override
+ @Deprecated
+ public void removeListener(Container.ItemSetChangeListener listener) {
+ removeItemSetChangeListener(listener);
+ }
+
+ @Override
+ public Collection<?> getListeners(Class<?> eventType) {
+ if (Container.ItemSetChangeEvent.class.isAssignableFrom(eventType)) {
+ if (itemSetEventListeners == null) {
+ return Collections.EMPTY_LIST;
+ } else {
+ return Collections
+ .unmodifiableCollection(itemSetEventListeners);
+ }
+ } else if (Container.PropertySetChangeEvent.class
+ .isAssignableFrom(eventType)) {
+ if (propertySetEventListeners == null) {
+ return Collections.EMPTY_LIST;
+ } else {
+ return Collections
+ .unmodifiableCollection(propertySetEventListeners);
+ }
+ }
+
+ return super.getListeners(eventType);
+ }
+
+ /**
+ * Lets the listener know a Containers Item set has changed.
+ *
+ * @see com.com.vaadin.v7.data.Container.ItemSetChangeListener#containerItemSetChange(com.com.vaadin.v7.data.Container.ItemSetChangeEvent)
+ */
+ @Override
+ public void containerItemSetChange(Container.ItemSetChangeEvent event) {
+ // Clears the item id mapping table
+ itemIdMapper.removeAll();
+
+ // Notify all listeners
+ fireItemSetChange();
+ }
+
+ /**
+ * Fires the property set change event.
+ */
+ protected void firePropertySetChange() {
+ if (propertySetEventListeners != null
+ && !propertySetEventListeners.isEmpty()) {
+ final Container.PropertySetChangeEvent event = new PropertySetChangeEvent(
+ this);
+ final Object[] listeners = propertySetEventListeners.toArray();
+ for (int i = 0; i < listeners.length; i++) {
+ ((Container.PropertySetChangeListener) listeners[i])
+ .containerPropertySetChange(event);
+ }
+ }
+ markAsDirty();
+ }
+
+ /**
+ * Fires the item set change event.
+ */
+ protected void fireItemSetChange() {
+ if (itemSetEventListeners != null && !itemSetEventListeners.isEmpty()) {
+ final Container.ItemSetChangeEvent event = new ItemSetChangeEvent(
+ this);
+ final Object[] listeners = itemSetEventListeners.toArray();
+ for (int i = 0; i < listeners.length; i++) {
+ ((Container.ItemSetChangeListener) listeners[i])
+ .containerItemSetChange(event);
+ }
+ }
+ markAsDirty();
+ }
+
+ /**
+ * Implementation of item set change event.
+ */
+ private static class ItemSetChangeEvent extends EventObject
+ implements Serializable, Container.ItemSetChangeEvent {
+
+ private ItemSetChangeEvent(Container source) {
+ super(source);
+ }
+
+ /**
+ * Gets the Property where the event occurred.
+ *
+ * @see com.com.vaadin.v7.data.Container.ItemSetChangeEvent#getContainer()
+ */
+ @Override
+ public Container getContainer() {
+ return (Container) getSource();
+ }
+
+ }
+
+ /**
+ * Implementation of property set change event.
+ */
+ private static class PropertySetChangeEvent extends EventObject
+ implements Container.PropertySetChangeEvent, Serializable {
+
+ private PropertySetChangeEvent(Container source) {
+ super(source);
+ }
+
+ /**
+ * Retrieves the Container whose contents have been modified.
+ *
+ * @see com.com.vaadin.v7.data.Container.PropertySetChangeEvent#getContainer()
+ */
+ @Override
+ public Container getContainer() {
+ return (Container) getSource();
+ }
+
+ }
+
+ /**
+ * For multi-selectable fields, also an empty collection of values is
+ * considered to be an empty field.
+ *
+ * @see LegacyAbstractField#isEmpty().
+ */
+ @Override
+ public boolean isEmpty() {
+ if (!isMultiSelect()) {
+ return super.isEmpty();
+ } else {
+ Object value = getValue();
+ return super.isEmpty() || (value instanceof Collection
+ && ((Collection<?>) value).isEmpty());
+ }
+ }
+
+ /**
+ * Allow or disallow empty selection by the user. If the select is in
+ * single-select mode, you can make an item represent the empty selection by
+ * calling <code>setNullSelectionItemId()</code>. This way you can for
+ * instance set an icon and caption for the null selection item.
+ *
+ * @param nullSelectionAllowed
+ * whether or not to allow empty selection
+ * @see #setNullSelectionItemId(Object)
+ * @see #isNullSelectionAllowed()
+ */
+ public void setNullSelectionAllowed(boolean nullSelectionAllowed) {
+ if (nullSelectionAllowed != this.nullSelectionAllowed) {
+ this.nullSelectionAllowed = nullSelectionAllowed;
+ markAsDirty();
+ }
+ }
+
+ /**
+ * Checks if null empty selection is allowed by the user.
+ *
+ * @return whether or not empty selection is allowed
+ * @see #setNullSelectionAllowed(boolean)
+ */
+ public boolean isNullSelectionAllowed() {
+ return nullSelectionAllowed;
+ }
+
+ /**
+ * Returns the item id that represents null value of this select in single
+ * select mode.
+ *
+ * <p>
+ * Data interface does not support nulls as item ids. Selecting the item
+ * identified by this id is the same as selecting no items at all. This
+ * setting only affects the single select mode.
+ * </p>
+ *
+ * @return the Object Null value item id.
+ * @see #setNullSelectionItemId(Object)
+ * @see #isSelected(Object)
+ * @see #select(Object)
+ */
+ public Object getNullSelectionItemId() {
+ return nullSelectionItemId;
+ }
+
+ /**
+ * Sets the item id that represents null value of this select.
+ *
+ * <p>
+ * Data interface does not support nulls as item ids. Selecting the item
+ * identified by this id is the same as selecting no items at all. This
+ * setting only affects the single select mode.
+ * </p>
+ *
+ * @param nullSelectionItemId
+ * the nullSelectionItemId to set.
+ * @see #getNullSelectionItemId()
+ * @see #isSelected(Object)
+ * @see #select(Object)
+ */
+ public void setNullSelectionItemId(Object nullSelectionItemId) {
+ if (nullSelectionItemId != null && isMultiSelect()) {
+ throw new IllegalStateException(
+ "Multiselect and NullSelectionItemId can not be set at the same time.");
+ }
+ this.nullSelectionItemId = nullSelectionItemId;
+ }
+
+ /**
+ * Notifies the component that it is connected to an application.
+ *
+ * @see com.vaadin.v7.ui.AbstractField#attach()
+ */
+ @Override
+ public void attach() {
+ super.attach();
+ }
+
+ /**
+ * Detaches the component from application.
+ *
+ * @see com.vaadin.ui.AbstractComponent#detach()
+ */
+ @Override
+ public void detach() {
+ getCaptionChangeListener().clear();
+ super.detach();
+ }
+
+ // Caption change listener
+ protected CaptionChangeListener getCaptionChangeListener() {
+ if (captionChangeListener == null) {
+ captionChangeListener = new CaptionChangeListener();
+ }
+ return captionChangeListener;
+ }
+
+ /**
+ * This is a listener helper for Item and Property changes that should cause
+ * a repaint. It should be attached to all items that are displayed, and the
+ * default implementation does this in paintContent(). Especially
+ * "lazyloading" components should take care to add and remove listeners as
+ * appropriate. Call addNotifierForItem() for each painted item (and
+ * remember to clear).
+ *
+ * NOTE: singleton, use getCaptionChangeListener().
+ *
+ */
+ protected class CaptionChangeListener implements
+ Item.PropertySetChangeListener, Property.ValueChangeListener {
+
+ // TODO clean this up - type is either Item.PropertySetChangeNotifier or
+ // Property.ValueChangeNotifier
+ HashSet<Object> captionChangeNotifiers = new HashSet<Object>();
+
+ public void addNotifierForItem(Object itemId) {
+ switch (getItemCaptionMode()) {
+ case ITEM:
+ final Item i = getItem(itemId);
+ if (i == null) {
+ return;
+ }
+ if (i instanceof Item.PropertySetChangeNotifier) {
+ ((Item.PropertySetChangeNotifier) i)
+ .addPropertySetChangeListener(
+ getCaptionChangeListener());
+ captionChangeNotifiers.add(i);
+ }
+ Collection<?> pids = i.getItemPropertyIds();
+ if (pids != null) {
+ for (Iterator<?> it = pids.iterator(); it.hasNext();) {
+ Property<?> p = i.getItemProperty(it.next());
+ if (p != null
+ && p instanceof Property.ValueChangeNotifier) {
+ ((Property.ValueChangeNotifier) p)
+ .addValueChangeListener(
+ getCaptionChangeListener());
+ captionChangeNotifiers.add(p);
+ }
+ }
+
+ }
+ break;
+ case PROPERTY:
+ final Property<?> p = getContainerProperty(itemId,
+ getItemCaptionPropertyId());
+ if (p != null && p instanceof Property.ValueChangeNotifier) {
+ ((Property.ValueChangeNotifier) p)
+ .addValueChangeListener(getCaptionChangeListener());
+ captionChangeNotifiers.add(p);
+ }
+ break;
+
+ }
+ if (getItemIconPropertyId() != null) {
+ final Property p = getContainerProperty(itemId,
+ getItemIconPropertyId());
+ if (p != null && p instanceof Property.ValueChangeNotifier) {
+ ((Property.ValueChangeNotifier) p)
+ .addValueChangeListener(getCaptionChangeListener());
+ captionChangeNotifiers.add(p);
+ }
+ }
+ }
+
+ public void clear() {
+ for (Iterator<Object> it = captionChangeNotifiers.iterator(); it
+ .hasNext();) {
+ Object notifier = it.next();
+ if (notifier instanceof Item.PropertySetChangeNotifier) {
+ ((Item.PropertySetChangeNotifier) notifier)
+ .removePropertySetChangeListener(
+ getCaptionChangeListener());
+ } else {
+ ((Property.ValueChangeNotifier) notifier)
+ .removeValueChangeListener(
+ getCaptionChangeListener());
+ }
+ }
+ captionChangeNotifiers.clear();
+ }
+
+ @Override
+ public void valueChange(
+ com.vaadin.v7.data.Property.ValueChangeEvent event) {
+ markAsDirty();
+ }
+
+ @Override
+ public void itemPropertySetChange(
+ com.vaadin.v7.data.Item.PropertySetChangeEvent event) {
+ markAsDirty();
+ }
+
+ }
+
+ /**
+ * Criterion which accepts a drop only if the drop target is (one of) the
+ * given Item identifier(s). Criterion can be used only on a drop targets
+ * that extends AbstractSelect like {@link Table} and {@link Tree}. The
+ * target and identifiers of valid Items are given in constructor.
+ *
+ * @since 6.3
+ */
+ public static class TargetItemIs extends AbstractItemSetCriterion {
+
+ /**
+ * @param select
+ * the select implementation that is used as a drop target
+ * @param itemId
+ * the identifier(s) that are valid drop locations
+ */
+ public TargetItemIs(AbstractSelect select, Object... itemId) {
+ super(select, itemId);
+ }
+
+ @Override
+ public boolean accept(DragAndDropEvent dragEvent) {
+ AbstractSelectTargetDetails dropTargetData = (AbstractSelectTargetDetails) dragEvent
+ .getTargetDetails();
+ if (dropTargetData.getTarget() != select) {
+ return false;
+ }
+ return itemIds.contains(dropTargetData.getItemIdOver());
+ }
+
+ }
+
+ /**
+ * Abstract helper class to implement item id based criterion.
+ *
+ * Note, inner class used not to open itemIdMapper for public access.
+ *
+ * @since 6.3
+ *
+ */
+ private static abstract class AbstractItemSetCriterion
+ extends ClientSideCriterion {
+ protected final Collection<Object> itemIds = new HashSet<Object>();
+ protected AbstractSelect select;
+
+ public AbstractItemSetCriterion(AbstractSelect select,
+ Object... itemId) {
+ if (itemIds == null || select == null) {
+ throw new IllegalArgumentException(
+ "Accepted item identifiers must be accepted.");
+ }
+ Collections.addAll(itemIds, itemId);
+ this.select = select;
+ }
+
+ @Override
+ public void paintContent(PaintTarget target) throws PaintException {
+ super.paintContent(target);
+ String[] keys = new String[itemIds.size()];
+ int i = 0;
+ for (Object itemId : itemIds) {
+ String key = select.itemIdMapper.key(itemId);
+ keys[i++] = key;
+ }
+ target.addAttribute("keys", keys);
+ target.addAttribute("s", select);
+ }
+
+ }
+
+ /**
+ * This criterion accepts a only a {@link Transferable} that contains given
+ * Item (practically its identifier) from a specific AbstractSelect.
+ *
+ * @since 6.3
+ */
+ public static class AcceptItem extends AbstractItemSetCriterion {
+
+ /**
+ * @param select
+ * the select from which the item id's are checked
+ * @param itemId
+ * the item identifier(s) of the select that are accepted
+ */
+ public AcceptItem(AbstractSelect select, Object... itemId) {
+ super(select, itemId);
+ }
+
+ @Override
+ public boolean accept(DragAndDropEvent dragEvent) {
+ DataBoundTransferable transferable = (DataBoundTransferable) dragEvent
+ .getTransferable();
+ if (transferable.getSourceComponent() != select) {
+ return false;
+ }
+ return itemIds.contains(transferable.getItemId());
+ }
+
+ /**
+ * A simple accept criterion which ensures that {@link Transferable}
+ * contains an {@link Item} (or actually its identifier). In other words
+ * the criterion check that drag is coming from a {@link Container} like
+ * {@link Tree} or {@link Table}.
+ */
+ public static final ClientSideCriterion ALL = new ContainsDataFlavor(
+ "itemId");
+
+ }
+
+ /**
+ * TargetDetails implementation for subclasses of {@link AbstractSelect}
+ * that implement {@link DropTarget}.
+ *
+ * @since 6.3
+ */
+ public class AbstractSelectTargetDetails extends TargetDetailsImpl {
+
+ /**
+ * The item id over which the drag event happened.
+ */
+ protected Object idOver;
+
+ /**
+ * Constructor that automatically converts itemIdOver key to
+ * corresponding item Id
+ *
+ */
+ protected AbstractSelectTargetDetails(
+ Map<String, Object> rawVariables) {
+ super(rawVariables, (DropTarget) AbstractSelect.this);
+ // eagar fetch itemid, mapper may be emptied
+ String keyover = (String) getData("itemIdOver");
+ if (keyover != null) {
+ idOver = itemIdMapper.get(keyover);
+ }
+ }
+
+ /**
+ * If the drag operation is currently over an {@link Item}, this method
+ * returns the identifier of that {@link Item}.
+ *
+ */
+ public Object getItemIdOver() {
+ return idOver;
+ }
+
+ /**
+ * Returns a detailed vertical location where the drop happened on Item.
+ */
+ public VerticalDropLocation getDropLocation() {
+ String detail = (String) getData("detail");
+ if (detail == null) {
+ return null;
+ }
+ return VerticalDropLocation.valueOf(detail);
+ }
+
+ }
+
+ /**
+ * An accept criterion to accept drops only on a specific vertical location
+ * of an item.
+ * <p>
+ * This accept criterion is currently usable in Tree and Table
+ * implementations.
+ */
+ public static class VerticalLocationIs extends TargetDetailIs {
+ public static VerticalLocationIs TOP = new VerticalLocationIs(
+ VerticalDropLocation.TOP);
+ public static VerticalLocationIs BOTTOM = new VerticalLocationIs(
+ VerticalDropLocation.BOTTOM);
+ public static VerticalLocationIs MIDDLE = new VerticalLocationIs(
+ VerticalDropLocation.MIDDLE);
+
+ private VerticalLocationIs(VerticalDropLocation l) {
+ super("detail", l.name());
+ }
+ }
+
+ /**
+ * Implement this interface and pass it to Tree.setItemDescriptionGenerator
+ * or Table.setItemDescriptionGenerator to generate mouse over descriptions
+ * ("tooltips") for the rows and cells in Table or for the items in Tree.
+ */
+ public interface ItemDescriptionGenerator extends Serializable {
+
+ /**
+ * Called by Table when a cell (and row) is painted or a item is painted
+ * in Tree
+ *
+ * @param source
+ * The source of the generator, the Tree or Table the
+ * generator is attached to
+ * @param itemId
+ * The itemId of the painted cell
+ * @param propertyId
+ * The propertyId of the cell, null when getting row
+ * description
+ * @return The description or "tooltip" of the item.
+ */
+ public String generateDescription(Component source, Object itemId,
+ Object propertyId);
+ }
+
+ @Override
+ public void readDesign(Element design, DesignContext context) {
+ // handle default attributes
+ super.readDesign(design, context);
+ // handle children specifying selectable items (<option>)
+ readItems(design, context);
+ }
+
+ protected void readItems(Element design, DesignContext context) {
+ Set<String> selected = new HashSet<String>();
+ for (Element child : design.children()) {
+ readItem(child, selected, context);
+ }
+ if (!selected.isEmpty()) {
+ if (isMultiSelect()) {
+ setValue(selected, false, true);
+ } else if (selected.size() == 1) {
+ setValue(selected.iterator().next(), false, true);
+ } else {
+ throw new DesignException(
+ "Multiple values selected for a single select component");
+ }
+ }
+ }
+
+ /**
+ * Reads an Item from a design and inserts it into the data source.
+ * Hierarchical select components should override this method to recursively
+ * recursively read any child items as well.
+ *
+ * @since 7.5.0
+ * @param child
+ * a child element representing the item
+ * @param selected
+ * A set accumulating selected items. If the item that is read is
+ * marked as selected, its item id should be added to this set.
+ * @param context
+ * the DesignContext instance used in parsing
+ * @return the item id of the new item
+ *
+ * @throws DesignException
+ * if the tag name of the {@code child} element is not
+ * {@code option}.
+ */
+ protected Object readItem(Element child, Set<String> selected,
+ DesignContext context) {
+ if (!"option".equals(child.tagName())) {
+ throw new DesignException("Unrecognized child element in "
+ + getClass().getSimpleName() + ": " + child.tagName());
+ }
+
+ String itemId;
+ String caption = DesignFormatter.decodeFromTextNode(child.html());
+ if (child.hasAttr("item-id")) {
+ itemId = child.attr("item-id");
+ addItem(itemId);
+ setItemCaption(itemId, caption);
+ } else {
+ addItem(itemId = caption);
+ }
+
+ if (child.hasAttr("icon")) {
+ setItemIcon(itemId, DesignAttributeHandler.readAttribute("icon",
+ child.attributes(), Resource.class));
+ }
+
+ if (child.hasAttr("selected")) {
+ selected.add(itemId);
+ }
+
+ return itemId;
+ }
+
+ @Override
+ public void writeDesign(Element design, DesignContext context) {
+ // Write default attributes
+ super.writeDesign(design, context);
+
+ // Write options if warranted
+ if (context.shouldWriteData(this)) {
+ writeItems(design, context);
+ }
+ }
+
+ /**
+ * Writes the data source items to a design. Hierarchical select components
+ * should override this method to only write the root items.
+ *
+ * @since 7.5.0
+ * @param design
+ * the element into which to insert the items
+ * @param context
+ * the DesignContext instance used in writing
+ */
+ protected void writeItems(Element design, DesignContext context) {
+ for (Object itemId : getItemIds()) {
+ writeItem(design, itemId, context);
+ }
+ }
+
+ /**
+ * Writes a data source Item to a design. Hierarchical select components
+ * should override this method to recursively write any child items as well.
+ *
+ * @since 7.5.0
+ * @param design
+ * the element into which to insert the item
+ * @param itemId
+ * the id of the item to write
+ * @param context
+ * the DesignContext instance used in writing
+ * @return
+ */
+ protected Element writeItem(Element design, Object itemId,
+ DesignContext context) {
+ Element element = design.appendElement("option");
+
+ String caption = getItemCaption(itemId);
+ if (caption != null && !caption.equals(itemId.toString())) {
+ element.html(DesignFormatter.encodeForTextNode(caption));
+ element.attr("item-id", itemId.toString());
+ } else {
+ element.html(DesignFormatter.encodeForTextNode(itemId.toString()));
+ }
+
+ Resource icon = getItemIcon(itemId);
+ if (icon != null) {
+ DesignAttributeHandler.writeAttribute("icon", element.attributes(),
+ icon, null, Resource.class);
+ }
+
+ if (isSelected(itemId)) {
+ element.attr("selected", "");
+ }
+
+ return element;
+ }
+
+ @Override
+ protected AbstractSelectState getState() {
+ return (AbstractSelectState) super.getState();
+ }
+
+ @Override
+ protected AbstractSelectState getState(boolean markAsDirty) {
+ return (AbstractSelectState) super.getState(markAsDirty);
+ }
+}
diff --git a/compatibility-server/src/main/java/com/vaadin/v7/ui/Calendar.java b/compatibility-server/src/main/java/com/vaadin/v7/ui/Calendar.java
new file mode 100644
index 0000000000..5c6e0de421
--- /dev/null
+++ b/compatibility-server/src/main/java/com/vaadin/v7/ui/Calendar.java
@@ -0,0 +1,2031 @@
+/*
+ * 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.v7.ui;
+
+import java.lang.reflect.Method;
+import java.text.DateFormat;
+import java.text.DateFormatSymbols;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Date;
+import java.util.EventListener;
+import java.util.GregorianCalendar;
+import java.util.HashMap;
+import java.util.LinkedHashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.TimeZone;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import org.jsoup.nodes.Attributes;
+import org.jsoup.nodes.Element;
+
+import com.vaadin.event.Action;
+import com.vaadin.event.Action.Handler;
+import com.vaadin.event.dd.DropHandler;
+import com.vaadin.event.dd.DropTarget;
+import com.vaadin.event.dd.TargetDetails;
+import com.vaadin.server.KeyMapper;
+import com.vaadin.server.PaintException;
+import com.vaadin.server.PaintTarget;
+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.AbstractComponent;
+import com.vaadin.ui.LegacyComponent;
+import com.vaadin.ui.declarative.DesignAttributeHandler;
+import com.vaadin.ui.declarative.DesignContext;
+import com.vaadin.v7.data.Container;
+import com.vaadin.v7.data.util.BeanItemContainer;
+import com.vaadin.v7.ui.components.calendar.CalendarComponentEvent;
+import com.vaadin.v7.ui.components.calendar.CalendarComponentEvents;
+import com.vaadin.v7.ui.components.calendar.CalendarComponentEvents.BackwardEvent;
+import com.vaadin.v7.ui.components.calendar.CalendarComponentEvents.BackwardHandler;
+import com.vaadin.v7.ui.components.calendar.CalendarComponentEvents.DateClickEvent;
+import com.vaadin.v7.ui.components.calendar.CalendarComponentEvents.DateClickHandler;
+import com.vaadin.v7.ui.components.calendar.CalendarComponentEvents.EventClick;
+import com.vaadin.v7.ui.components.calendar.CalendarComponentEvents.EventClickHandler;
+import com.vaadin.v7.ui.components.calendar.CalendarComponentEvents.EventMoveHandler;
+import com.vaadin.v7.ui.components.calendar.CalendarComponentEvents.EventResize;
+import com.vaadin.v7.ui.components.calendar.CalendarComponentEvents.EventResizeHandler;
+import com.vaadin.v7.ui.components.calendar.CalendarComponentEvents.ForwardEvent;
+import com.vaadin.v7.ui.components.calendar.CalendarComponentEvents.ForwardHandler;
+import com.vaadin.v7.ui.components.calendar.CalendarComponentEvents.MoveEvent;
+import com.vaadin.v7.ui.components.calendar.CalendarComponentEvents.RangeSelectEvent;
+import com.vaadin.v7.ui.components.calendar.CalendarComponentEvents.RangeSelectHandler;
+import com.vaadin.v7.ui.components.calendar.CalendarComponentEvents.WeekClick;
+import com.vaadin.v7.ui.components.calendar.CalendarComponentEvents.WeekClickHandler;
+import com.vaadin.v7.ui.components.calendar.CalendarDateRange;
+import com.vaadin.v7.ui.components.calendar.CalendarTargetDetails;
+import com.vaadin.v7.ui.components.calendar.ContainerEventProvider;
+import com.vaadin.v7.ui.components.calendar.event.BasicEventProvider;
+import com.vaadin.v7.ui.components.calendar.event.CalendarEditableEventProvider;
+import com.vaadin.v7.ui.components.calendar.event.CalendarEvent;
+import com.vaadin.v7.ui.components.calendar.event.CalendarEvent.EventChangeEvent;
+import com.vaadin.v7.ui.components.calendar.event.CalendarEvent.EventChangeListener;
+import com.vaadin.v7.ui.components.calendar.event.CalendarEventProvider;
+import com.vaadin.v7.ui.components.calendar.handler.BasicBackwardHandler;
+import com.vaadin.v7.ui.components.calendar.handler.BasicDateClickHandler;
+import com.vaadin.v7.ui.components.calendar.handler.BasicEventMoveHandler;
+import com.vaadin.v7.ui.components.calendar.handler.BasicEventResizeHandler;
+import com.vaadin.v7.ui.components.calendar.handler.BasicForwardHandler;
+import com.vaadin.v7.ui.components.calendar.handler.BasicWeekClickHandler;
+
+/**
+ * <p>
+ * Vaadin Calendar is for visualizing events in a calendar. Calendar events can
+ * be visualized in the variable length view depending on the start and end
+ * dates.
+ * </p>
+ *
+ * <li>You can set the viewable date range with the {@link #setStartDate(Date)}
+ * and {@link #setEndDate(Date)} methods. Calendar has a default date range of
+ * one week</li>
+ *
+ * <li>Calendar has two kind of views: monthly and weekly view</li>
+ *
+ * <li>If date range is seven days or shorter, the weekly view is used.</li>
+ *
+ * <li>Calendar queries its events by using a
+ * {@link com.vaadin.addon.calendar.event.CalendarEventProvider
+ * CalendarEventProvider}. By default, a
+ * {@link com.vaadin.addon.calendar.event.BasicEventProvider BasicEventProvider}
+ * is used.</li>
+ *
+ * @since 7.1
+ * @author Vaadin Ltd.
+ */
+@SuppressWarnings("serial")
+public class Calendar extends AbstractComponent
+ implements CalendarComponentEvents.NavigationNotifier,
+ CalendarComponentEvents.EventMoveNotifier,
+ CalendarComponentEvents.RangeSelectNotifier,
+ CalendarComponentEvents.EventResizeNotifier,
+ CalendarEventProvider.EventSetChangeListener, DropTarget,
+ CalendarEditableEventProvider, Action.Container, LegacyComponent {
+
+ /**
+ * Calendar can use either 12 hours clock or 24 hours clock.
+ */
+ public enum TimeFormat {
+
+ Format12H(), Format24H();
+ }
+
+ /** Defines currently active format for time. 12H/24H. */
+ protected TimeFormat currentTimeFormat;
+
+ /** Internal calendar data source. */
+ protected java.util.Calendar currentCalendar = java.util.Calendar
+ .getInstance();
+
+ /** Defines the component's active time zone. */
+ protected TimeZone timezone;
+
+ /** Defines the calendar's date range starting point. */
+ protected Date startDate = null;
+
+ /** Defines the calendar's date range ending point. */
+ protected Date endDate = null;
+
+ /** Event provider. */
+ private CalendarEventProvider calendarEventProvider;
+
+ /**
+ * Internal buffer for the events that are retrieved from the event
+ * provider.
+ */
+ protected List<CalendarEvent> events;
+
+ /** Date format that will be used in the UIDL for dates. */
+ protected DateFormat df_date = new SimpleDateFormat("yyyy-MM-dd");
+
+ /** Time format that will be used in the UIDL for time. */
+ protected DateFormat df_time = new SimpleDateFormat("HH:mm:ss");
+
+ /** Date format that will be used in the UIDL for both date and time. */
+ protected DateFormat df_date_time = new SimpleDateFormat(
+ DateConstants.CLIENT_DATE_FORMAT + "-"
+ + DateConstants.CLIENT_TIME_FORMAT);
+
+ /**
+ * Week view's scroll position. Client sends updates to this value so that
+ * scroll position wont reset all the time.
+ */
+ private int scrollTop = 0;
+
+ /** Caption format for the weekly view */
+ private String weeklyCaptionFormat = null;
+
+ /** Map from event ids to event handlers */
+ private final Map<String, EventListener> handlers;
+
+ /**
+ * Drop Handler for Vaadin DD. By default null.
+ */
+ private DropHandler dropHandler;
+
+ /**
+ * First day to show for a week
+ */
+ private int firstDay = 1;
+
+ /**
+ * Last day to show for a week
+ */
+ private int lastDay = 7;
+
+ /**
+ * First hour to show for a day
+ */
+ private int firstHour = 0;
+
+ /**
+ * Last hour to show for a day
+ */
+ private int lastHour = 23;
+
+ /**
+ * List of action handlers.
+ */
+ private LinkedList<Action.Handler> actionHandlers = null;
+
+ /**
+ * Action mapper.
+ */
+ private KeyMapper<Action> actionMapper = null;
+
+ /**
+ *
+ */
+ private CalendarServerRpcImpl rpc = new CalendarServerRpcImpl();
+
+ private Integer customFirstDayOfWeek;
+
+ /**
+ * Returns the logger for the calendar
+ */
+ protected Logger getLogger() {
+ return Logger.getLogger(Calendar.class.getName());
+ }
+
+ /**
+ * Construct a Vaadin Calendar with a BasicEventProvider and no caption.
+ * Default date range is one week.
+ */
+ public Calendar() {
+ this(null, new BasicEventProvider());
+ }
+
+ /**
+ * Construct a Vaadin Calendar with a BasicEventProvider and the provided
+ * caption. Default date range is one week.
+ *
+ * @param caption
+ */
+ public Calendar(String caption) {
+ this(caption, new BasicEventProvider());
+ }
+
+ /**
+ * <p>
+ * Construct a Vaadin Calendar with event provider. Event provider is
+ * obligatory, because calendar component will query active events through
+ * it.
+ * </p>
+ *
+ * <p>
+ * By default, Vaadin Calendar will show dates from the start of the current
+ * week to the end of the current week. Use {@link #setStartDate(Date)} and
+ * {@link #setEndDate(Date)} to change this.
+ * </p>
+ *
+ * @param eventProvider
+ * Event provider, cannot be null.
+ */
+ public Calendar(CalendarEventProvider eventProvider) {
+ this(null, eventProvider);
+ }
+
+ /**
+ * <p>
+ * Construct a Vaadin Calendar with event provider and a caption. Event
+ * provider is obligatory, because calendar component will query active
+ * events through it.
+ * </p>
+ *
+ * <p>
+ * By default, Vaadin Calendar will show dates from the start of the current
+ * week to the end of the current week. Use {@link #setStartDate(Date)} and
+ * {@link #setEndDate(Date)} to change this.
+ * </p>
+ *
+ * @param eventProvider
+ * Event provider, cannot be null.
+ */
+ // this is the constructor every other constructor calls
+ public Calendar(String caption, CalendarEventProvider eventProvider) {
+ registerRpc(rpc);
+ setCaption(caption);
+ handlers = new HashMap<String, EventListener>();
+ setDefaultHandlers();
+ currentCalendar.setTime(new Date());
+ setEventProvider(eventProvider);
+ getState().firstDayOfWeek = firstDay;
+ getState().lastVisibleDayOfWeek = lastDay;
+ getState().firstHourOfDay = firstHour;
+ getState().lastHourOfDay = lastHour;
+ setTimeFormat(null);
+
+ }
+
+ @Override
+ public CalendarState getState() {
+ return (CalendarState) super.getState();
+ }
+
+ @Override
+ protected CalendarState getState(boolean markAsDirty) {
+ return (CalendarState) super.getState(markAsDirty);
+ }
+
+ @Override
+ public void beforeClientResponse(boolean initial) {
+ super.beforeClientResponse(initial);
+
+ initCalendarWithLocale();
+
+ getState().format24H = TimeFormat.Format24H == getTimeFormat();
+ setupDaysAndActions();
+ setupCalendarEvents();
+ rpc.scroll(scrollTop);
+ }
+
+ /**
+ * Set all the wanted default handlers here. This is always called after
+ * constructing this object. All other events have default handlers except
+ * range and event click.
+ */
+ protected void setDefaultHandlers() {
+ setHandler(new BasicBackwardHandler());
+ setHandler(new BasicForwardHandler());
+ setHandler(new BasicWeekClickHandler());
+ setHandler(new BasicDateClickHandler());
+ setHandler(new BasicEventMoveHandler());
+ setHandler(new BasicEventResizeHandler());
+ }
+
+ /**
+ * Gets the calendar's start date.
+ *
+ * @return First visible date.
+ */
+ public Date getStartDate() {
+ if (startDate == null) {
+ currentCalendar.set(java.util.Calendar.MILLISECOND, 0);
+ currentCalendar.set(java.util.Calendar.SECOND, 0);
+ currentCalendar.set(java.util.Calendar.MINUTE, 0);
+ currentCalendar.set(java.util.Calendar.HOUR_OF_DAY, 0);
+ currentCalendar.set(java.util.Calendar.DAY_OF_WEEK,
+ currentCalendar.getFirstDayOfWeek());
+ return currentCalendar.getTime();
+ }
+ return startDate;
+ }
+
+ /**
+ * Sets start date for the calendar. This and {@link #setEndDate(Date)}
+ * control the range of dates visible on the component. The default range is
+ * one week.
+ *
+ * @param date
+ * First visible date to show.
+ */
+ public void setStartDate(Date date) {
+ if (!date.equals(startDate)) {
+ startDate = date;
+ markAsDirty();
+ }
+ }
+
+ /**
+ * Gets the calendar's end date.
+ *
+ * @return Last visible date.
+ */
+ public Date getEndDate() {
+ if (endDate == null) {
+ currentCalendar.set(java.util.Calendar.MILLISECOND, 0);
+ currentCalendar.set(java.util.Calendar.SECOND, 59);
+ currentCalendar.set(java.util.Calendar.MINUTE, 59);
+ currentCalendar.set(java.util.Calendar.HOUR_OF_DAY, 23);
+ currentCalendar.set(java.util.Calendar.DAY_OF_WEEK,
+ currentCalendar.getFirstDayOfWeek() + 6);
+ return currentCalendar.getTime();
+ }
+ return endDate;
+ }
+
+ /**
+ * Sets end date for the calendar. Starting from startDate, only six weeks
+ * will be shown if duration to endDate is longer than six weeks.
+ *
+ * This and {@link #setStartDate(Date)} control the range of dates visible
+ * on the component. The default range is one week.
+ *
+ * @param date
+ * Last visible date to show.
+ */
+ public void setEndDate(Date date) {
+ if (startDate != null && startDate.after(date)) {
+ startDate = (Date) date.clone();
+ markAsDirty();
+ } else if (!date.equals(endDate)) {
+ endDate = date;
+ markAsDirty();
+ }
+ }
+
+ /**
+ * Sets the locale to be used in the Calendar component.
+ *
+ * @see com.vaadin.ui.AbstractComponent#setLocale(java.util.Locale)
+ */
+ @Override
+ public void setLocale(Locale newLocale) {
+ super.setLocale(newLocale);
+ initCalendarWithLocale();
+ }
+
+ /**
+ * Initialize the java calendar instance with the current locale and
+ * timezone.
+ */
+ private void initCalendarWithLocale() {
+ if (timezone != null) {
+ currentCalendar = java.util.Calendar.getInstance(timezone,
+ getLocale());
+
+ } else {
+ currentCalendar = java.util.Calendar.getInstance(getLocale());
+ }
+
+ if (customFirstDayOfWeek != null) {
+ currentCalendar.setFirstDayOfWeek(customFirstDayOfWeek);
+ }
+ }
+
+ private void setupCalendarEvents() {
+ int durationInDays = (int) (((endDate.getTime()) - startDate.getTime())
+ / DateConstants.DAYINMILLIS);
+ durationInDays++;
+ if (durationInDays > 60) {
+ throw new RuntimeException(
+ "Daterange is too big (max 60) = " + durationInDays);
+ }
+
+ Date firstDateToShow = expandStartDate(startDate, durationInDays > 7);
+ Date lastDateToShow = expandEndDate(endDate, durationInDays > 7);
+
+ currentCalendar.setTime(firstDateToShow);
+ events = getEventProvider().getEvents(firstDateToShow, lastDateToShow);
+
+ List<CalendarState.Event> calendarStateEvents = new ArrayList<CalendarState.Event>();
+ if (events != null) {
+ for (int i = 0; i < events.size(); i++) {
+ CalendarEvent e = events.get(i);
+ CalendarState.Event event = new CalendarState.Event();
+ event.index = i;
+ event.caption = e.getCaption() == null ? "" : e.getCaption();
+ event.dateFrom = df_date.format(e.getStart());
+ event.dateTo = df_date.format(e.getEnd());
+ event.timeFrom = df_time.format(e.getStart());
+ event.timeTo = df_time.format(e.getEnd());
+ event.description = e.getDescription() == null ? ""
+ : e.getDescription();
+ event.styleName = e.getStyleName() == null ? ""
+ : e.getStyleName();
+ event.allDay = e.isAllDay();
+ calendarStateEvents.add(event);
+ }
+ }
+ getState().events = calendarStateEvents;
+ }
+
+ private void setupDaysAndActions() {
+ // Make sure we have a up-to-date locale
+ initCalendarWithLocale();
+
+ CalendarState state = getState();
+
+ state.firstDayOfWeek = currentCalendar.getFirstDayOfWeek();
+
+ // If only one is null, throw exception
+ // If both are null, set defaults
+ if (startDate == null ^ endDate == null) {
+ String message = "Schedule cannot be painted without a proper date range.\n";
+ if (startDate == null) {
+ throw new IllegalStateException(message
+ + "You must set a start date using setStartDate(Date).");
+
+ } else {
+ throw new IllegalStateException(message
+ + "You must set an end date using setEndDate(Date).");
+ }
+
+ } else if (startDate == null && endDate == null) {
+ // set defaults
+ startDate = getStartDate();
+ endDate = getEndDate();
+ }
+
+ int durationInDays = (int) (((endDate.getTime()) - startDate.getTime())
+ / DateConstants.DAYINMILLIS);
+ durationInDays++;
+ if (durationInDays > 60) {
+ throw new RuntimeException(
+ "Daterange is too big (max 60) = " + durationInDays);
+ }
+
+ state.dayNames = getDayNamesShort();
+ state.monthNames = getMonthNamesShort();
+
+ // Use same timezone in all dates this component handles.
+ // Show "now"-marker in browser within given timezone.
+ Date now = new Date();
+ currentCalendar.setTime(now);
+ now = currentCalendar.getTime();
+
+ // Reset time zones for custom date formats
+ df_date.setTimeZone(currentCalendar.getTimeZone());
+ df_time.setTimeZone(currentCalendar.getTimeZone());
+
+ state.now = (df_date.format(now) + " " + df_time.format(now));
+
+ Date firstDateToShow = expandStartDate(startDate, durationInDays > 7);
+ Date lastDateToShow = expandEndDate(endDate, durationInDays > 7);
+
+ currentCalendar.setTime(firstDateToShow);
+
+ DateFormat weeklyCaptionFormatter = getWeeklyCaptionFormatter();
+ weeklyCaptionFormatter.setTimeZone(currentCalendar.getTimeZone());
+
+ Map<CalendarDateRange, Set<Action>> actionMap = new HashMap<CalendarDateRange, Set<Action>>();
+
+ List<CalendarState.Day> days = new ArrayList<CalendarState.Day>();
+
+ // Send all dates to client from server. This
+ // approach was taken because gwt doesn't
+ // support date localization properly.
+ while (currentCalendar.getTime().compareTo(lastDateToShow) < 1) {
+ final Date date = currentCalendar.getTime();
+ final CalendarState.Day day = new CalendarState.Day();
+ day.date = df_date.format(date);
+ day.localizedDateFormat = weeklyCaptionFormatter.format(date);
+ day.dayOfWeek = getDowByLocale(currentCalendar);
+ day.week = getWeek(currentCalendar);
+ day.yearOfWeek = getYearOfWeek(currentCalendar);
+
+ days.add(day);
+
+ // Get actions for a specific date
+ if (actionHandlers != null) {
+ for (Action.Handler actionHandler : actionHandlers) {
+
+ // Create calendar which omits time
+ GregorianCalendar cal = new GregorianCalendar(getTimeZone(),
+ getLocale());
+ cal.clear();
+ cal.set(currentCalendar.get(java.util.Calendar.YEAR),
+ currentCalendar.get(java.util.Calendar.MONTH),
+ currentCalendar.get(java.util.Calendar.DATE));
+
+ // Get day start and end times
+ Date start = cal.getTime();
+ cal.add(java.util.Calendar.DATE, 1);
+ cal.add(java.util.Calendar.SECOND, -1);
+ Date end = cal.getTime();
+
+ boolean monthView = (durationInDays > 7);
+
+ /**
+ * If in day or week view add actions for each half-an-hour.
+ * If in month view add actions for each day
+ */
+ if (monthView) {
+ setActionsForDay(actionMap, start, end, actionHandler);
+ } else {
+ setActionsForEachHalfHour(actionMap, start, end,
+ actionHandler);
+ }
+
+ }
+ }
+
+ currentCalendar.add(java.util.Calendar.DATE, 1);
+ }
+ state.days = days;
+ state.actions = createActionsList(actionMap);
+ }
+
+ private int getWeek(java.util.Calendar calendar) {
+ return calendar.get(java.util.Calendar.WEEK_OF_YEAR);
+ }
+
+ private int getYearOfWeek(java.util.Calendar calendar) {
+ // Would use calendar.getWeekYear() but it's only available since 1.7.
+ int week = getWeek(calendar);
+ int month = calendar.get(java.util.Calendar.MONTH);
+ int year = calendar.get(java.util.Calendar.YEAR);
+
+ if (week == 1 && month == java.util.Calendar.DECEMBER) {
+ return year + 1;
+ }
+
+ return year;
+ }
+
+ private void setActionsForEachHalfHour(
+ Map<CalendarDateRange, Set<Action>> actionMap, Date start, Date end,
+ Action.Handler actionHandler) {
+ GregorianCalendar cal = new GregorianCalendar(getTimeZone(),
+ getLocale());
+ cal.setTime(start);
+ while (cal.getTime().before(end)) {
+ Date s = cal.getTime();
+ cal.add(java.util.Calendar.MINUTE, 30);
+ Date e = cal.getTime();
+ CalendarDateRange range = new CalendarDateRange(s, e,
+ getTimeZone());
+ Action[] actions = actionHandler.getActions(range, this);
+ if (actions != null) {
+ Set<Action> actionSet = new LinkedHashSet<Action>(
+ Arrays.asList(actions));
+ actionMap.put(range, actionSet);
+ }
+ }
+ }
+
+ private void setActionsForDay(Map<CalendarDateRange, Set<Action>> actionMap,
+ Date start, Date end, Action.Handler actionHandler) {
+ CalendarDateRange range = new CalendarDateRange(start, end,
+ getTimeZone());
+ Action[] actions = actionHandler.getActions(range, this);
+ if (actions != null) {
+ Set<Action> actionSet = new LinkedHashSet<Action>(
+ Arrays.asList(actions));
+ actionMap.put(range, actionSet);
+ }
+ }
+
+ private List<CalendarState.Action> createActionsList(
+ Map<CalendarDateRange, Set<Action>> actionMap) {
+ if (actionMap.isEmpty()) {
+ return null;
+ }
+
+ List<CalendarState.Action> calendarActions = new ArrayList<CalendarState.Action>();
+
+ SimpleDateFormat formatter = new SimpleDateFormat(
+ DateConstants.ACTION_DATE_FORMAT_PATTERN);
+ formatter.setTimeZone(getTimeZone());
+
+ for (Entry<CalendarDateRange, Set<Action>> entry : actionMap
+ .entrySet()) {
+ CalendarDateRange range = entry.getKey();
+ Set<Action> actions = entry.getValue();
+ for (Action action : actions) {
+ String key = actionMapper.key(action);
+ CalendarState.Action calendarAction = new CalendarState.Action();
+ calendarAction.actionKey = key;
+ calendarAction.caption = action.getCaption();
+ setResource(key, action.getIcon());
+ calendarAction.iconKey = key;
+ calendarAction.startDate = formatter.format(range.getStart());
+ calendarAction.endDate = formatter.format(range.getEnd());
+ calendarActions.add(calendarAction);
+ }
+ }
+
+ return calendarActions;
+ }
+
+ /**
+ * Gets currently active time format. Value is either TimeFormat.Format12H
+ * or TimeFormat.Format24H.
+ *
+ * @return TimeFormat Format for the time.
+ */
+ public TimeFormat getTimeFormat() {
+ if (currentTimeFormat == null) {
+ SimpleDateFormat f;
+ if (getLocale() == null) {
+ f = (SimpleDateFormat) SimpleDateFormat
+ .getTimeInstance(SimpleDateFormat.SHORT);
+ } else {
+ f = (SimpleDateFormat) SimpleDateFormat
+ .getTimeInstance(SimpleDateFormat.SHORT, getLocale());
+ }
+ String p = f.toPattern();
+ if (p.indexOf("HH") != -1 || p.indexOf("H") != -1) {
+ return TimeFormat.Format24H;
+ }
+ return TimeFormat.Format12H;
+ }
+ return currentTimeFormat;
+ }
+
+ /**
+ * Example: <code>setTimeFormat(TimeFormat.Format12H);</code></br>
+ * Set to null, if you want the format being defined by the locale.
+ *
+ * @param format
+ * Set 12h or 24h format. Default is defined by the locale.
+ */
+ public void setTimeFormat(TimeFormat format) {
+ currentTimeFormat = format;
+ markAsDirty();
+ }
+
+ /**
+ * Returns a time zone that is currently used by this component.
+ *
+ * @return Component's Time zone
+ */
+ public TimeZone getTimeZone() {
+ if (timezone == null) {
+ return currentCalendar.getTimeZone();
+ }
+ return timezone;
+ }
+
+ /**
+ * Set time zone that this component will use. Null value sets the default
+ * time zone.
+ *
+ * @param zone
+ * Time zone to use
+ */
+ public void setTimeZone(TimeZone zone) {
+ timezone = zone;
+ if (!currentCalendar.getTimeZone().equals(zone)) {
+ if (zone == null) {
+ zone = TimeZone.getDefault();
+ }
+ currentCalendar.setTimeZone(zone);
+ df_date_time.setTimeZone(zone);
+ markAsDirty();
+ }
+ }
+
+ /**
+ * Get the internally used Calendar instance. This is the currently used
+ * instance of {@link java.util.Calendar} but is bound to change during the
+ * lifetime of the component.
+ *
+ * @return the currently used java calendar
+ */
+ public java.util.Calendar getInternalCalendar() {
+ return currentCalendar;
+ }
+
+ /**
+ * <p>
+ * This method restricts the weekdays that are shown. This affects both the
+ * monthly and the weekly view. The general contract is that <b>firstDay <
+ * lastDay</b>.
+ * </p>
+ *
+ * <p>
+ * Note that this only affects the rendering process. Events are still
+ * requested by the dates set by {@link #setStartDate(Date)} and
+ * {@link #setEndDate(Date)}.
+ * </p>
+ *
+ * @param firstDay
+ * the first day of the week to show, between 1 and 7
+ */
+ public void setFirstVisibleDayOfWeek(int firstDay) {
+ if (this.firstDay != firstDay && firstDay >= 1 && firstDay <= 7
+ && getLastVisibleDayOfWeek() >= firstDay) {
+ this.firstDay = firstDay;
+ getState().firstVisibleDayOfWeek = firstDay;
+ }
+ }
+
+ /**
+ * Get the first visible day of the week. Returns the weekdays as integers
+ * represented by {@link java.util.Calendar#DAY_OF_WEEK}
+ *
+ * @return An integer representing the week day according to
+ * {@link java.util.Calendar#DAY_OF_WEEK}
+ */
+ public int getFirstVisibleDayOfWeek() {
+ return firstDay;
+ }
+
+ /**
+ * <p>
+ * This method restricts the weekdays that are shown. This affects both the
+ * monthly and the weekly view. The general contract is that <b>firstDay <
+ * lastDay</b>.
+ * </p>
+ *
+ * <p>
+ * Note that this only affects the rendering process. Events are still
+ * requested by the dates set by {@link #setStartDate(Date)} and
+ * {@link #setEndDate(Date)}.
+ * </p>
+ *
+ * @param lastDay
+ * the first day of the week to show, between 1 and 7
+ */
+ public void setLastVisibleDayOfWeek(int lastDay) {
+ if (this.lastDay != lastDay && lastDay >= 1 && lastDay <= 7
+ && getFirstVisibleDayOfWeek() <= lastDay) {
+ this.lastDay = lastDay;
+ getState().lastVisibleDayOfWeek = lastDay;
+ }
+ }
+
+ /**
+ * Get the last visible day of the week. Returns the weekdays as integers
+ * represented by {@link java.util.Calendar#DAY_OF_WEEK}
+ *
+ * @return An integer representing the week day according to
+ * {@link java.util.Calendar#DAY_OF_WEEK}
+ */
+ public int getLastVisibleDayOfWeek() {
+ return lastDay;
+ }
+
+ /**
+ * <p>
+ * This method restricts the hours that are shown per day. This affects the
+ * weekly view. The general contract is that <b>firstHour < lastHour</b>.
+ * </p>
+ *
+ * <p>
+ * Note that this only affects the rendering process. Events are still
+ * requested by the dates set by {@link #setStartDate(Date)} and
+ * {@link #setEndDate(Date)}.
+ * </p>
+ *
+ * @param firstHour
+ * the first hour of the day to show, between 0 and 23
+ */
+ public void setFirstVisibleHourOfDay(int firstHour) {
+ if (this.firstHour != firstHour && firstHour >= 0 && firstHour <= 23
+ && firstHour <= getLastVisibleHourOfDay()) {
+ this.firstHour = firstHour;
+ getState().firstHourOfDay = firstHour;
+ }
+ }
+
+ /**
+ * Returns the first visible hour in the week view. Returns the hour using a
+ * 24h time format
+ *
+ */
+ public int getFirstVisibleHourOfDay() {
+ return firstHour;
+ }
+
+ /**
+ * <p>
+ * This method restricts the hours that are shown per day. This affects the
+ * weekly view. The general contract is that <b>firstHour < lastHour</b>.
+ * </p>
+ *
+ * <p>
+ * Note that this only affects the rendering process. Events are still
+ * requested by the dates set by {@link #setStartDate(Date)} and
+ * {@link #setEndDate(Date)}.
+ * </p>
+ *
+ * @param lastHour
+ * the first hour of the day to show, between 0 and 23
+ */
+ public void setLastVisibleHourOfDay(int lastHour) {
+ if (this.lastHour != lastHour && lastHour >= 0 && lastHour <= 23
+ && lastHour >= getFirstVisibleHourOfDay()) {
+ this.lastHour = lastHour;
+ getState().lastHourOfDay = lastHour;
+ }
+ }
+
+ /**
+ * Returns the last visible hour in the week view. Returns the hour using a
+ * 24h time format
+ *
+ */
+ public int getLastVisibleHourOfDay() {
+ return lastHour;
+ }
+
+ /**
+ * Gets the date caption format for the weekly view.
+ *
+ * @return The pattern used in caption of dates in weekly view.
+ */
+ public String getWeeklyCaptionFormat() {
+ return weeklyCaptionFormat;
+ }
+
+ /**
+ * Sets custom date format for the weekly view. This is the caption of the
+ * date. Format could be like "mmm MM/dd".
+ *
+ * @param dateFormatPattern
+ * The date caption pattern.
+ */
+ public void setWeeklyCaptionFormat(String dateFormatPattern) {
+ if ((weeklyCaptionFormat == null && dateFormatPattern != null)
+ || (weeklyCaptionFormat != null
+ && !weeklyCaptionFormat.equals(dateFormatPattern))) {
+ weeklyCaptionFormat = dateFormatPattern;
+ markAsDirty();
+ }
+ }
+
+ private DateFormat getWeeklyCaptionFormatter() {
+ if (weeklyCaptionFormat != null) {
+ return new SimpleDateFormat(weeklyCaptionFormat, getLocale());
+ } else {
+ return SimpleDateFormat.getDateInstance(SimpleDateFormat.SHORT,
+ getLocale());
+ }
+ }
+
+ /**
+ * Get the day of week by the given calendar and its locale
+ *
+ * @param calendar
+ * The calendar to use
+ * @return
+ */
+ private static int getDowByLocale(java.util.Calendar calendar) {
+ int fow = calendar.get(java.util.Calendar.DAY_OF_WEEK);
+
+ // monday first
+ if (calendar.getFirstDayOfWeek() == java.util.Calendar.MONDAY) {
+ fow = (fow == java.util.Calendar.SUNDAY) ? 7 : fow - 1;
+ }
+
+ return fow;
+ }
+
+ /**
+ * Is the user allowed to trigger events which alters the events
+ *
+ * @return true if the client is allowed to send changes to server
+ * @see #isEventClickAllowed()
+ */
+ protected boolean isClientChangeAllowed() {
+ return !isReadOnly();
+ }
+
+ /**
+ * Is the user allowed to trigger click events. Returns {@code true} by
+ * default. Subclass can override this method to disallow firing event
+ * clicks got from the client side.
+ *
+ * @return true if the client is allowed to click events
+ * @see #isClientChangeAllowed()
+ * @deprecated As of 7.4, override {@link #fireEventClick(Integer)} instead.
+ */
+ @Deprecated
+ protected boolean isEventClickAllowed() {
+ return true;
+ }
+
+ /**
+ * Fires an event when the user selecing moving forward/backward in the
+ * calendar.
+ *
+ * @param forward
+ * True if the calendar moved forward else backward is assumed.
+ */
+ protected void fireNavigationEvent(boolean forward) {
+ if (forward) {
+ fireEvent(new ForwardEvent(this));
+ } else {
+ fireEvent(new BackwardEvent(this));
+ }
+ }
+
+ /**
+ * Fires an event move event to all server side move listerners
+ *
+ * @param index
+ * The index of the event in the events list
+ * @param newFromDatetime
+ * The changed from date time
+ */
+ protected void fireEventMove(int index, Date newFromDatetime) {
+ MoveEvent event = new MoveEvent(this, events.get(index),
+ newFromDatetime);
+
+ if (calendarEventProvider instanceof EventMoveHandler) {
+ // Notify event provider if it is an event move handler
+ ((EventMoveHandler) calendarEventProvider).eventMove(event);
+ }
+
+ // Notify event move handler attached by using the
+ // setHandler(EventMoveHandler) method
+ fireEvent(event);
+ }
+
+ /**
+ * Fires event when a week was clicked in the calendar.
+ *
+ * @param week
+ * The week that was clicked
+ * @param year
+ * The year of the week
+ */
+ protected void fireWeekClick(int week, int year) {
+ fireEvent(new WeekClick(this, week, year));
+ }
+
+ /**
+ * Fires event when a date was clicked in the calendar. Uses an existing
+ * event from the event cache.
+ *
+ * @param index
+ * The index of the event in the event cache.
+ */
+ protected void fireEventClick(Integer index) {
+ fireEvent(new EventClick(this, events.get(index)));
+ }
+
+ /**
+ * Fires event when a date was clicked in the calendar. Creates a new event
+ * for the date and passes it to the listener.
+ *
+ * @param date
+ * The date and time that was clicked
+ */
+ protected void fireDateClick(Date date) {
+ fireEvent(new DateClickEvent(this, date));
+ }
+
+ /**
+ * Fires an event range selected event. The event is fired when a user
+ * highlights an area in the calendar. The highlighted areas start and end
+ * dates are returned as arguments.
+ *
+ * @param from
+ * The start date and time of the highlighted area
+ * @param to
+ * The end date and time of the highlighted area
+ * @param monthlyMode
+ * Is the calendar in monthly mode
+ */
+ protected void fireRangeSelect(Date from, Date to, boolean monthlyMode) {
+ fireEvent(new RangeSelectEvent(this, from, to, monthlyMode));
+ }
+
+ /**
+ * Fires an event resize event. The event is fired when a user resizes the
+ * event in the calendar causing the time range of the event to increase or
+ * decrease. The new start and end times are returned as arguments to this
+ * method.
+ *
+ * @param index
+ * The index of the event in the event cache
+ * @param startTime
+ * The new start date and time of the event
+ * @param endTime
+ * The new end date and time of the event
+ */
+ protected void fireEventResize(int index, Date startTime, Date endTime) {
+ EventResize event = new EventResize(this, events.get(index), startTime,
+ endTime);
+
+ if (calendarEventProvider instanceof EventResizeHandler) {
+ // Notify event provider if it is an event resize handler
+ ((EventResizeHandler) calendarEventProvider).eventResize(event);
+ }
+
+ // Notify event resize handler attached by using the
+ // setHandler(EventMoveHandler) method
+ fireEvent(event);
+ }
+
+ /**
+ * Localized display names for week days starting from sunday. Returned
+ * array's length is always 7.
+ *
+ * @return Array of localized weekday names.
+ */
+ protected String[] getDayNamesShort() {
+ DateFormatSymbols s = new DateFormatSymbols(getLocale());
+ return Arrays.copyOfRange(s.getWeekdays(), 1, 8);
+ }
+
+ /**
+ * Localized display names for months starting from January. Returned
+ * array's length is always 12.
+ *
+ * @return Array of localized month names.
+ */
+ protected String[] getMonthNamesShort() {
+ DateFormatSymbols s = new DateFormatSymbols(getLocale());
+ return Arrays.copyOf(s.getShortMonths(), 12);
+ }
+
+ /**
+ * Gets a date that is first day in the week that target given date belongs
+ * to.
+ *
+ * @param date
+ * Target date
+ * @return Date that is first date in same week that given date is.
+ */
+ protected Date getFirstDateForWeek(Date date) {
+ int firstDayOfWeek = currentCalendar.getFirstDayOfWeek();
+ currentCalendar.setTime(date);
+ while (firstDayOfWeek != currentCalendar
+ .get(java.util.Calendar.DAY_OF_WEEK)) {
+ currentCalendar.add(java.util.Calendar.DATE, -1);
+ }
+ return currentCalendar.getTime();
+ }
+
+ /**
+ * Gets a date that is last day in the week that target given date belongs
+ * to.
+ *
+ * @param date
+ * Target date
+ * @return Date that is last date in same week that given date is.
+ */
+ protected Date getLastDateForWeek(Date date) {
+ currentCalendar.setTime(date);
+ currentCalendar.add(java.util.Calendar.DATE, 1);
+ int firstDayOfWeek = currentCalendar.getFirstDayOfWeek();
+ // Roll to weeks last day using firstdayofweek. Roll until FDofW is
+ // found and then roll back one day.
+ while (firstDayOfWeek != currentCalendar
+ .get(java.util.Calendar.DAY_OF_WEEK)) {
+ currentCalendar.add(java.util.Calendar.DATE, 1);
+ }
+ currentCalendar.add(java.util.Calendar.DATE, -1);
+ return currentCalendar.getTime();
+ }
+
+ /**
+ * Calculates the end time of the day using the given calendar and date
+ *
+ * @param date
+ * @param calendar
+ * the calendar instance to be used in the calculation. The given
+ * instance is unchanged in this operation.
+ * @return the given date, with time set to the end of the day
+ */
+ private static Date getEndOfDay(java.util.Calendar calendar, Date date) {
+ java.util.Calendar calendarClone = (java.util.Calendar) calendar
+ .clone();
+
+ calendarClone.setTime(date);
+ calendarClone.set(java.util.Calendar.MILLISECOND,
+ calendarClone.getActualMaximum(java.util.Calendar.MILLISECOND));
+ calendarClone.set(java.util.Calendar.SECOND,
+ calendarClone.getActualMaximum(java.util.Calendar.SECOND));
+ calendarClone.set(java.util.Calendar.MINUTE,
+ calendarClone.getActualMaximum(java.util.Calendar.MINUTE));
+ calendarClone.set(java.util.Calendar.HOUR,
+ calendarClone.getActualMaximum(java.util.Calendar.HOUR));
+ calendarClone.set(java.util.Calendar.HOUR_OF_DAY,
+ calendarClone.getActualMaximum(java.util.Calendar.HOUR_OF_DAY));
+
+ return calendarClone.getTime();
+ }
+
+ /**
+ * Calculates the end time of the day using the given calendar and date
+ *
+ * @param date
+ * @param calendar
+ * the calendar instance to be used in the calculation. The given
+ * instance is unchanged in this operation.
+ * @return the given date, with time set to the end of the day
+ */
+ private static Date getStartOfDay(java.util.Calendar calendar, Date date) {
+ java.util.Calendar calendarClone = (java.util.Calendar) calendar
+ .clone();
+
+ calendarClone.setTime(date);
+ calendarClone.set(java.util.Calendar.MILLISECOND, 0);
+ calendarClone.set(java.util.Calendar.SECOND, 0);
+ calendarClone.set(java.util.Calendar.MINUTE, 0);
+ calendarClone.set(java.util.Calendar.HOUR, 0);
+ calendarClone.set(java.util.Calendar.HOUR_OF_DAY, 0);
+
+ return calendarClone.getTime();
+ }
+
+ /**
+ * Finds the first day of the week and returns a day representing the start
+ * of that day
+ *
+ * @param start
+ * The actual date
+ * @param expandToFullWeek
+ * Should the returned date be moved to the start of the week
+ * @return If expandToFullWeek is set then it returns the first day of the
+ * week, else it returns a clone of the actual date with the time
+ * set to the start of the day
+ */
+ protected Date expandStartDate(Date start, boolean expandToFullWeek) {
+ // If the duration is more than week, use monthly view and get startweek
+ // and endweek. Example if views daterange is from tuesday to next weeks
+ // wednesday->expand to monday to nextweeks sunday. If firstdayofweek =
+ // monday
+ if (expandToFullWeek) {
+ start = getFirstDateForWeek(start);
+
+ } else {
+ start = (Date) start.clone();
+ }
+
+ // Always expand to the start of the first day to the end of the last
+ // day
+ start = getStartOfDay(currentCalendar, start);
+
+ return start;
+ }
+
+ /**
+ * Finds the last day of the week and returns a day representing the end of
+ * that day
+ *
+ * @param end
+ * The actual date
+ * @param expandToFullWeek
+ * Should the returned date be moved to the end of the week
+ * @return If expandToFullWeek is set then it returns the last day of the
+ * week, else it returns a clone of the actual date with the time
+ * set to the end of the day
+ */
+ protected Date expandEndDate(Date end, boolean expandToFullWeek) {
+ // If the duration is more than week, use monthly view and get startweek
+ // and endweek. Example if views daterange is from tuesday to next weeks
+ // wednesday->expand to monday to nextweeks sunday. If firstdayofweek =
+ // monday
+ if (expandToFullWeek) {
+ end = getLastDateForWeek(end);
+
+ } else {
+ end = (Date) end.clone();
+ }
+
+ // Always expand to the start of the first day to the end of the last
+ // day
+ end = getEndOfDay(currentCalendar, end);
+
+ return end;
+ }
+
+ /**
+ * Set the {@link com.vaadin.addon.calendar.event.CalendarEventProvider
+ * CalendarEventProvider} to be used with this calendar. The EventProvider
+ * is used to query for events to show, and must be non-null. By default a
+ * {@link com.vaadin.addon.calendar.event.BasicEventProvider
+ * BasicEventProvider} is used.
+ *
+ * @param calendarEventProvider
+ * the calendarEventProvider to set. Cannot be null.
+ */
+ public void setEventProvider(CalendarEventProvider calendarEventProvider) {
+ if (calendarEventProvider == null) {
+ throw new IllegalArgumentException(
+ "Calendar event provider cannot be null");
+ }
+
+ // remove old listener
+ if (getEventProvider() instanceof EventSetChangeNotifier) {
+ ((EventSetChangeNotifier) getEventProvider())
+ .removeEventSetChangeListener(this);
+ }
+
+ this.calendarEventProvider = calendarEventProvider;
+
+ // add new listener
+ if (calendarEventProvider instanceof EventSetChangeNotifier) {
+ ((EventSetChangeNotifier) calendarEventProvider)
+ .addEventSetChangeListener(this);
+ }
+ }
+
+ /**
+ * @return the {@link com.vaadin.addon.calendar.event.CalendarEventProvider
+ * CalendarEventProvider} currently used
+ */
+ public CalendarEventProvider getEventProvider() {
+ return calendarEventProvider;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.addon.calendar.ui.CalendarEvents.EventChangeListener#
+ * eventChange (com.vaadin.addon.calendar.ui.CalendarEvents.EventChange)
+ */
+ @Override
+ public void eventSetChange(EventSetChangeEvent changeEvent) {
+ // sanity check
+ if (calendarEventProvider == changeEvent.getProvider()) {
+ markAsDirty();
+ }
+ }
+
+ /**
+ * Set the handler for the given type information. Mirrors
+ * {@link #addListener(String, Class, Object, Method) addListener} from
+ * AbstractComponent
+ *
+ * @param eventId
+ * A unique id for the event. Usually one of
+ * {@link CalendarEventId}
+ * @param eventType
+ * The class of the event, most likely a subclass of
+ * {@link CalendarComponentEvent}
+ * @param listener
+ * A listener that listens to the given event
+ * @param listenerMethod
+ * The method on the lister to call when the event is triggered
+ */
+ protected void setHandler(String eventId, Class<?> eventType,
+ EventListener listener, Method listenerMethod) {
+ if (handlers.get(eventId) != null) {
+ removeListener(eventId, eventType, handlers.get(eventId));
+ handlers.remove(eventId);
+ }
+
+ if (listener != null) {
+ addListener(eventId, eventType, listener, listenerMethod);
+ handlers.put(eventId, listener);
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.addon.calendar.ui.CalendarComponentEvents.NavigationNotifier
+ * #addListener
+ * (com.vaadin.addon.calendar.ui.CalendarComponentEvents.ForwardHandler)
+ */
+ @Override
+ public void setHandler(ForwardHandler listener) {
+ setHandler(ForwardEvent.EVENT_ID, ForwardEvent.class, listener,
+ ForwardHandler.forwardMethod);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.addon.calendar.ui.CalendarComponentEvents.NavigationNotifier
+ * #addListener
+ * (com.vaadin.addon.calendar.ui.CalendarComponentEvents.BackwardHandler)
+ */
+ @Override
+ public void setHandler(BackwardHandler listener) {
+ setHandler(BackwardEvent.EVENT_ID, BackwardEvent.class, listener,
+ BackwardHandler.backwardMethod);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.addon.calendar.ui.CalendarComponentEvents.NavigationNotifier
+ * #addListener
+ * (com.vaadin.addon.calendar.ui.CalendarComponentEvents.DateClickHandler)
+ */
+ @Override
+ public void setHandler(DateClickHandler listener) {
+ setHandler(DateClickEvent.EVENT_ID, DateClickEvent.class, listener,
+ DateClickHandler.dateClickMethod);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.addon.calendar.ui.CalendarComponentEvents.NavigationNotifier
+ * #addListener
+ * (com.vaadin.addon.calendar.ui.CalendarComponentEvents.EventClickHandler)
+ */
+ @Override
+ public void setHandler(EventClickHandler listener) {
+ setHandler(EventClick.EVENT_ID, EventClick.class, listener,
+ EventClickHandler.eventClickMethod);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.addon.calendar.ui.CalendarComponentEvents.NavigationNotifier
+ * #addListener
+ * (com.vaadin.addon.calendar.ui.CalendarComponentEvents.WeekClickHandler)
+ */
+ @Override
+ public void setHandler(WeekClickHandler listener) {
+ setHandler(WeekClick.EVENT_ID, WeekClick.class, listener,
+ WeekClickHandler.weekClickMethod);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.addon.calendar.ui.CalendarComponentEvents.EventResizeNotifier
+ * #addListener
+ * (com.vaadin.addon.calendar.ui.CalendarComponentEvents.EventResizeHandler
+ * )
+ */
+ @Override
+ public void setHandler(EventResizeHandler listener) {
+ setHandler(EventResize.EVENT_ID, EventResize.class, listener,
+ EventResizeHandler.eventResizeMethod);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.addon.calendar.ui.CalendarComponentEvents.RangeSelectNotifier
+ * #addListener
+ * (com.vaadin.addon.calendar.ui.CalendarComponentEvents.RangeSelectHandler
+ * )
+ */
+ @Override
+ public void setHandler(RangeSelectHandler listener) {
+ setHandler(RangeSelectEvent.EVENT_ID, RangeSelectEvent.class, listener,
+ RangeSelectHandler.rangeSelectMethod);
+
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.addon.calendar.ui.CalendarComponentEvents.EventMoveNotifier
+ * #addListener
+ * (com.vaadin.addon.calendar.ui.CalendarComponentEvents.EventMoveHandler)
+ */
+ @Override
+ public void setHandler(EventMoveHandler listener) {
+ setHandler(MoveEvent.EVENT_ID, MoveEvent.class, listener,
+ EventMoveHandler.eventMoveMethod);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.addon.calendar.ui.CalendarComponentEvents.
+ * CalendarEventNotifier #getHandler(java.lang.String)
+ */
+ @Override
+ public EventListener getHandler(String eventId) {
+ return handlers.get(eventId);
+ }
+
+ /**
+ * Get the currently active drop handler
+ */
+ @Override
+ public DropHandler getDropHandler() {
+ return dropHandler;
+ }
+
+ /**
+ * Set the drop handler for the calendar See {@link DropHandler} for
+ * implementation details.
+ *
+ * @param dropHandler
+ * The drop handler to set
+ */
+ public void setDropHandler(DropHandler dropHandler) {
+ this.dropHandler = dropHandler;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.event.dd.DropTarget#translateDropTargetDetails(java.util.Map)
+ */
+ @Override
+ public TargetDetails translateDropTargetDetails(
+ Map<String, Object> clientVariables) {
+ Map<String, Object> serverVariables = new HashMap<String, Object>();
+
+ if (clientVariables.containsKey("dropSlotIndex")) {
+ int slotIndex = (Integer) clientVariables.get("dropSlotIndex");
+ int dayIndex = (Integer) clientVariables.get("dropDayIndex");
+
+ currentCalendar.setTime(getStartOfDay(currentCalendar, startDate));
+ currentCalendar.add(java.util.Calendar.DATE, dayIndex);
+
+ // change this if slot length is modified
+ currentCalendar.add(java.util.Calendar.MINUTE, slotIndex * 30);
+
+ serverVariables.put("dropTime", currentCalendar.getTime());
+
+ } else {
+ int dayIndex = (Integer) clientVariables.get("dropDayIndex");
+ currentCalendar.setTime(expandStartDate(startDate, true));
+ currentCalendar.add(java.util.Calendar.DATE, dayIndex);
+ serverVariables.put("dropDay", currentCalendar.getTime());
+ }
+ serverVariables.put("mouseEvent", clientVariables.get("mouseEvent"));
+
+ CalendarTargetDetails td = new CalendarTargetDetails(serverVariables,
+ this);
+ td.setHasDropTime(clientVariables.containsKey("dropSlotIndex"));
+
+ return td;
+ }
+
+ /**
+ * Sets a container as a data source for the events in the calendar.
+ * Equivalent for doing
+ * <code>Calendar.setEventProvider(new ContainerEventProvider(container))</code>
+ *
+ * Use this method if you are adding a container which uses the default
+ * property ids like {@link BeanItemContainer} for instance. If you are
+ * using custom properties instead use
+ * {@link Calendar#setContainerDataSource(com.vaadin.v7.data.Container.Indexed, Object, Object, Object, Object, Object)}
+ *
+ * Please note that the container must be sorted by date!
+ *
+ * @param container
+ * The container to use as a datasource
+ */
+ public void setContainerDataSource(Container.Indexed container) {
+ ContainerEventProvider provider = new ContainerEventProvider(container);
+ provider.addEventSetChangeListener(
+ new CalendarEventProvider.EventSetChangeListener() {
+ @Override
+ public void eventSetChange(
+ EventSetChangeEvent changeEvent) {
+ // Repaint if events change
+ markAsDirty();
+ }
+ });
+ provider.addEventChangeListener(new EventChangeListener() {
+ @Override
+ public void eventChange(EventChangeEvent changeEvent) {
+ // Repaint if event changes
+ markAsDirty();
+ }
+ });
+ setEventProvider(provider);
+ }
+
+ /**
+ * Sets a container as a data source for the events in the calendar.
+ * Equivalent for doing
+ * <code>Calendar.setEventProvider(new ContainerEventProvider(container))</code>
+ *
+ * Please note that the container must be sorted by date!
+ *
+ * @param container
+ * The container to use as a data source
+ * @param captionProperty
+ * The property that has the caption, null if no caption property
+ * is present
+ * @param descriptionProperty
+ * The property that has the description, null if no description
+ * property is present
+ * @param startDateProperty
+ * The property that has the starting date
+ * @param endDateProperty
+ * The property that has the ending date
+ * @param styleNameProperty
+ * The property that has the stylename, null if no stylname
+ * property is present
+ */
+ public void setContainerDataSource(Container.Indexed container,
+ Object captionProperty, Object descriptionProperty,
+ Object startDateProperty, Object endDateProperty,
+ Object styleNameProperty) {
+ ContainerEventProvider provider = new ContainerEventProvider(container);
+ provider.setCaptionProperty(captionProperty);
+ provider.setDescriptionProperty(descriptionProperty);
+ provider.setStartDateProperty(startDateProperty);
+ provider.setEndDateProperty(endDateProperty);
+ provider.setStyleNameProperty(styleNameProperty);
+ provider.addEventSetChangeListener(
+ new CalendarEventProvider.EventSetChangeListener() {
+ @Override
+ public void eventSetChange(
+ EventSetChangeEvent changeEvent) {
+ // Repaint if events change
+ markAsDirty();
+ }
+ });
+ provider.addEventChangeListener(new EventChangeListener() {
+ @Override
+ public void eventChange(EventChangeEvent changeEvent) {
+ // Repaint if event changes
+ markAsDirty();
+ }
+ });
+ setEventProvider(provider);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.addon.calendar.event.CalendarEventProvider#getEvents(java.
+ * util.Date, java.util.Date)
+ */
+ @Override
+ public List<CalendarEvent> getEvents(Date startDate, Date endDate) {
+ return getEventProvider().getEvents(startDate, endDate);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.addon.calendar.event.CalendarEditableEventProvider#addEvent
+ * (com.vaadin.addon.calendar.event.CalendarEvent)
+ */
+ @Override
+ public void addEvent(CalendarEvent event) {
+ if (getEventProvider() instanceof CalendarEditableEventProvider) {
+ CalendarEditableEventProvider provider = (CalendarEditableEventProvider) getEventProvider();
+ provider.addEvent(event);
+ markAsDirty();
+ } else {
+ throw new UnsupportedOperationException(
+ "Event provider does not support adding events");
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.addon.calendar.event.CalendarEditableEventProvider#removeEvent
+ * (com.vaadin.addon.calendar.event.CalendarEvent)
+ */
+ @Override
+ public void removeEvent(CalendarEvent event) {
+ if (getEventProvider() instanceof CalendarEditableEventProvider) {
+ CalendarEditableEventProvider provider = (CalendarEditableEventProvider) getEventProvider();
+ provider.removeEvent(event);
+ markAsDirty();
+ } else {
+ throw new UnsupportedOperationException(
+ "Event provider does not support removing events");
+ }
+ }
+
+ /**
+ * Adds an action handler to the calender that handles event produced by the
+ * context menu.
+ *
+ * <p>
+ * The {@link Handler#getActions(Object, Object)} parameters depend on what
+ * view the Calendar is in:
+ * <ul>
+ * <li>If the Calendar is in <i>Day or Week View</i> then the target
+ * parameter will be a {@link CalendarDateRange} with a range of
+ * half-an-hour. The {@link Handler#getActions(Object, Object)} method will
+ * be called once per half-hour slot.</li>
+ * <li>If the Calendar is in <i>Month View</i> then the target parameter
+ * will be a {@link CalendarDateRange} with a range of one day. The
+ * {@link Handler#getActions(Object, Object)} will be called once for each
+ * day.
+ * </ul>
+ * The Dates passed into the {@link CalendarDateRange} are in the same
+ * timezone as the calendar is.
+ * </p>
+ *
+ * <p>
+ * The {@link Handler#handleAction(Action, Object, Object)} parameters
+ * depend on what the context menu is called upon:
+ * <ul>
+ * <li>If the context menu is called upon an event then the target parameter
+ * is the event, i.e. instanceof {@link CalendarEvent}</li>
+ * <li>If the context menu is called upon an empty slot then the target is a
+ * {@link Date} representing that slot
+ * </ul>
+ * </p>
+ */
+ @Override
+ public void addActionHandler(Handler actionHandler) {
+ if (actionHandler != null) {
+ if (actionHandlers == null) {
+ actionHandlers = new LinkedList<Action.Handler>();
+ actionMapper = new KeyMapper<Action>();
+ }
+ if (!actionHandlers.contains(actionHandler)) {
+ actionHandlers.add(actionHandler);
+ markAsDirty();
+ }
+ }
+ }
+
+ /**
+ * Is the calendar in a mode where all days of the month is shown
+ *
+ * @return Returns true if calendar is in monthly mode and false if it is in
+ * weekly mode
+ */
+ public boolean isMonthlyMode() {
+ CalendarState state = getState(false);
+ if (state.days != null) {
+ return state.days.size() > 7;
+ } else {
+ // Default mode
+ return true;
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.event.Action.Container#removeActionHandler(com.vaadin.event
+ * .Action.Handler)
+ */
+ @Override
+ public void removeActionHandler(Handler actionHandler) {
+ if (actionHandlers != null && actionHandlers.contains(actionHandler)) {
+ actionHandlers.remove(actionHandler);
+ if (actionHandlers.isEmpty()) {
+ actionHandlers = null;
+ actionMapper = null;
+ }
+ markAsDirty();
+ }
+ }
+
+ private class CalendarServerRpcImpl implements CalendarServerRpc {
+
+ @Override
+ public void eventMove(int eventIndex, String newDate) {
+ if (!isClientChangeAllowed()) {
+ return;
+ }
+ if (newDate != null) {
+ try {
+ Date d = df_date_time.parse(newDate);
+ if (eventIndex >= 0 && eventIndex < events.size()
+ && events.get(eventIndex) != null) {
+ fireEventMove(eventIndex, d);
+ }
+ } catch (ParseException e) {
+ getLogger().log(Level.WARNING, e.getMessage());
+ }
+ }
+ }
+
+ @Override
+ public void rangeSelect(String range) {
+ if (!isClientChangeAllowed()) {
+ return;
+ }
+
+ if (range != null && range.length() > 14 && range.contains("TO")) {
+ String[] dates = range.split("TO");
+ try {
+ Date d1 = df_date.parse(dates[0]);
+ Date d2 = df_date.parse(dates[1]);
+
+ fireRangeSelect(d1, d2, true);
+
+ } catch (ParseException e) {
+ // NOP
+ }
+ } else if (range != null && range.length() > 12
+ && range.contains(":")) {
+ String[] dates = range.split(":");
+ if (dates.length == 3) {
+ try {
+ Date d = df_date.parse(dates[0]);
+ currentCalendar.setTime(d);
+ int startMinutes = Integer.parseInt(dates[1]);
+ int endMinutes = Integer.parseInt(dates[2]);
+ currentCalendar.add(java.util.Calendar.MINUTE,
+ startMinutes);
+ Date start = currentCalendar.getTime();
+ currentCalendar.add(java.util.Calendar.MINUTE,
+ endMinutes - startMinutes);
+ Date end = currentCalendar.getTime();
+ fireRangeSelect(start, end, false);
+ } catch (ParseException e) {
+ // NOP
+ } catch (NumberFormatException e) {
+ // NOP
+ }
+ }
+ }
+ }
+
+ @Override
+ public void forward() {
+ fireEvent(new ForwardEvent(Calendar.this));
+ }
+
+ @Override
+ public void backward() {
+ fireEvent(new BackwardEvent(Calendar.this));
+ }
+
+ @Override
+ public void dateClick(String date) {
+ if (date != null && date.length() > 6) {
+ try {
+ Date d = df_date.parse(date);
+ fireDateClick(d);
+ } catch (ParseException e) {
+ }
+ }
+ }
+
+ @Override
+ public void weekClick(String event) {
+ if (event.length() > 0 && event.contains("w")) {
+ String[] splitted = event.split("w");
+ if (splitted.length == 2) {
+ try {
+ int yr = Integer.parseInt(splitted[0]);
+ int week = Integer.parseInt(splitted[1]);
+ fireWeekClick(week, yr);
+ } catch (NumberFormatException e) {
+ // NOP
+ }
+ }
+ }
+ }
+
+ @Override
+ public void eventClick(int eventIndex) {
+ if (!isEventClickAllowed()) {
+ return;
+ }
+ if (eventIndex >= 0 && eventIndex < events.size()
+ && events.get(eventIndex) != null) {
+ fireEventClick(eventIndex);
+ }
+ }
+
+ @Override
+ public void eventResize(int eventIndex, String newStartDate,
+ String newEndDate) {
+ if (!isClientChangeAllowed()) {
+ return;
+ }
+ if (newStartDate != null && !"".equals(newStartDate)
+ && newEndDate != null && !"".equals(newEndDate)) {
+ try {
+ Date newStartTime = df_date_time.parse(newStartDate);
+ Date newEndTime = df_date_time.parse(newEndDate);
+
+ fireEventResize(eventIndex, newStartTime, newEndTime);
+ } catch (ParseException e) {
+ // NOOP
+ }
+ }
+ }
+
+ @Override
+ public void scroll(int scrollPosition) {
+ scrollTop = scrollPosition;
+ markAsDirty();
+ }
+
+ @Override
+ public void actionOnEmptyCell(String actionKey, String startDate,
+ String endDate) {
+ Action action = actionMapper.get(actionKey);
+ SimpleDateFormat formatter = new SimpleDateFormat(
+ DateConstants.ACTION_DATE_FORMAT_PATTERN);
+ formatter.setTimeZone(getTimeZone());
+ try {
+ Date start = formatter.parse(startDate);
+ for (Action.Handler ah : actionHandlers) {
+ ah.handleAction(action, Calendar.this, start);
+ }
+
+ } catch (ParseException e) {
+ getLogger().log(Level.WARNING,
+ "Could not parse action date string");
+ }
+
+ }
+
+ @Override
+ public void actionOnEvent(String actionKey, String startDate,
+ String endDate, int eventIndex) {
+ Action action = actionMapper.get(actionKey);
+ SimpleDateFormat formatter = new SimpleDateFormat(
+ DateConstants.ACTION_DATE_FORMAT_PATTERN);
+ formatter.setTimeZone(getTimeZone());
+ for (Action.Handler ah : actionHandlers) {
+ ah.handleAction(action, Calendar.this, events.get(eventIndex));
+ }
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.server.VariableOwner#changeVariables(java.lang.Object,
+ * java.util.Map)
+ */
+ @Override
+ public void changeVariables(Object source, Map<String, Object> variables) {
+ /*
+ * Only defined to fulfill the LegacyComponent interface used for
+ * calendar drag & drop. No implementation required.
+ */
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.ui.LegacyComponent#paintContent(com.vaadin.server.PaintTarget)
+ */
+ @Override
+ public void paintContent(PaintTarget target) throws PaintException {
+ if (dropHandler != null) {
+ dropHandler.getAcceptCriterion().paint(target);
+ }
+ }
+
+ /**
+ * 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) {
+ getState().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 getState(false).eventCaptionAsHtml;
+ }
+
+ @Override
+ public void readDesign(Element design, DesignContext designContext) {
+ super.readDesign(design, designContext);
+
+ Attributes attr = design.attributes();
+ if (design.hasAttr("time-format")) {
+ setTimeFormat(TimeFormat.valueOf(
+ "Format" + design.attr("time-format").toUpperCase()));
+ }
+
+ if (design.hasAttr("start-date")) {
+ setStartDate(DesignAttributeHandler.readAttribute("start-date",
+ attr, Date.class));
+ }
+ if (design.hasAttr("end-date")) {
+ setEndDate(DesignAttributeHandler.readAttribute("end-date", attr,
+ Date.class));
+ }
+ };
+
+ @Override
+ public void writeDesign(Element design, DesignContext designContext) {
+ super.writeDesign(design, designContext);
+
+ if (currentTimeFormat != null) {
+ design.attr("time-format",
+ (currentTimeFormat == TimeFormat.Format12H ? "12h"
+ : "24h"));
+ }
+ if (startDate != null) {
+ design.attr("start-date", df_date.format(getStartDate()));
+ }
+ if (endDate != null) {
+ design.attr("end-date", df_date.format(getEndDate()));
+ }
+ if (!getTimeZone().equals(TimeZone.getDefault())) {
+ design.attr("time-zone", getTimeZone().getID());
+ }
+ }
+
+ @Override
+ protected Collection<String> getCustomAttributes() {
+ Collection<String> customAttributes = super.getCustomAttributes();
+ customAttributes.add("time-format");
+ customAttributes.add("start-date");
+ customAttributes.add("end-date");
+ return customAttributes;
+ }
+
+ /**
+ * Allow setting first day of week independent of Locale. Set to null if you
+ * want first day of week being defined by the locale
+ *
+ * @since 7.6
+ * @param dayOfWeek
+ * any of java.util.Calendar.SUNDAY..java.util.Calendar.SATURDAY
+ * or null to revert to default first day of week by locale
+ */
+ public void setFirstDayOfWeek(Integer dayOfWeek) {
+ int minimalSupported = java.util.Calendar.SUNDAY;
+ int maximalSupported = java.util.Calendar.SATURDAY;
+ if (dayOfWeek != null && (dayOfWeek < minimalSupported
+ || dayOfWeek > maximalSupported)) {
+ throw new IllegalArgumentException(String.format(
+ "Day of week must be between %s and %s. Actually received: %s",
+ minimalSupported, maximalSupported, dayOfWeek));
+ }
+ customFirstDayOfWeek = dayOfWeek;
+ markAsDirty();
+ }
+}
diff --git a/compatibility-server/src/main/java/com/vaadin/v7/ui/ColorPicker.java b/compatibility-server/src/main/java/com/vaadin/v7/ui/ColorPicker.java
new file mode 100644
index 0000000000..34b03a2447
--- /dev/null
+++ b/compatibility-server/src/main/java/com/vaadin/v7/ui/ColorPicker.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.v7.ui;
+
+import com.vaadin.shared.ui.colorpicker.Color;
+
+/**
+ * A class that defines default (button-like) implementation for a color picker
+ * component.
+ *
+ * @since 7.0.0
+ *
+ * @see ColorPickerArea
+ *
+ */
+public class ColorPicker extends AbstractColorPicker {
+
+ /**
+ * Instantiates a new color picker.
+ */
+ public ColorPicker() {
+ super();
+ }
+
+ /**
+ * Instantiates a new color picker.
+ *
+ * @param popupCaption
+ * caption of the color select popup
+ */
+ public ColorPicker(String popupCaption) {
+ super(popupCaption);
+ }
+
+ /**
+ * Instantiates a new color picker.
+ *
+ * @param popupCaption
+ * caption of the color select popup
+ * @param initialColor
+ * the initial color
+ */
+ public ColorPicker(String popupCaption, Color initialColor) {
+ super(popupCaption, initialColor);
+ setDefaultCaptionEnabled(true);
+ }
+
+ @Override
+ protected void setDefaultStyles() {
+ setPrimaryStyleName(STYLENAME_BUTTON);
+ addStyleName(STYLENAME_DEFAULT);
+ }
+
+}
diff --git a/compatibility-server/src/main/java/com/vaadin/v7/ui/ColorPickerArea.java b/compatibility-server/src/main/java/com/vaadin/v7/ui/ColorPickerArea.java
new file mode 100644
index 0000000000..624d567ec7
--- /dev/null
+++ b/compatibility-server/src/main/java/com/vaadin/v7/ui/ColorPickerArea.java
@@ -0,0 +1,77 @@
+/*
+ * 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.v7.ui;
+
+import com.vaadin.shared.ui.colorpicker.Color;
+
+/**
+ * A class that defines area-like implementation for a color picker component.
+ *
+ * @since 7.0.0
+ *
+ * @see ColorPicker
+ *
+ */
+public class ColorPickerArea extends AbstractColorPicker {
+
+ /**
+ * Instantiates a new color picker.
+ */
+ public ColorPickerArea() {
+ super();
+ }
+
+ /**
+ * Instantiates a new color picker.
+ *
+ * @param popupCaption
+ * caption of the color select popup
+ */
+ public ColorPickerArea(String popupCaption) {
+ super(popupCaption);
+ }
+
+ /**
+ * Instantiates a new color picker.
+ *
+ * @param popupCaption
+ * caption of the color select popup
+ * @param initialColor
+ * the initial color
+ */
+ public ColorPickerArea(String popupCaption, Color initialColor) {
+ super(popupCaption, initialColor);
+ setDefaultCaptionEnabled(false);
+ }
+
+ @Override
+ protected void setDefaultStyles() {
+ // state already has correct default
+ }
+
+ @Override
+ public void beforeClientResponse(boolean initial) {
+ super.beforeClientResponse(initial);
+
+ if ("".equals(getState().height)) {
+ getState().height = "30px";
+ }
+ if ("".equals(getState().width)) {
+ getState().width = "30px";
+ }
+ }
+
+}
diff --git a/compatibility-server/src/main/java/com/vaadin/v7/ui/ComboBox.java b/compatibility-server/src/main/java/com/vaadin/v7/ui/ComboBox.java
new file mode 100644
index 0000000000..50b6ac505c
--- /dev/null
+++ b/compatibility-server/src/main/java/com/vaadin/v7/ui/ComboBox.java
@@ -0,0 +1,926 @@
+/*
+ * 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.v7.ui;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+import com.vaadin.event.FieldEvents;
+import com.vaadin.event.FieldEvents.BlurEvent;
+import com.vaadin.event.FieldEvents.BlurListener;
+import com.vaadin.event.FieldEvents.FocusAndBlurServerRpcImpl;
+import com.vaadin.event.FieldEvents.FocusEvent;
+import com.vaadin.event.FieldEvents.FocusListener;
+import com.vaadin.server.PaintException;
+import com.vaadin.server.PaintTarget;
+import com.vaadin.server.Resource;
+import com.vaadin.shared.ui.combobox.ComboBoxServerRpc;
+import com.vaadin.shared.ui.combobox.ComboBoxState;
+import com.vaadin.shared.ui.combobox.FilteringMode;
+import com.vaadin.ui.Component;
+import com.vaadin.v7.data.Container;
+import com.vaadin.v7.data.util.filter.SimpleStringFilter;
+
+/**
+ * A filtering dropdown single-select. Suitable for newItemsAllowed, but it's
+ * turned of by default to avoid mistakes. Items are filtered based on user
+ * input, and loaded dynamically ("lazy-loading") from the server. You can turn
+ * on newItemsAllowed and change filtering mode (and also turn it off), but you
+ * can not turn on multi-select mode.
+ *
+ */
+@SuppressWarnings("serial")
+public class ComboBox extends AbstractSelect
+ implements AbstractSelect.Filtering, FieldEvents.BlurNotifier,
+ FieldEvents.FocusNotifier {
+
+ /**
+ * ItemStyleGenerator can be used to add custom styles to combo box items
+ * shown in the popup. The CSS class name that will be added to the item
+ * style names is <tt>v-filterselect-item-[style name]</tt>.
+ *
+ * @since 7.5.6
+ * @see ComboBox#setItemStyleGenerator(ItemStyleGenerator)
+ */
+ public interface ItemStyleGenerator extends Serializable {
+
+ /**
+ * Called by ComboBox when an item is painted.
+ *
+ * @param source
+ * the source combo box
+ * @param itemId
+ * The itemId of the item to be painted. Can be
+ * <code>null</code> if null selection is allowed.
+ * @return The style name to add to this item. (the CSS class name will
+ * be v-filterselect-item-[style name]
+ */
+ public String getStyle(ComboBox source, Object itemId);
+ }
+
+ private ComboBoxServerRpc rpc = new ComboBoxServerRpc() {
+ @Override
+ public void createNewItem(String itemValue) {
+ if (isNewItemsAllowed()) {
+ // New option entered (and it is allowed)
+ if (itemValue != null && itemValue.length() > 0) {
+ getNewItemHandler().addNewItem(itemValue);
+ // rebuild list
+ filterstring = null;
+ prevfilterstring = null;
+ }
+ }
+ }
+
+ @Override
+ public void setSelectedItem(String item) {
+ if (item == null) {
+ setValue(null, true);
+ } else {
+ final Object id = itemIdMapper.get(item);
+ if (id != null && id.equals(getNullSelectionItemId())) {
+ setValue(null, true);
+ } else {
+ setValue(id, true);
+ }
+ }
+ }
+
+ @Override
+ public void requestPage(String filter, int page) {
+ filterstring = filter;
+ if (filterstring != null) {
+ filterstring = filterstring.toLowerCase(getLocale());
+ }
+ currentPage = page;
+
+ // TODO this should trigger a data-only update instead of a full
+ // repaint
+ requestRepaint();
+ }
+ };
+
+ FocusAndBlurServerRpcImpl focusBlurRpc = new FocusAndBlurServerRpcImpl(
+ this) {
+ @Override
+ protected void fireEvent(Component.Event event) {
+ ComboBox.this.fireEvent(event);
+ }
+ };
+
+ // Current page when the user is 'paging' trough options
+ private int currentPage = -1;
+
+ private String filterstring;
+ private String prevfilterstring;
+
+ /**
+ * Number of options that pass the filter, excluding the null item if any.
+ */
+ private int filteredSize;
+
+ /**
+ * Cache of filtered options, used only by the in-memory filtering system.
+ */
+ private List<Object> filteredOptions;
+
+ /**
+ * Flag to indicate that request repaint is called by filter request only
+ */
+ private boolean optionRequest;
+
+ /**
+ * True while painting to suppress item set change notifications that could
+ * be caused by temporary filtering.
+ */
+ private boolean isPainting;
+
+ /**
+ * Flag to indicate whether to scroll the selected item visible (select the
+ * page on which it is) when opening the popup or not. Only applies to
+ * single select mode.
+ *
+ * This requires finding the index of the item, which can be expensive in
+ * many large lazy loading containers.
+ */
+ private boolean scrollToSelectedItem = true;
+
+ private ItemStyleGenerator itemStyleGenerator = null;
+
+ public ComboBox() {
+ init();
+ }
+
+ public ComboBox(String caption, Collection<?> options) {
+ super(caption, options);
+ init();
+ }
+
+ public ComboBox(String caption, Container dataSource) {
+ super(caption, dataSource);
+ init();
+ }
+
+ public ComboBox(String caption) {
+ super(caption);
+ init();
+ }
+
+ /**
+ * Initialize the ComboBox with default settings and register client to
+ * server RPC implementation.
+ */
+ private void init() {
+ registerRpc(rpc);
+ registerRpc(focusBlurRpc);
+
+ setNewItemsAllowed(false);
+ setImmediate(true);
+ }
+
+ /**
+ * Gets the current input prompt.
+ *
+ * @see #setInputPrompt(String)
+ * @return the current input prompt, or null if not enabled
+ */
+ public String getInputPrompt() {
+ return getState(false).inputPrompt;
+ }
+
+ /**
+ * Sets the input prompt - a textual prompt that is displayed when the
+ * select would otherwise be empty, to prompt the user for input.
+ *
+ * @param inputPrompt
+ * the desired input prompt, or null to disable
+ */
+ public void setInputPrompt(String inputPrompt) {
+ getState().inputPrompt = inputPrompt;
+ }
+
+ private boolean isFilteringNeeded() {
+ return filterstring != null && filterstring.length() > 0
+ && getFilteringMode() != FilteringMode.OFF;
+ }
+
+ /**
+ * A class representing an item in a ComboBox for server to client
+ * communication. This class is for internal use only and subject to change.
+ *
+ * @since
+ */
+ private static class ComboBoxItem implements Serializable {
+ String key = "";
+ String caption = "";
+ String style = null;
+ Resource icon = null;
+
+ // constructor for a null item
+ public ComboBoxItem() {
+ }
+
+ public ComboBoxItem(String key, String caption, String style,
+ Resource icon) {
+ this.key = key;
+ this.caption = caption;
+ this.style = style;
+ this.icon = icon;
+ }
+ }
+
+ @Override
+ public void paintContent(PaintTarget target) throws PaintException {
+ isPainting = true;
+ try {
+ // clear caption change listeners
+ getCaptionChangeListener().clear();
+
+ if (isNewItemsAllowed()) {
+ target.addAttribute("allownewitem", true);
+ }
+
+ boolean needNullSelectOption = false;
+ if (isNullSelectionAllowed()) {
+ target.addAttribute("nullselect", true);
+ needNullSelectOption = (getNullSelectionItemId() == null);
+ if (!needNullSelectOption) {
+ target.addAttribute("nullselectitem", true);
+ }
+ }
+
+ // Constructs selected keys array
+ String[] selectedKeys = new String[(getValue() == null
+ && getNullSelectionItemId() == null ? 0 : 1)];
+
+ // Paints the options and create array of selected id keys
+ int keyIndex = 0;
+
+ if (currentPage < 0) {
+ optionRequest = false;
+ currentPage = 0;
+ filterstring = "";
+ }
+
+ boolean nullFilteredOut = isFilteringNeeded();
+ // null option is needed and not filtered out, even if not on
+ // current page
+ boolean nullOptionVisible = needNullSelectOption
+ && !nullFilteredOut;
+
+ // first try if using container filters is possible
+ List<?> options = getOptionsWithFilter(nullOptionVisible);
+ if (null == options) {
+ // not able to use container filters, perform explicit in-memory
+ // filtering
+ options = getFilteredOptions();
+ filteredSize = options.size();
+ options = sanitetizeList(options, nullOptionVisible);
+ }
+
+ final boolean paintNullSelection = needNullSelectOption
+ && currentPage == 0 && !nullFilteredOut;
+
+ List<ComboBoxItem> items = new ArrayList<ComboBoxItem>();
+
+ if (paintNullSelection) {
+ ComboBoxItem item = new ComboBoxItem();
+ item.style = getItemStyle(null);
+ items.add(item);
+ }
+
+ final Iterator<?> i = options.iterator();
+ // Paints the available selection options from data source
+
+ while (i.hasNext()) {
+
+ final Object id = i.next();
+
+ if (!isNullSelectionAllowed() && id != null
+ && id.equals(getNullSelectionItemId())
+ && !isSelected(id)) {
+ continue;
+ }
+
+ // Gets the option attribute values
+ final String key = itemIdMapper.key(id);
+ final String caption = getItemCaption(id);
+ final Resource icon = getItemIcon(id);
+
+ getCaptionChangeListener().addNotifierForItem(id);
+
+ // Prepare to paint the option
+ ComboBoxItem item = new ComboBoxItem(key, caption,
+ getItemStyle(id), icon);
+ items.add(item);
+ if (keyIndex < selectedKeys.length && isSelected(id)) {
+ // at most one item can be selected at a time
+ selectedKeys[keyIndex++] = key;
+ }
+ }
+
+ // paint the items
+ target.startTag("options");
+ for (ComboBoxItem item : items) {
+ target.startTag("so");
+ if (item.icon != null) {
+ target.addAttribute("icon", item.icon);
+ }
+ target.addAttribute("caption", item.caption);
+ target.addAttribute("key", item.key);
+ if (item.style != null) {
+ target.addAttribute("style", item.style);
+ }
+
+ target.endTag("so");
+ }
+ target.endTag("options");
+
+ target.addAttribute("totalitems",
+ size() + (needNullSelectOption ? 1 : 0));
+ if (filteredSize > 0 || nullOptionVisible) {
+ target.addAttribute("totalMatches",
+ filteredSize + (nullOptionVisible ? 1 : 0));
+ }
+
+ // Paint variables
+ target.addVariable(this, "selected", selectedKeys);
+ if (getValue() != null && selectedKeys[0] == null) {
+ // not always available, e.g. scrollToSelectedIndex=false
+ // Give the caption for selected item still, not to make it look
+ // like there is no selection at all
+ target.addAttribute("selectedCaption",
+ getItemCaption(getValue()));
+ }
+ if (isNewItemsAllowed()) {
+ target.addVariable(this, "newitem", "");
+ }
+
+ target.addVariable(this, "filter", filterstring);
+ target.addVariable(this, "page", currentPage);
+
+ currentPage = -1; // current page is always set by client
+
+ optionRequest = true;
+ } finally {
+ isPainting = false;
+ }
+
+ }
+
+ private String getItemStyle(Object itemId) throws PaintException {
+ if (itemStyleGenerator != null) {
+ return itemStyleGenerator.getStyle(this, itemId);
+ }
+ return null;
+ }
+
+ /**
+ * Sets whether it is possible to input text into the field or whether the
+ * field area of the component is just used to show what is selected. By
+ * disabling text input, the comboBox will work in the same way as a
+ * {@link NativeSelect}
+ *
+ * @see #isTextInputAllowed()
+ *
+ * @param textInputAllowed
+ * true to allow entering text, false to just show the current
+ * selection
+ */
+ public void setTextInputAllowed(boolean textInputAllowed) {
+ getState().textInputAllowed = textInputAllowed;
+ }
+
+ /**
+ * Returns true if the user can enter text into the field to either filter
+ * the selections or enter a new value if {@link #isNewItemsAllowed()}
+ * returns true. If text input is disabled, the comboBox will work in the
+ * same way as a {@link NativeSelect}
+ *
+ * @return
+ */
+ public boolean isTextInputAllowed() {
+ return getState(false).textInputAllowed;
+ }
+
+ @Override
+ protected ComboBoxState getState() {
+ return (ComboBoxState) super.getState();
+ }
+
+ @Override
+ protected ComboBoxState getState(boolean markAsDirty) {
+ return (ComboBoxState) super.getState(markAsDirty);
+ }
+
+ /**
+ * Returns the filtered options for the current page using a container
+ * filter.
+ *
+ * As a size effect, {@link #filteredSize} is set to the total number of
+ * items passing the filter.
+ *
+ * The current container must be {@link Filterable} and {@link Indexed}, and
+ * the filtering mode must be suitable for container filtering (tested with
+ * {@link #canUseContainerFilter()}).
+ *
+ * Use {@link #getFilteredOptions()} and
+ * {@link #sanitetizeList(List, boolean)} if this is not the case.
+ *
+ * @param needNullSelectOption
+ * @return filtered list of options (may be empty) or null if cannot use
+ * container filters
+ */
+ protected List<?> getOptionsWithFilter(boolean needNullSelectOption) {
+ Container container = getContainerDataSource();
+
+ if (getPageLength() == 0 && !isFilteringNeeded()) {
+ // no paging or filtering: return all items
+ filteredSize = container.size();
+ assert filteredSize >= 0;
+ return new ArrayList<Object>(container.getItemIds());
+ }
+
+ if (!(container instanceof Filterable)
+ || !(container instanceof Indexed)
+ || getItemCaptionMode() != ITEM_CAPTION_MODE_PROPERTY) {
+ return null;
+ }
+
+ Filterable filterable = (Filterable) container;
+
+ Filter filter = buildFilter(filterstring, getFilteringMode());
+
+ // adding and removing filters leads to extraneous item set
+ // change events from the underlying container, but the ComboBox does
+ // not process or propagate them based on the flag filteringContainer
+ if (filter != null) {
+ filterable.addContainerFilter(filter);
+ }
+
+ // try-finally to ensure that the filter is removed from container even
+ // if a exception is thrown...
+ try {
+ Indexed indexed = (Indexed) container;
+
+ int indexToEnsureInView = -1;
+
+ // if not an option request (item list when user changes page), go
+ // to page with the selected item after filtering if accepted by
+ // filter
+ Object selection = getValue();
+ if (isScrollToSelectedItem() && !optionRequest
+ && selection != null) {
+ // ensure proper page
+ indexToEnsureInView = indexed.indexOfId(selection);
+ }
+
+ filteredSize = container.size();
+ assert filteredSize >= 0;
+ currentPage = adjustCurrentPage(currentPage, needNullSelectOption,
+ indexToEnsureInView, filteredSize);
+ int first = getFirstItemIndexOnCurrentPage(needNullSelectOption,
+ filteredSize);
+ int last = getLastItemIndexOnCurrentPage(needNullSelectOption,
+ filteredSize, first);
+
+ // Compute the number of items to fetch from the indexes given or
+ // based on the filtered size of the container
+ int lastItemToFetch = Math.min(last, filteredSize - 1);
+ int nrOfItemsToFetch = (lastItemToFetch + 1) - first;
+
+ List<?> options = indexed.getItemIds(first, nrOfItemsToFetch);
+
+ return options;
+ } finally {
+ // to the outside, filtering should not be visible
+ if (filter != null) {
+ filterable.removeContainerFilter(filter);
+ }
+ }
+ }
+
+ /**
+ * Constructs a filter instance to use when using a Filterable container in
+ * the <code>ITEM_CAPTION_MODE_PROPERTY</code> mode.
+ *
+ * Note that the client side implementation expects the filter string to
+ * apply to the item caption string it sees, so changing the behavior of
+ * this method can cause problems.
+ *
+ * @param filterString
+ * @param filteringMode
+ * @return
+ */
+ protected Filter buildFilter(String filterString,
+ FilteringMode filteringMode) {
+ Filter filter = null;
+
+ if (null != filterString && !"".equals(filterString)) {
+ switch (filteringMode) {
+ case OFF:
+ break;
+ case STARTSWITH:
+ filter = new SimpleStringFilter(getItemCaptionPropertyId(),
+ filterString, true, true);
+ break;
+ case CONTAINS:
+ filter = new SimpleStringFilter(getItemCaptionPropertyId(),
+ filterString, true, false);
+ break;
+ }
+ }
+ return filter;
+ }
+
+ @Override
+ public void containerItemSetChange(Container.ItemSetChangeEvent event) {
+ if (!isPainting) {
+ super.containerItemSetChange(event);
+ }
+ }
+
+ /**
+ * Makes correct sublist of given list of options.
+ *
+ * If paint is not an option request (affected by page or filter change),
+ * page will be the one where possible selection exists.
+ *
+ * Detects proper first and last item in list to return right page of
+ * options. Also, if the current page is beyond the end of the list, it will
+ * be adjusted.
+ *
+ * @param options
+ * @param needNullSelectOption
+ * flag to indicate if nullselect option needs to be taken into
+ * consideration
+ */
+ private List<?> sanitetizeList(List<?> options,
+ boolean needNullSelectOption) {
+
+ if (getPageLength() != 0 && options.size() > getPageLength()) {
+
+ int indexToEnsureInView = -1;
+
+ // if not an option request (item list when user changes page), go
+ // to page with the selected item after filtering if accepted by
+ // filter
+ Object selection = getValue();
+ if (isScrollToSelectedItem() && !optionRequest
+ && selection != null) {
+ // ensure proper page
+ indexToEnsureInView = options.indexOf(selection);
+ }
+
+ int size = options.size();
+ currentPage = adjustCurrentPage(currentPage, needNullSelectOption,
+ indexToEnsureInView, size);
+ int first = getFirstItemIndexOnCurrentPage(needNullSelectOption,
+ size);
+ int last = getLastItemIndexOnCurrentPage(needNullSelectOption, size,
+ first);
+ return options.subList(first, last + 1);
+ } else {
+ return options;
+ }
+ }
+
+ /**
+ * Returns the index of the first item on the current page. The index is to
+ * the underlying (possibly filtered) contents. The null item, if any, does
+ * not have an index but takes up a slot on the first page.
+ *
+ * @param needNullSelectOption
+ * true if a null option should be shown before any other options
+ * (takes up the first slot on the first page, not counted in
+ * index)
+ * @param size
+ * number of items after filtering (not including the null item,
+ * if any)
+ * @return first item to show on the UI (index to the filtered list of
+ * options, not taking the null item into consideration if any)
+ */
+ private int getFirstItemIndexOnCurrentPage(boolean needNullSelectOption,
+ int size) {
+ // Not all options are visible, find out which ones are on the
+ // current "page".
+ int first = currentPage * getPageLength();
+ if (needNullSelectOption && currentPage > 0) {
+ first--;
+ }
+ return first;
+ }
+
+ /**
+ * Returns the index of the last item on the current page. The index is to
+ * the underlying (possibly filtered) contents. If needNullSelectOption is
+ * true, the null item takes up the first slot on the first page,
+ * effectively reducing the first page size by one.
+ *
+ * @param needNullSelectOption
+ * true if a null option should be shown before any other options
+ * (takes up the first slot on the first page, not counted in
+ * index)
+ * @param size
+ * number of items after filtering (not including the null item,
+ * if any)
+ * @param first
+ * index in the filtered view of the first item of the page
+ * @return index in the filtered view of the last item on the page
+ */
+ private int getLastItemIndexOnCurrentPage(boolean needNullSelectOption,
+ int size, int first) {
+ // page length usable for non-null items
+ int effectivePageLength = getPageLength()
+ - (needNullSelectOption && (currentPage == 0) ? 1 : 0);
+ return Math.min(size - 1, first + effectivePageLength - 1);
+ }
+
+ /**
+ * Adjusts the index of the current page if necessary: make sure the current
+ * page is not after the end of the contents, and optionally go to the page
+ * containg a specific item. There are no side effects but the adjusted page
+ * index is returned.
+ *
+ * @param page
+ * page number to use as the starting point
+ * @param needNullSelectOption
+ * true if a null option should be shown before any other options
+ * (takes up the first slot on the first page, not counted in
+ * index)
+ * @param indexToEnsureInView
+ * index of an item that should be included on the page (in the
+ * data set, not counting the null item if any), -1 for none
+ * @param size
+ * number of items after filtering (not including the null item,
+ * if any)
+ */
+ private int adjustCurrentPage(int page, boolean needNullSelectOption,
+ int indexToEnsureInView, int size) {
+ if (indexToEnsureInView != -1) {
+ int newPage = (indexToEnsureInView + (needNullSelectOption ? 1 : 0))
+ / getPageLength();
+ page = newPage;
+ }
+ // adjust the current page if beyond the end of the list
+ if (page * getPageLength() > size) {
+ page = (size + (needNullSelectOption ? 1 : 0)) / getPageLength();
+ }
+ return page;
+ }
+
+ /**
+ * Filters the options in memory and returns the full filtered list.
+ *
+ * This can be less efficient than using container filters, so use
+ * {@link #getOptionsWithFilter(boolean)} if possible (filterable container
+ * and suitable item caption mode etc.).
+ *
+ * @return
+ */
+ protected List<?> getFilteredOptions() {
+ if (!isFilteringNeeded()) {
+ prevfilterstring = null;
+ filteredOptions = new LinkedList<Object>(getItemIds());
+ return filteredOptions;
+ }
+
+ if (filterstring.equals(prevfilterstring)) {
+ return filteredOptions;
+ }
+
+ Collection<?> items;
+ if (prevfilterstring != null
+ && filterstring.startsWith(prevfilterstring)) {
+ items = filteredOptions;
+ } else {
+ items = getItemIds();
+ }
+ prevfilterstring = filterstring;
+
+ filteredOptions = new LinkedList<Object>();
+ for (final Iterator<?> it = items.iterator(); it.hasNext();) {
+ final Object itemId = it.next();
+ String caption = getItemCaption(itemId);
+ if (caption == null || caption.equals("")) {
+ continue;
+ } else {
+ caption = caption.toLowerCase(getLocale());
+ }
+ switch (getFilteringMode()) {
+ case CONTAINS:
+ if (caption.indexOf(filterstring) > -1) {
+ filteredOptions.add(itemId);
+ }
+ break;
+ case STARTSWITH:
+ default:
+ if (caption.startsWith(filterstring)) {
+ filteredOptions.add(itemId);
+ }
+ break;
+ }
+ }
+
+ return filteredOptions;
+ }
+
+ /**
+ * Invoked when the value of a variable has changed.
+ *
+ * @see com.vaadin.ui.AbstractComponent#changeVariables(java.lang.Object,
+ * java.util.Map)
+ */
+ @Override
+ public void changeVariables(Object source, Map<String, Object> variables) {
+ // Not calling super.changeVariables due the history of select
+ // component hierarchy
+
+ // all the client to server requests are now handled by RPC
+ }
+
+ @Override
+ public void setFilteringMode(FilteringMode filteringMode) {
+ getState().filteringMode = filteringMode;
+ }
+
+ @Override
+ public FilteringMode getFilteringMode() {
+ return getState(false).filteringMode;
+ }
+
+ @Override
+ public void addBlurListener(BlurListener listener) {
+ addListener(BlurEvent.EVENT_ID, BlurEvent.class, listener,
+ BlurListener.blurMethod);
+ }
+
+ @Override
+ public void removeBlurListener(BlurListener listener) {
+ removeListener(BlurEvent.EVENT_ID, BlurEvent.class, listener);
+ }
+
+ @Override
+ public void addFocusListener(FocusListener listener) {
+ addListener(FocusEvent.EVENT_ID, FocusEvent.class, listener,
+ FocusListener.focusMethod);
+ }
+
+ @Override
+ public void removeFocusListener(FocusListener listener) {
+ removeListener(FocusEvent.EVENT_ID, FocusEvent.class, listener);
+ }
+
+ /**
+ * ComboBox does not support multi select mode.
+ *
+ * @deprecated As of 7.0, use {@link ListSelect}, {@link OptionGroup} or
+ * {@link TwinColSelect} instead
+ * @see com.vaadin.v7.ui.AbstractSelect#setMultiSelect(boolean)
+ * @throws UnsupportedOperationException
+ * if trying to activate multiselect mode
+ */
+ @Deprecated
+ @Override
+ public void setMultiSelect(boolean multiSelect) {
+ if (multiSelect) {
+ throw new UnsupportedOperationException(
+ "Multiselect not supported");
+ }
+ }
+
+ /**
+ * ComboBox does not support multi select mode.
+ *
+ * @deprecated As of 7.0, use {@link ListSelect}, {@link OptionGroup} or
+ * {@link TwinColSelect} instead
+ *
+ * @see com.vaadin.v7.ui.AbstractSelect#isMultiSelect()
+ *
+ * @return false
+ */
+ @Deprecated
+ @Override
+ public boolean isMultiSelect() {
+ return false;
+ }
+
+ /**
+ * Returns the page length of the suggestion popup.
+ *
+ * @return the pageLength
+ */
+ public int getPageLength() {
+ return getState(false).pageLength;
+ }
+
+ /**
+ * Returns the suggestion pop-up's width as a CSS string.
+ *
+ * @see #setPopupWidth
+ * @since 7.7
+ */
+ public String getPopupWidth() {
+ return getState(false).suggestionPopupWidth;
+ }
+
+ /**
+ * Sets the page length for the suggestion popup. Setting the page length to
+ * 0 will disable suggestion popup paging (all items visible).
+ *
+ * @param pageLength
+ * the pageLength to set
+ */
+ public void setPageLength(int pageLength) {
+ getState().pageLength = pageLength;
+ }
+
+ /**
+ * Sets the suggestion pop-up's width as a CSS string. By using relative
+ * units (e.g. "50%") it's possible to set the popup's width relative to the
+ * ComboBox itself.
+ *
+ * @see #getPopupWidth()
+ * @since 7.7
+ * @param width
+ * the width
+ */
+ public void setPopupWidth(String width) {
+ getState().suggestionPopupWidth = width;
+ }
+
+ /**
+ * Sets whether to scroll the selected item visible (directly open the page
+ * on which it is) when opening the combo box popup or not. Only applies to
+ * single select mode.
+ *
+ * This requires finding the index of the item, which can be expensive in
+ * many large lazy loading containers.
+ *
+ * @param scrollToSelectedItem
+ * true to find the page with the selected item when opening the
+ * selection popup
+ */
+ public void setScrollToSelectedItem(boolean scrollToSelectedItem) {
+ this.scrollToSelectedItem = scrollToSelectedItem;
+ }
+
+ /**
+ * Returns true if the select should find the page with the selected item
+ * when opening the popup (single select combo box only).
+ *
+ * @see #setScrollToSelectedItem(boolean)
+ *
+ * @return true if the page with the selected item will be shown when
+ * opening the popup
+ */
+ public boolean isScrollToSelectedItem() {
+ return scrollToSelectedItem;
+ }
+
+ /**
+ * Sets the item style generator that is used to produce custom styles for
+ * showing items in the popup. The CSS class name that will be added to the
+ * item style names is <tt>v-filterselect-item-[style name]</tt>.
+ *
+ * @param itemStyleGenerator
+ * the item style generator to set, or <code>null</code> to not
+ * use any custom item styles
+ * @since 7.5.6
+ */
+ public void setItemStyleGenerator(ItemStyleGenerator itemStyleGenerator) {
+ this.itemStyleGenerator = itemStyleGenerator;
+ markAsDirty();
+ }
+
+ /**
+ * Gets the currently used item style generator.
+ *
+ * @return the itemStyleGenerator the currently used item style generator,
+ * or <code>null</code> if no generator is used
+ * @since 7.5.6
+ */
+ public ItemStyleGenerator getItemStyleGenerator() {
+ return itemStyleGenerator;
+ }
+
+}
diff --git a/compatibility-server/src/main/java/com/vaadin/v7/ui/LegacyDateField.java b/compatibility-server/src/main/java/com/vaadin/v7/ui/DateField.java
index 0d094c28bf..b02e7b87a4 100644
--- a/compatibility-server/src/main/java/com/vaadin/v7/ui/LegacyDateField.java
+++ b/compatibility-server/src/main/java/com/vaadin/v7/ui/DateField.java
@@ -27,7 +27,6 @@ import java.util.logging.Logger;
import org.jsoup.nodes.Element;
-import com.vaadin.data.Property;
import com.vaadin.event.FieldEvents;
import com.vaadin.event.FieldEvents.BlurEvent;
import com.vaadin.event.FieldEvents.BlurListener;
@@ -42,10 +41,11 @@ import com.vaadin.ui.Component;
import com.vaadin.ui.LegacyComponent;
import com.vaadin.ui.declarative.DesignAttributeHandler;
import com.vaadin.ui.declarative.DesignContext;
+import com.vaadin.v7.data.Property;
import com.vaadin.v7.data.Validator;
import com.vaadin.v7.data.Validator.InvalidValueException;
-import com.vaadin.v7.data.util.converter.LegacyConverter;
-import com.vaadin.v7.data.validator.LegacyDateRangeValidator;
+import com.vaadin.v7.data.util.converter.Converter;
+import com.vaadin.v7.data.validator.DateRangeValidator;
/**
* <p>
@@ -54,11 +54,11 @@ import com.vaadin.v7.data.validator.LegacyDateRangeValidator;
* </p>
* <p>
* Since <code>DateField</code> extends <code>LegacyAbstractField</code> it
- * implements the {@link com.vaadin.data.Buffered}interface.
+ * implements the {@link com.vaadin.v7.data.Buffered}interface.
* </p>
* <p>
* A <code>DateField</code> is in write-through mode by default, so
- * {@link com.vaadin.v7.ui.LegacyAbstractField#setWriteThrough(boolean)}must
+ * {@link com.vaadin.v7.ui.AbstractField#setWriteThrough(boolean)}must
* be called to enable buffering.
* </p>
*
@@ -66,7 +66,7 @@ import com.vaadin.v7.data.validator.LegacyDateRangeValidator;
* @since 3.0
*/
@SuppressWarnings("serial")
-public class LegacyDateField extends LegacyAbstractField<Date> implements
+public class DateField extends AbstractField<Date> implements
FieldEvents.BlurNotifier, FieldEvents.FocusNotifier, LegacyComponent {
/**
@@ -157,7 +157,7 @@ public class LegacyDateField extends LegacyAbstractField<Date> implements
private String dateOutOfRangeMessage = "Date is out of allowed range";
- private LegacyDateRangeValidator currentRangeValidator;
+ private DateRangeValidator currentRangeValidator;
/**
* Determines whether the ValueChangeEvent should be fired. Used to prevent
@@ -180,7 +180,7 @@ public class LegacyDateField extends LegacyAbstractField<Date> implements
/**
* Constructs an empty <code>DateField</code> with no caption.
*/
- public LegacyDateField() {
+ public DateField() {
}
/**
@@ -189,7 +189,7 @@ public class LegacyDateField extends LegacyAbstractField<Date> implements
* @param caption
* the caption of the datefield.
*/
- public LegacyDateField(String caption) {
+ public DateField(String caption) {
setCaption(caption);
}
@@ -202,7 +202,7 @@ public class LegacyDateField extends LegacyAbstractField<Date> implements
* @param dataSource
* the Property to be edited with this editor.
*/
- public LegacyDateField(String caption, Property dataSource) {
+ public DateField(String caption, Property dataSource) {
this(dataSource);
setCaption(caption);
}
@@ -214,7 +214,7 @@ public class LegacyDateField extends LegacyAbstractField<Date> implements
* @param dataSource
* the Property to be edited with this editor.
*/
- public LegacyDateField(Property dataSource)
+ public DateField(Property dataSource)
throws IllegalArgumentException {
if (!Date.class.isAssignableFrom(dataSource.getType())) {
throw new IllegalArgumentException(
@@ -229,7 +229,7 @@ public class LegacyDateField extends LegacyAbstractField<Date> implements
* Constructs a new <code>DateField</code> with the given caption and
* initial text contents. The editor constructed this way will not be bound
* to a Property unless
- * {@link com.vaadin.data.Property.Viewer#setPropertyDataSource(Property)}
+ * {@link com.vaadin.v7.data.Property.Viewer#setPropertyDataSource(Property)}
* is called to bind it.
*
* @param caption
@@ -237,7 +237,7 @@ public class LegacyDateField extends LegacyAbstractField<Date> implements
* @param value
* the Date value.
*/
- public LegacyDateField(String caption, Date value) {
+ public DateField(String caption, Date value) {
setValue(value);
setCaption(caption);
}
@@ -421,7 +421,7 @@ public class LegacyDateField extends LegacyAbstractField<Date> implements
currentRangeValidator = null;
}
if (getRangeStart() != null || getRangeEnd() != null) {
- currentRangeValidator = new LegacyDateRangeValidator(
+ currentRangeValidator = new DateRangeValidator(
dateOutOfRangeMessage, getRangeStart(resolution),
getRangeEnd(resolution), null);
addValidator(currentRangeValidator);
@@ -555,7 +555,7 @@ public class LegacyDateField extends LegacyAbstractField<Date> implements
* this case the invalid text remains in the DateField.
*/
markAsDirty();
- } catch (LegacyConverter.ConversionException e) {
+ } catch (Converter.ConversionException e) {
/*
* Datefield now contains some text that could't be parsed
@@ -650,9 +650,9 @@ public class LegacyDateField extends LegacyAbstractField<Date> implements
* to keep the old value and indicate an error
*/
protected Date handleUnparsableDateString(String dateString)
- throws LegacyConverter.ConversionException {
+ throws Converter.ConversionException {
currentParseErrorMessage = null;
- throw new LegacyConverter.ConversionException(getParseErrorMessage());
+ throw new Converter.ConversionException(getParseErrorMessage());
}
/* Property features */
@@ -886,7 +886,7 @@ public class LegacyDateField extends LegacyAbstractField<Date> implements
* invalid if it contains text typed in by the user that couldn't be parsed
* into a Date value.
*
- * @see com.vaadin.v7.ui.LegacyAbstractField#validate()
+ * @see com.vaadin.v7.ui.AbstractField#validate()
*/
@Override
public void validate() throws InvalidValueException {
@@ -980,7 +980,7 @@ public class LegacyDateField extends LegacyAbstractField<Date> implements
.parse(design.attr("value"), Date.class);
// formatting will return null if it cannot parse the string
if (date == null) {
- Logger.getLogger(LegacyDateField.class.getName()).info(
+ Logger.getLogger(DateField.class.getName()).info(
"cannot parse " + design.attr("value") + " as date");
}
this.setValue(date, false, true);
diff --git a/compatibility-server/src/main/java/com/vaadin/v7/ui/DefaultFieldFactory.java b/compatibility-server/src/main/java/com/vaadin/v7/ui/DefaultFieldFactory.java
new file mode 100644
index 0000000000..53035ba087
--- /dev/null
+++ b/compatibility-server/src/main/java/com/vaadin/v7/ui/DefaultFieldFactory.java
@@ -0,0 +1,111 @@
+/*
+ * 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.v7.ui;
+
+import java.text.Normalizer.Form;
+import java.util.Date;
+
+import com.vaadin.shared.util.SharedUtil;
+import com.vaadin.ui.Component;
+import com.vaadin.v7.data.Container;
+import com.vaadin.v7.data.Property;
+
+/**
+ * This class contains a basic implementation for {@link TableFieldFactory}. The
+ * class is singleton, use {@link #get()} method to get reference to the
+ * instance.
+ *
+ * <p>
+ * There are also some static helper methods available for custom built field
+ * factories.
+ *
+ */
+public class DefaultFieldFactory implements TableFieldFactory {
+
+ private static final DefaultFieldFactory instance = new DefaultFieldFactory();
+
+ /**
+ * Singleton method to get an instance of DefaultFieldFactory.
+ *
+ * @return an instance of DefaultFieldFactory
+ */
+ public static DefaultFieldFactory get() {
+ return instance;
+ }
+
+ protected DefaultFieldFactory() {
+ }
+
+ @Override
+ public Field createField(Container container, Object itemId,
+ Object propertyId, Component uiContext) {
+ Property containerProperty = container.getContainerProperty(itemId,
+ propertyId);
+ Class<?> type = containerProperty.getType();
+ Field<?> field = createFieldByPropertyType(type);
+ field.setCaption(createCaptionByPropertyId(propertyId));
+ return field;
+ }
+
+ /**
+ * If name follows method naming conventions, convert the name to spaced
+ * upper case text. For example, convert "firstName" to "First Name"
+ *
+ * @param propertyId
+ * @return the formatted caption string
+ */
+ public static String createCaptionByPropertyId(Object propertyId) {
+ return SharedUtil.propertyIdToHumanFriendly(propertyId);
+ }
+
+ /**
+ * Creates fields based on the property type.
+ * <p>
+ * The default field type is {@link TextField}. Other field types generated
+ * by this method:
+ * <p>
+ * <b>Boolean</b>: {@link CheckBox}.<br/>
+ * <b>Date</b>: {@link DateField}(resolution: day).<br/>
+ * <b>Item</b>: {@link Form}. <br/>
+ * <b>default field type</b>: {@link TextField}.
+ * <p>
+ *
+ * @param type
+ * the type of the property
+ * @return the most suitable generic {@link Field} for given type
+ */
+ public static Field<?> createFieldByPropertyType(Class<?> type) {
+ // Null typed properties can not be edited
+ if (type == null) {
+ return null;
+ }
+
+ // Date field
+ if (Date.class.isAssignableFrom(type)) {
+ final DateField df = new DateField();
+ df.setResolution(DateField.RESOLUTION_DAY);
+ return df;
+ }
+
+ // Boolean field
+ if (Boolean.class.isAssignableFrom(type)) {
+ return new CheckBox();
+ }
+
+ return new TextField();
+ }
+
+}
diff --git a/compatibility-server/src/main/java/com/vaadin/v7/ui/Grid.java b/compatibility-server/src/main/java/com/vaadin/v7/ui/Grid.java
new file mode 100644
index 0000000000..0a5e5b40a4
--- /dev/null
+++ b/compatibility-server/src/main/java/com/vaadin/v7/ui/Grid.java
@@ -0,0 +1,7355 @@
+/*
+ * 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.v7.ui;
+
+import java.io.Serializable;
+import java.lang.reflect.Method;
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import org.jsoup.nodes.Attributes;
+import org.jsoup.nodes.Element;
+import org.jsoup.select.Elements;
+
+import com.vaadin.data.sort.Sort;
+import com.vaadin.data.sort.SortOrder;
+import com.vaadin.event.ContextClickEvent;
+import com.vaadin.event.ItemClickEvent;
+import com.vaadin.event.ItemClickEvent.ItemClickListener;
+import com.vaadin.event.ItemClickEvent.ItemClickNotifier;
+import com.vaadin.event.SelectionEvent;
+import com.vaadin.event.SelectionEvent.SelectionListener;
+import com.vaadin.event.SelectionEvent.SelectionNotifier;
+import com.vaadin.event.SortEvent;
+import com.vaadin.event.SortEvent.SortListener;
+import com.vaadin.event.SortEvent.SortNotifier;
+import com.vaadin.server.AbstractClientConnector;
+import com.vaadin.server.AbstractExtension;
+import com.vaadin.server.EncodeResult;
+import com.vaadin.server.ErrorMessage;
+import com.vaadin.server.Extension;
+import com.vaadin.server.JsonCodec;
+import com.vaadin.server.KeyMapper;
+import com.vaadin.server.VaadinSession;
+import com.vaadin.shared.MouseEventDetails;
+import com.vaadin.shared.data.sort.SortDirection;
+import com.vaadin.shared.ui.grid.EditorClientRpc;
+import com.vaadin.shared.ui.grid.EditorServerRpc;
+import com.vaadin.shared.ui.grid.GridClientRpc;
+import com.vaadin.shared.ui.grid.GridColumnState;
+import com.vaadin.shared.ui.grid.GridConstants;
+import com.vaadin.shared.ui.grid.GridConstants.Section;
+import com.vaadin.shared.ui.grid.GridServerRpc;
+import com.vaadin.shared.ui.grid.GridState;
+import com.vaadin.shared.ui.grid.GridStaticCellType;
+import com.vaadin.shared.ui.grid.GridStaticSectionState;
+import com.vaadin.shared.ui.grid.GridStaticSectionState.CellState;
+import com.vaadin.shared.ui.grid.GridStaticSectionState.RowState;
+import com.vaadin.shared.ui.grid.HeightMode;
+import com.vaadin.shared.ui.grid.ScrollDestination;
+import com.vaadin.shared.ui.grid.selection.MultiSelectionModelServerRpc;
+import com.vaadin.shared.ui.grid.selection.MultiSelectionModelState;
+import com.vaadin.shared.ui.grid.selection.SingleSelectionModelServerRpc;
+import com.vaadin.shared.ui.grid.selection.SingleSelectionModelState;
+import com.vaadin.shared.util.SharedUtil;
+import com.vaadin.ui.AbstractFocusable;
+import com.vaadin.ui.Component;
+import com.vaadin.ui.ConnectorTracker;
+import com.vaadin.ui.SelectiveRenderer;
+import com.vaadin.ui.UI;
+import com.vaadin.ui.declarative.DesignAttributeHandler;
+import com.vaadin.ui.declarative.DesignContext;
+import com.vaadin.ui.declarative.DesignException;
+import com.vaadin.ui.declarative.DesignFormatter;
+import com.vaadin.util.ReflectTools;
+import com.vaadin.v7.data.Container;
+import com.vaadin.v7.data.Container.Indexed;
+import com.vaadin.v7.data.Container.ItemSetChangeEvent;
+import com.vaadin.v7.data.Container.ItemSetChangeListener;
+import com.vaadin.v7.data.Container.ItemSetChangeNotifier;
+import com.vaadin.v7.data.Container.PropertySetChangeEvent;
+import com.vaadin.v7.data.Container.PropertySetChangeListener;
+import com.vaadin.v7.data.Container.PropertySetChangeNotifier;
+import com.vaadin.v7.data.Container.Sortable;
+import com.vaadin.v7.data.Item;
+import com.vaadin.v7.data.Property;
+import com.vaadin.v7.data.Validator.InvalidValueException;
+import com.vaadin.v7.data.fieldgroup.DefaultFieldGroupFieldFactory;
+import com.vaadin.v7.data.fieldgroup.FieldGroup;
+import com.vaadin.v7.data.fieldgroup.FieldGroup.CommitException;
+import com.vaadin.v7.data.fieldgroup.FieldGroupFieldFactory;
+import com.vaadin.v7.data.util.IndexedContainer;
+import com.vaadin.v7.data.util.converter.Converter;
+import com.vaadin.v7.data.util.converter.ConverterUtil;
+import com.vaadin.v7.server.communication.data.DataGenerator;
+import com.vaadin.v7.server.communication.data.RpcDataProviderExtension;
+import com.vaadin.v7.ui.renderers.HtmlRenderer;
+import com.vaadin.v7.ui.renderers.Renderer;
+import com.vaadin.v7.ui.renderers.TextRenderer;
+
+import elemental.json.Json;
+import elemental.json.JsonObject;
+import elemental.json.JsonValue;
+
+/**
+ * A grid component for displaying tabular data.
+ * <p>
+ * Grid is always bound to a {@link Container.Indexed}, but is not a
+ * {@code Container} of any kind in of itself. The contents of the given
+ * Container is displayed with the help of {@link Renderer Renderers}.
+ *
+ * <h3 id="grid-headers-and-footers">Headers and Footers</h3>
+ * <p>
+ *
+ *
+ * <h3 id="grid-converters-and-renderers">Converters and Renderers</h3>
+ * <p>
+ * Each column has its own {@link Renderer} that displays data into something
+ * that can be displayed in the browser. That data is first converted with a
+ * {@link com.vaadin.v7.data.util.converter.Converter Converter} into
+ * something that the Renderer can process. This can also be an implicit step -
+ * if a column has a simple data type, like a String, no explicit assignment is
+ * needed.
+ * <p>
+ * Usually a renderer takes some kind of object, and converts it into a
+ * HTML-formatted string.
+ * <p>
+ * <code><pre>
+ * Grid grid = new Grid(myContainer);
+ * Column column = grid.getColumn(STRING_DATE_PROPERTY);
+ * column.setConverter(new StringToDateConverter());
+ * column.setRenderer(new MyColorfulDateRenderer());
+ * </pre></code>
+ *
+ * <h3 id="grid-lazyloading">Lazy Loading</h3>
+ * <p>
+ * The data is accessed as it is needed by Grid and not any sooner. In other
+ * words, if the given Container is huge, but only the first few rows are
+ * displayed to the user, only those (and a few more, for caching purposes) are
+ * accessed.
+ *
+ * <h3 id="grid-selection-modes-and-models">Selection Modes and Models</h3>
+ * <p>
+ * Grid supports three selection <em>{@link SelectionMode modes}</em> (single,
+ * multi, none), and comes bundled with one <em>{@link SelectionModel
+ * model}</em> for each of the modes. The distinction between a selection mode
+ * and selection model is as follows: a <em>mode</em> essentially says whether
+ * you can have one, many or no rows selected. The model, however, has the
+ * behavioral details of each. A single selection model may require that the
+ * user deselects one row before selecting another one. A variant of a
+ * multiselect might have a configurable maximum of rows that may be selected.
+ * And so on.
+ * <p>
+ * <code><pre>
+ * Grid grid = new Grid(myContainer);
+ *
+ * // uses the bundled SingleSelectionModel class
+ * grid.setSelectionMode(SelectionMode.SINGLE);
+ *
+ * // changes the behavior to a custom selection model
+ * grid.setSelectionModel(new MyTwoSelectionModel());
+ * </pre></code>
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public class Grid extends AbstractFocusable implements SelectionNotifier,
+ SortNotifier, SelectiveRenderer, ItemClickNotifier {
+
+ /**
+ * An event listener for column visibility change events in the Grid.
+ *
+ * @since 7.5.0
+ */
+ public interface ColumnVisibilityChangeListener extends Serializable {
+ /**
+ * Called when a column has become hidden or unhidden.
+ *
+ * @param event
+ */
+ void columnVisibilityChanged(ColumnVisibilityChangeEvent event);
+ }
+
+ /**
+ * An event that is fired when a column's visibility changes.
+ *
+ * @since 7.5.0
+ */
+ public static class ColumnVisibilityChangeEvent extends Component.Event {
+
+ private final Column column;
+ private final boolean userOriginated;
+ private final boolean hidden;
+
+ /**
+ * Constructor for a column visibility change event.
+ *
+ * @param source
+ * the grid from which this event originates
+ * @param column
+ * the column that changed its visibility
+ * @param hidden
+ * <code>true</code> if the column was hidden,
+ * <code>false</code> if it became visible
+ * @param isUserOriginated
+ * <code>true</code> iff the event was triggered by an UI
+ * interaction
+ */
+ public ColumnVisibilityChangeEvent(Grid source, Column column,
+ boolean hidden, boolean isUserOriginated) {
+ super(source);
+ this.column = column;
+ this.hidden = hidden;
+ userOriginated = isUserOriginated;
+ }
+
+ /**
+ * Gets the column that became hidden or visible.
+ *
+ * @return the column that became hidden or visible.
+ * @see Column#isHidden()
+ */
+ public Column getColumn() {
+ return column;
+ }
+
+ /**
+ * Was the column set hidden or visible.
+ *
+ * @return <code>true</code> if the column was hidden <code>false</code>
+ * if it was set visible
+ */
+ public boolean isHidden() {
+ return hidden;
+ }
+
+ /**
+ * Returns <code>true</code> if the column reorder was done by the user,
+ * <code>false</code> if not and it was triggered by server side code.
+ *
+ * @return <code>true</code> if event is a result of user interaction
+ */
+ public boolean isUserOriginated() {
+ return userOriginated;
+ }
+ }
+
+ /**
+ * A callback interface for generating details for a particular row in Grid.
+ *
+ * @since 7.5.0
+ * @author Vaadin Ltd
+ * @see DetailsGenerator#NULL
+ */
+ public interface DetailsGenerator extends Serializable {
+
+ /** A details generator that provides no details */
+ public DetailsGenerator NULL = new DetailsGenerator() {
+ @Override
+ public Component getDetails(RowReference rowReference) {
+ return null;
+ }
+ };
+
+ /**
+ * This method is called for whenever a details row needs to be shown on
+ * the client. Grid removes all of its references to details components
+ * when they are no longer displayed on the client-side and will
+ * re-request once needed again.
+ * <p>
+ * <em>Note:</em> If a component gets generated, it may not be manually
+ * attached anywhere. The same details component can not be displayed
+ * for multiple different rows.
+ *
+ * @param rowReference
+ * the reference for the row for which to generate details
+ * @return the details for the given row, or <code>null</code> to leave
+ * the details empty.
+ */
+ Component getDetails(RowReference rowReference);
+ }
+
+ /**
+ * A class that manages details components by calling
+ * {@link DetailsGenerator} as needed. Details components are attached by
+ * this class when the {@link RpcDataProviderExtension} is sending data to
+ * the client. Details components are detached and forgotten when client
+ * informs that it has dropped the corresponding item.
+ *
+ * @since 7.6.1
+ */
+ public final static class DetailComponentManager
+ extends AbstractGridExtension implements DataGenerator {
+
+ /**
+ * The user-defined details generator.
+ *
+ * @see #setDetailsGenerator(DetailsGenerator)
+ */
+ private DetailsGenerator detailsGenerator;
+
+ /**
+ * This map represents all details that are currently visible on the
+ * client. Details components get destroyed once they scroll out of
+ * view.
+ */
+ private final Map<Object, Component> itemIdToDetailsComponent = new HashMap<>();
+
+ /**
+ * Set of item ids that got <code>null</code> from DetailsGenerator when
+ * {@link DetailsGenerator#getDetails(RowReference)} was called.
+ */
+ private final Set<Object> emptyDetails = new HashSet<>();
+
+ /**
+ * Set of item IDs for all open details rows. Contains even the ones
+ * that are not currently visible on the client.
+ */
+ private final Set<Object> openDetails = new HashSet<>();
+
+ public DetailComponentManager(Grid grid) {
+ this(grid, DetailsGenerator.NULL);
+ }
+
+ public DetailComponentManager(Grid grid,
+ DetailsGenerator detailsGenerator) {
+ super(grid);
+ setDetailsGenerator(detailsGenerator);
+ }
+
+ /**
+ * Creates a details component with the help of the user-defined
+ * {@link DetailsGenerator}.
+ * <p>
+ * This method attaches created components to the parent {@link Grid}.
+ *
+ * @param itemId
+ * the item id for which to create the details component.
+ * @throws IllegalStateException
+ * if the current details generator provides a component
+ * that was manually attached.
+ */
+ private void createDetails(Object itemId) throws IllegalStateException {
+ assert itemId != null : "itemId was null";
+
+ if (itemIdToDetailsComponent.containsKey(itemId)
+ || emptyDetails.contains(itemId)) {
+ // Don't overwrite existing components
+ return;
+ }
+
+ RowReference rowReference = new RowReference(getParentGrid());
+ rowReference.set(itemId);
+
+ DetailsGenerator detailsGenerator = getParentGrid()
+ .getDetailsGenerator();
+ Component details = detailsGenerator.getDetails(rowReference);
+ if (details != null) {
+ if (details.getParent() != null) {
+ String name = detailsGenerator.getClass().getName();
+ throw new IllegalStateException(
+ name + " generated a details component that already "
+ + "was attached. (itemId: " + itemId
+ + ", component: " + details + ")");
+ }
+
+ itemIdToDetailsComponent.put(itemId, details);
+
+ addComponentToGrid(details);
+
+ assert !emptyDetails.contains(itemId) : "Bookeeping thinks "
+ + "itemId is empty even though we just created a "
+ + "component for it (" + itemId + ")";
+ } else {
+ emptyDetails.add(itemId);
+ }
+
+ }
+
+ /**
+ * Destroys a details component correctly.
+ * <p>
+ * This method will detach the component from parent {@link Grid}.
+ *
+ * @param itemId
+ * the item id for which to destroy the details component
+ */
+ private void destroyDetails(Object itemId) {
+ emptyDetails.remove(itemId);
+
+ Component removedComponent = itemIdToDetailsComponent
+ .remove(itemId);
+ if (removedComponent == null) {
+ return;
+ }
+
+ removeComponentFromGrid(removedComponent);
+ }
+
+ /**
+ * Recreates all visible details components.
+ */
+ public void refreshDetails() {
+ Set<Object> visibleItemIds = new HashSet<>(
+ itemIdToDetailsComponent.keySet());
+ for (Object itemId : visibleItemIds) {
+ destroyDetails(itemId);
+ createDetails(itemId);
+ refreshRow(itemId);
+ }
+ }
+
+ /**
+ * Sets details visiblity status of given item id.
+ *
+ * @param itemId
+ * item id to set
+ * @param visible
+ * <code>true</code> if visible; <code>false</code> if not
+ */
+ public void setDetailsVisible(Object itemId, boolean visible) {
+ if ((visible && openDetails.contains(itemId))
+ || (!visible && !openDetails.contains(itemId))) {
+ return;
+ }
+
+ if (visible) {
+ openDetails.add(itemId);
+ refreshRow(itemId);
+ } else {
+ openDetails.remove(itemId);
+ destroyDetails(itemId);
+ refreshRow(itemId);
+ }
+ }
+
+ @Override
+ public void generateData(Object itemId, Item item, JsonObject rowData) {
+ // DetailComponentManager should not send anything if details
+ // generator is the default null version.
+ if (openDetails.contains(itemId)
+ && !detailsGenerator.equals(DetailsGenerator.NULL)) {
+ // Double check to be sure details component exists.
+ createDetails(itemId);
+
+ Component detailsComponent = itemIdToDetailsComponent
+ .get(itemId);
+ rowData.put(GridState.JSONKEY_DETAILS_VISIBLE,
+ (detailsComponent != null
+ ? detailsComponent.getConnectorId() : ""));
+ }
+ }
+
+ @Override
+ public void destroyData(Object itemId) {
+ if (openDetails.contains(itemId)) {
+ destroyDetails(itemId);
+ }
+ }
+
+ /**
+ * Sets a new details generator for row details.
+ * <p>
+ * The currently opened row details will be re-rendered.
+ *
+ * @param detailsGenerator
+ * the details generator to set
+ * @throws IllegalArgumentException
+ * if detailsGenerator is <code>null</code>;
+ */
+ public void setDetailsGenerator(DetailsGenerator detailsGenerator)
+ throws IllegalArgumentException {
+ if (detailsGenerator == null) {
+ throw new IllegalArgumentException(
+ "Details generator may not be null");
+ } else if (detailsGenerator == this.detailsGenerator) {
+ return;
+ }
+
+ this.detailsGenerator = detailsGenerator;
+
+ refreshDetails();
+ }
+
+ /**
+ * Gets the current details generator for row details.
+ *
+ * @return the detailsGenerator the current details generator
+ */
+ public DetailsGenerator getDetailsGenerator() {
+ return detailsGenerator;
+ }
+
+ /**
+ * Checks whether details are visible for the given item.
+ *
+ * @param itemId
+ * the id of the item for which to check details visibility
+ * @return <code>true</code> iff the details are visible
+ */
+ public boolean isDetailsVisible(Object itemId) {
+ return openDetails.contains(itemId);
+ }
+ }
+
+ /**
+ * Custom field group that allows finding property types before an item has
+ * been bound.
+ */
+ private final class CustomFieldGroup extends FieldGroup {
+
+ public CustomFieldGroup() {
+ setFieldFactory(EditorFieldFactory.get());
+ }
+
+ @Override
+ protected Class<?> getPropertyType(Object propertyId)
+ throws BindException {
+ if (getItemDataSource() == null) {
+ return datasource.getType(propertyId);
+ } else {
+ return super.getPropertyType(propertyId);
+ }
+ }
+
+ @Override
+ protected <T extends Field> T build(String caption,
+ Class<?> dataType, Class<T> fieldType) throws BindException {
+ T field = super.build(caption, dataType, fieldType);
+ if (field instanceof CheckBox) {
+ field.setCaption(null);
+ }
+ return field;
+ }
+ }
+
+ /**
+ * Field factory used by default in the editor.
+ *
+ * Aims to fields of suitable type and with suitable size for use in the
+ * editor row.
+ */
+ public static class EditorFieldFactory
+ extends DefaultFieldGroupFieldFactory {
+ private static final EditorFieldFactory INSTANCE = new EditorFieldFactory();
+
+ protected EditorFieldFactory() {
+ }
+
+ /**
+ * Returns the singleton instance
+ *
+ * @return the singleton instance
+ */
+ public static EditorFieldFactory get() {
+ return INSTANCE;
+ }
+
+ @Override
+ public <T extends Field> T createField(Class<?> type,
+ Class<T> fieldType) {
+ T f = super.createField(type, fieldType);
+ if (f != null) {
+ f.setWidth("100%");
+ }
+ return f;
+ }
+
+ @Override
+ protected AbstractSelect createCompatibleSelect(
+ Class<? extends AbstractSelect> fieldType) {
+ if (anySelect(fieldType)) {
+ return super.createCompatibleSelect(ComboBox.class);
+ }
+ return super.createCompatibleSelect(fieldType);
+ }
+
+ @Override
+ protected void populateWithEnumData(AbstractSelect select,
+ Class<? extends Enum> enumClass) {
+ // Use enums directly and the EnumToStringConverter to be consistent
+ // with what is shown in the Grid
+ @SuppressWarnings("unchecked")
+ EnumSet<?> enumSet = EnumSet.allOf(enumClass);
+ for (Object r : enumSet) {
+ select.addItem(r);
+ }
+ }
+ }
+
+ /**
+ * Error handler for the editor
+ */
+ public interface EditorErrorHandler extends Serializable {
+
+ /**
+ * Called when an exception occurs while the editor row is being saved
+ *
+ * @param event
+ * An event providing more information about the error
+ */
+ void commitError(CommitErrorEvent event);
+ }
+
+ /**
+ * ContextClickEvent for the Grid Component.
+ *
+ * @since 7.6
+ */
+ public static class GridContextClickEvent extends ContextClickEvent {
+
+ private final Object itemId;
+ private final int rowIndex;
+ private final Object propertyId;
+ private final Section section;
+
+ public GridContextClickEvent(Grid source,
+ MouseEventDetails mouseEventDetails, Section section,
+ int rowIndex, Object itemId, Object propertyId) {
+ super(source, mouseEventDetails);
+ this.itemId = itemId;
+ this.propertyId = propertyId;
+ this.section = section;
+ this.rowIndex = rowIndex;
+ }
+
+ /**
+ * Returns the item id of context clicked row.
+ *
+ * @return item id of clicked row; <code>null</code> if header or footer
+ */
+ public Object getItemId() {
+ return itemId;
+ }
+
+ /**
+ * Returns property id of clicked column.
+ *
+ * @return property id
+ */
+ public Object getPropertyId() {
+ return propertyId;
+ }
+
+ /**
+ * Return the clicked section of Grid.
+ *
+ * @return section of grid
+ */
+ public Section getSection() {
+ return section;
+ }
+
+ /**
+ * Returns the clicked row index relative to Grid section. In the body
+ * of the Grid the index is the item index in the Container. Header and
+ * Footer rows for index can be fetched with
+ * {@link Grid#getHeaderRow(int)} and {@link Grid#getFooterRow(int)}.
+ *
+ * @return row index in section
+ */
+ public int getRowIndex() {
+ return rowIndex;
+ }
+
+ @Override
+ public Grid getComponent() {
+ return (Grid) super.getComponent();
+ }
+ }
+
+ /**
+ * An event which is fired when saving the editor fails
+ */
+ public static class CommitErrorEvent extends Component.Event {
+
+ private CommitException cause;
+
+ private Set<Column> errorColumns = new HashSet<>();
+
+ private String userErrorMessage;
+
+ public CommitErrorEvent(Grid grid, CommitException cause) {
+ super(grid);
+ this.cause = cause;
+ userErrorMessage = cause.getLocalizedMessage();
+ }
+
+ /**
+ * Retrieves the cause of the failure
+ *
+ * @return the cause of the failure
+ */
+ public CommitException getCause() {
+ return cause;
+ }
+
+ @Override
+ public Grid getComponent() {
+ return (Grid) super.getComponent();
+ }
+
+ /**
+ * Checks if validation exceptions caused this error
+ *
+ * @return true if the problem was caused by a validation error
+ */
+ public boolean isValidationFailure() {
+ return cause.getCause() instanceof InvalidValueException;
+ }
+
+ /**
+ * Marks that an error indicator should be shown for the editor of a
+ * column.
+ *
+ * @param column
+ * the column to show an error for
+ */
+ public void addErrorColumn(Column column) {
+ errorColumns.add(column);
+ }
+
+ /**
+ * Gets all the columns that have been marked as erroneous.
+ *
+ * @return an umodifiable collection of erroneous columns
+ */
+ public Collection<Column> getErrorColumns() {
+ return Collections.unmodifiableCollection(errorColumns);
+ }
+
+ /**
+ * Gets the error message to show to the user.
+ *
+ * @return error message to show
+ */
+ public String getUserErrorMessage() {
+ return userErrorMessage;
+ }
+
+ /**
+ * Sets the error message to show to the user.
+ *
+ * @param userErrorMessage
+ * the user error message to set
+ */
+ public void setUserErrorMessage(String userErrorMessage) {
+ this.userErrorMessage = userErrorMessage;
+ }
+
+ }
+
+ /**
+ * An event listener for column reorder events in the Grid.
+ *
+ * @since 7.5.0
+ */
+ public interface ColumnReorderListener extends Serializable {
+
+ /**
+ * Called when the columns of the grid have been reordered.
+ *
+ * @param event
+ * An event providing more information
+ */
+ void columnReorder(ColumnReorderEvent event);
+ }
+
+ /**
+ * An event that is fired when the columns are reordered.
+ *
+ * @since 7.5.0
+ */
+ public static class ColumnReorderEvent extends Component.Event {
+
+ private final boolean userOriginated;
+
+ /**
+ *
+ * @param source
+ * the grid where the event originated from
+ * @param userOriginated
+ * <code>true</code> if event is a result of user
+ * interaction, <code>false</code> if from API call
+ */
+ public ColumnReorderEvent(Grid source, boolean userOriginated) {
+ super(source);
+ this.userOriginated = userOriginated;
+ }
+
+ /**
+ * Returns <code>true</code> if the column reorder was done by the user,
+ * <code>false</code> if not and it was triggered by server side code.
+ *
+ * @return <code>true</code> if event is a result of user interaction
+ */
+ public boolean isUserOriginated() {
+ return userOriginated;
+ }
+
+ }
+
+ /**
+ * An event listener for column resize events in the Grid.
+ *
+ * @since 7.6
+ */
+ public interface ColumnResizeListener extends Serializable {
+
+ /**
+ * Called when the columns of the grid have been resized.
+ *
+ * @param event
+ * An event providing more information
+ */
+ void columnResize(ColumnResizeEvent event);
+ }
+
+ /**
+ * An event that is fired when a column is resized, either programmatically
+ * or by the user.
+ *
+ * @since 7.6
+ */
+ public static class ColumnResizeEvent extends Component.Event {
+
+ private final Column column;
+ private final boolean userOriginated;
+
+ /**
+ *
+ * @param source
+ * the grid where the event originated from
+ * @param userOriginated
+ * <code>true</code> if event is a result of user
+ * interaction, <code>false</code> if from API call
+ */
+ public ColumnResizeEvent(Grid source, Column column,
+ boolean userOriginated) {
+ super(source);
+ this.column = column;
+ this.userOriginated = userOriginated;
+ }
+
+ /**
+ * Returns the column that was resized.
+ *
+ * @return the resized column.
+ */
+ public Column getColumn() {
+ return column;
+ }
+
+ /**
+ * Returns <code>true</code> if the column resize was done by the user,
+ * <code>false</code> if not and it was triggered by server side code.
+ *
+ * @return <code>true</code> if event is a result of user interaction
+ */
+ public boolean isUserOriginated() {
+ return userOriginated;
+ }
+
+ }
+
+ /**
+ * Interface for an editor event listener
+ */
+ public interface EditorListener extends Serializable {
+
+ public static final Method EDITOR_OPEN_METHOD = ReflectTools.findMethod(
+ EditorListener.class, "editorOpened", EditorOpenEvent.class);
+ public static final Method EDITOR_MOVE_METHOD = ReflectTools.findMethod(
+ EditorListener.class, "editorMoved", EditorMoveEvent.class);
+ public static final Method EDITOR_CLOSE_METHOD = ReflectTools
+ .findMethod(EditorListener.class, "editorClosed",
+ EditorCloseEvent.class);
+
+ /**
+ * Called when an editor is opened
+ *
+ * @param e
+ * an editor open event object
+ */
+ public void editorOpened(EditorOpenEvent e);
+
+ /**
+ * Called when an editor is reopened without closing it first
+ *
+ * @param e
+ * an editor move event object
+ */
+ public void editorMoved(EditorMoveEvent e);
+
+ /**
+ * Called when an editor is closed
+ *
+ * @param e
+ * an editor close event object
+ */
+ public void editorClosed(EditorCloseEvent e);
+
+ }
+
+ /**
+ * Base class for editor related events
+ */
+ public static abstract class EditorEvent extends Component.Event {
+
+ private Object itemID;
+
+ protected EditorEvent(Grid source, Object itemID) {
+ super(source);
+ this.itemID = itemID;
+ }
+
+ /**
+ * Get the item (row) for which this editor was opened
+ */
+ public Object getItem() {
+ return itemID;
+ }
+
+ }
+
+ /**
+ * This event gets fired when an editor is opened
+ */
+ public static class EditorOpenEvent extends EditorEvent {
+
+ public EditorOpenEvent(Grid source, Object itemID) {
+ super(source, itemID);
+ }
+ }
+
+ /**
+ * This event gets fired when an editor is opened while another row is being
+ * edited (i.e. editor focus moves elsewhere)
+ */
+ public static class EditorMoveEvent extends EditorEvent {
+
+ public EditorMoveEvent(Grid source, Object itemID) {
+ super(source, itemID);
+ }
+ }
+
+ /**
+ * This event gets fired when an editor is dismissed or closed by other
+ * means.
+ */
+ public static class EditorCloseEvent extends EditorEvent {
+
+ public EditorCloseEvent(Grid source, Object itemID) {
+ super(source, itemID);
+ }
+ }
+
+ /**
+ * Default error handler for the editor
+ *
+ */
+ public class DefaultEditorErrorHandler implements EditorErrorHandler {
+
+ @Override
+ public void commitError(CommitErrorEvent event) {
+ Map<Field<?>, InvalidValueException> invalidFields = event
+ .getCause().getInvalidFields();
+
+ if (!invalidFields.isEmpty()) {
+ Object firstErrorPropertyId = null;
+ Field<?> firstErrorField = null;
+
+ FieldGroup fieldGroup = event.getCause().getFieldGroup();
+ for (Column column : getColumns()) {
+ Object propertyId = column.getPropertyId();
+ Field<?> field = fieldGroup.getField(propertyId);
+ if (invalidFields.keySet().contains(field)) {
+ event.addErrorColumn(column);
+
+ if (firstErrorPropertyId == null) {
+ firstErrorPropertyId = propertyId;
+ firstErrorField = field;
+ }
+ }
+ }
+
+ /*
+ * Validation error, show first failure as
+ * "<Column header>: <message>"
+ */
+ String caption = getColumn(firstErrorPropertyId)
+ .getHeaderCaption();
+ String message = invalidFields.get(firstErrorField)
+ .getLocalizedMessage();
+
+ event.setUserErrorMessage(caption + ": " + message);
+ } else {
+ com.vaadin.server.ErrorEvent.findErrorHandler(Grid.this).error(
+ new ConnectorErrorEvent(Grid.this, event.getCause()));
+ }
+ }
+
+ private Object getFirstPropertyId(FieldGroup fieldGroup,
+ Set<Field<?>> keySet) {
+ for (Column c : getColumns()) {
+ Object propertyId = c.getPropertyId();
+ Field<?> f = fieldGroup.getField(propertyId);
+ if (keySet.contains(f)) {
+ return propertyId;
+ }
+ }
+ return null;
+ }
+ }
+
+ /**
+ * Selection modes representing built-in {@link SelectionModel
+ * SelectionModels} that come bundled with {@link Grid}.
+ * <p>
+ * Passing one of these enums into
+ * {@link Grid#setSelectionMode(SelectionMode)} is equivalent to calling
+ * {@link Grid#setSelectionModel(SelectionModel)} with one of the built-in
+ * implementations of {@link SelectionModel}.
+ *
+ * @see Grid#setSelectionMode(SelectionMode)
+ * @see Grid#setSelectionModel(SelectionModel)
+ */
+ public enum SelectionMode {
+ /** A SelectionMode that maps to {@link SingleSelectionModel} */
+ SINGLE {
+ @Override
+ protected SelectionModel createModel() {
+ return new SingleSelectionModel();
+ }
+
+ },
+
+ /** A SelectionMode that maps to {@link MultiSelectionModel} */
+ MULTI {
+ @Override
+ protected SelectionModel createModel() {
+ return new MultiSelectionModel();
+ }
+ },
+
+ /** A SelectionMode that maps to {@link NoSelectionModel} */
+ NONE {
+ @Override
+ protected SelectionModel createModel() {
+ return new NoSelectionModel();
+ }
+ };
+
+ protected abstract SelectionModel createModel();
+ }
+
+ /**
+ * The server-side interface that controls Grid's selection state.
+ * SelectionModel should extend {@link AbstractGridExtension}.
+ */
+ public interface SelectionModel extends Serializable, Extension {
+ /**
+ * Checks whether an item is selected or not.
+ *
+ * @param itemId
+ * the item id to check for
+ * @return <code>true</code> iff the item is selected
+ */
+ boolean isSelected(Object itemId);
+
+ /**
+ * Returns a collection of all the currently selected itemIds.
+ *
+ * @return a collection of all the currently selected itemIds
+ */
+ Collection<Object> getSelectedRows();
+
+ /**
+ * Injects the current {@link Grid} instance into the SelectionModel.
+ * This method should usually call the extend method of
+ * {@link AbstractExtension}.
+ * <p>
+ * <em>Note:</em> This method should not be called manually.
+ *
+ * @param grid
+ * the Grid in which the SelectionModel currently is, or
+ * <code>null</code> when a selection model is being detached
+ * from a Grid.
+ */
+ void setGrid(Grid grid);
+
+ /**
+ * Resets the SelectiomModel to an initial state.
+ * <p>
+ * Most often this means that the selection state is cleared, but
+ * implementations are free to interpret the "initial state" as they
+ * wish. Some, for example, may want to keep the first selected item as
+ * selected.
+ */
+ void reset();
+
+ /**
+ * A SelectionModel that supports multiple selections to be made.
+ * <p>
+ * This interface has a contract of having the same behavior, no matter
+ * how the selection model is interacted with. In other words, if
+ * something is forbidden to do in e.g. the user interface, it must also
+ * be forbidden to do in the server-side and client-side APIs.
+ */
+ public interface Multi extends SelectionModel {
+
+ /**
+ * Marks items as selected.
+ * <p>
+ * This method does not clear any previous selection state, only
+ * adds to it.
+ *
+ * @param itemIds
+ * the itemId(s) to mark as selected
+ * @return <code>true</code> if the selection state changed.
+ * <code>false</code> if all the given itemIds already were
+ * selected
+ * @throws IllegalArgumentException
+ * if the <code>itemIds</code> varargs array is
+ * <code>null</code> or given itemIds don't exist in the
+ * container of Grid
+ * @see #deselect(Object...)
+ */
+ boolean select(Object... itemIds) throws IllegalArgumentException;
+
+ /**
+ * Marks items as selected.
+ * <p>
+ * This method does not clear any previous selection state, only
+ * adds to it.
+ *
+ * @param itemIds
+ * the itemIds to mark as selected
+ * @return <code>true</code> if the selection state changed.
+ * <code>false</code> if all the given itemIds already were
+ * selected
+ * @throws IllegalArgumentException
+ * if <code>itemIds</code> is <code>null</code> or given
+ * itemIds don't exist in the container of Grid
+ * @see #deselect(Collection)
+ */
+ boolean select(Collection<?> itemIds)
+ throws IllegalArgumentException;
+
+ /**
+ * Marks items as deselected.
+ *
+ * @param itemIds
+ * the itemId(s) to remove from being selected
+ * @return <code>true</code> if the selection state changed.
+ * <code>false</code> if none the given itemIds were
+ * selected previously
+ * @throws IllegalArgumentException
+ * if the <code>itemIds</code> varargs array is
+ * <code>null</code>
+ * @see #select(Object...)
+ */
+ boolean deselect(Object... itemIds) throws IllegalArgumentException;
+
+ /**
+ * Marks items as deselected.
+ *
+ * @param itemIds
+ * the itemId(s) to remove from being selected
+ * @return <code>true</code> if the selection state changed.
+ * <code>false</code> if none the given itemIds were
+ * selected previously
+ * @throws IllegalArgumentException
+ * if <code>itemIds</code> is <code>null</code>
+ * @see #select(Collection)
+ */
+ boolean deselect(Collection<?> itemIds)
+ throws IllegalArgumentException;
+
+ /**
+ * Marks all the items in the current Container as selected
+ *
+ * @return <code>true</code> iff some items were previously not
+ * selected
+ * @see #deselectAll()
+ */
+ boolean selectAll();
+
+ /**
+ * Marks all the items in the current Container as deselected
+ *
+ * @return <code>true</code> iff some items were previously selected
+ * @see #selectAll()
+ */
+ boolean deselectAll();
+
+ /**
+ * Marks items as selected while deselecting all items not in the
+ * given Collection.
+ *
+ * @param itemIds
+ * the itemIds to mark as selected
+ * @return <code>true</code> if the selection state changed.
+ * <code>false</code> if all the given itemIds already were
+ * selected
+ * @throws IllegalArgumentException
+ * if <code>itemIds</code> is <code>null</code> or given
+ * itemIds don't exist in the container of Grid
+ */
+ boolean setSelected(Collection<?> itemIds)
+ throws IllegalArgumentException;
+
+ /**
+ * Marks items as selected while deselecting all items not in the
+ * varargs array.
+ *
+ * @param itemIds
+ * the itemIds to mark as selected
+ * @return <code>true</code> if the selection state changed.
+ * <code>false</code> if all the given itemIds already were
+ * selected
+ * @throws IllegalArgumentException
+ * if the <code>itemIds</code> varargs array is
+ * <code>null</code> or given itemIds don't exist in the
+ * container of Grid
+ */
+ boolean setSelected(Object... itemIds)
+ throws IllegalArgumentException;
+ }
+
+ /**
+ * A SelectionModel that supports for only single rows to be selected at
+ * a time.
+ * <p>
+ * This interface has a contract of having the same behavior, no matter
+ * how the selection model is interacted with. In other words, if
+ * something is forbidden to do in e.g. the user interface, it must also
+ * be forbidden to do in the server-side and client-side APIs.
+ */
+ public interface Single extends SelectionModel {
+
+ /**
+ * Marks an item as selected.
+ *
+ * @param itemId
+ * the itemId to mark as selected; <code>null</code> for
+ * deselect
+ * @return <code>true</code> if the selection state changed.
+ * <code>false</code> if the itemId already was selected
+ * @throws IllegalStateException
+ * if the selection was illegal. One such reason might
+ * be that the given id was null, indicating a deselect,
+ * but implementation doesn't allow deselecting.
+ * re-selecting something
+ * @throws IllegalArgumentException
+ * if given itemId does not exist in the container of
+ * Grid
+ */
+ boolean select(Object itemId)
+ throws IllegalStateException, IllegalArgumentException;
+
+ /**
+ * Gets the item id of the currently selected item.
+ *
+ * @return the item id of the currently selected item, or
+ * <code>null</code> if nothing is selected
+ */
+ Object getSelectedRow();
+
+ /**
+ * Sets whether it's allowed to deselect the selected row through
+ * the UI. Deselection is allowed by default.
+ *
+ * @param deselectAllowed
+ * <code>true</code> if the selected row can be
+ * deselected without selecting another row instead;
+ * otherwise <code>false</code>.
+ */
+ public void setDeselectAllowed(boolean deselectAllowed);
+
+ /**
+ * Sets whether it's allowed to deselect the selected row through
+ * the UI.
+ *
+ * @return <code>true</code> if deselection is allowed; otherwise
+ * <code>false</code>
+ */
+ public boolean isDeselectAllowed();
+ }
+
+ /**
+ * A SelectionModel that does not allow for rows to be selected.
+ * <p>
+ * This interface has a contract of having the same behavior, no matter
+ * how the selection model is interacted with. In other words, if the
+ * developer is unable to select something programmatically, it is not
+ * allowed for the end-user to select anything, either.
+ */
+ public interface None extends SelectionModel {
+
+ /**
+ * {@inheritDoc}
+ *
+ * @return always <code>false</code>.
+ */
+ @Override
+ public boolean isSelected(Object itemId);
+
+ /**
+ * {@inheritDoc}
+ *
+ * @return always an empty collection.
+ */
+ @Override
+ public Collection<Object> getSelectedRows();
+ }
+ }
+
+ /**
+ * A base class for SelectionModels that contains some of the logic that is
+ * reusable.
+ */
+ public static abstract class AbstractSelectionModel extends
+ AbstractGridExtension implements SelectionModel, DataGenerator {
+ protected final LinkedHashSet<Object> selection = new LinkedHashSet<>();
+
+ @Override
+ public boolean isSelected(final Object itemId) {
+ return selection.contains(itemId);
+ }
+
+ @Override
+ public Collection<Object> getSelectedRows() {
+ return new ArrayList<>(selection);
+ }
+
+ @Override
+ public void setGrid(final Grid grid) {
+ if (grid != null) {
+ extend(grid);
+ }
+ }
+
+ /**
+ * Sanity check for existence of item id.
+ *
+ * @param itemId
+ * item id to be selected / deselected
+ *
+ * @throws IllegalArgumentException
+ * if item Id doesn't exist in the container of Grid
+ */
+ protected void checkItemIdExists(Object itemId)
+ throws IllegalArgumentException {
+ if (!getParentGrid().getContainerDataSource().containsId(itemId)) {
+ throw new IllegalArgumentException("Given item id (" + itemId
+ + ") does not exist in the container");
+ }
+ }
+
+ /**
+ * Sanity check for existence of item ids in given collection.
+ *
+ * @param itemIds
+ * item id collection to be selected / deselected
+ *
+ * @throws IllegalArgumentException
+ * if at least one item id doesn't exist in the container of
+ * Grid
+ */
+ protected void checkItemIdsExist(Collection<?> itemIds)
+ throws IllegalArgumentException {
+ for (Object itemId : itemIds) {
+ checkItemIdExists(itemId);
+ }
+ }
+
+ /**
+ * Fires a {@link SelectionEvent} to all the {@link SelectionListener
+ * SelectionListeners} currently added to the Grid in which this
+ * SelectionModel is.
+ * <p>
+ * Note that this is only a helper method, and routes the call all the
+ * way to Grid. A {@link SelectionModel} is not a
+ * {@link SelectionNotifier}
+ *
+ * @param oldSelection
+ * the complete {@link Collection} of the itemIds that were
+ * selected <em>before</em> this event happened
+ * @param newSelection
+ * the complete {@link Collection} of the itemIds that are
+ * selected <em>after</em> this event happened
+ */
+ protected void fireSelectionEvent(final Collection<Object> oldSelection,
+ final Collection<Object> newSelection) {
+ getParentGrid().fireSelectionEvent(oldSelection, newSelection);
+ }
+
+ @Override
+ public void generateData(Object itemId, Item item, JsonObject rowData) {
+ if (isSelected(itemId)) {
+ rowData.put(GridState.JSONKEY_SELECTED, true);
+ }
+ }
+
+ @Override
+ public void destroyData(Object itemId) {
+ // NO-OP
+ }
+
+ @Override
+ protected Object getItemId(String rowKey) {
+ return rowKey != null ? super.getItemId(rowKey) : null;
+ }
+ }
+
+ /**
+ * A default implementation of a {@link SelectionModel.Single}
+ */
+ public static class SingleSelectionModel extends AbstractSelectionModel
+ implements SelectionModel.Single {
+
+ @Override
+ protected void extend(AbstractClientConnector target) {
+ super.extend(target);
+ registerRpc(new SingleSelectionModelServerRpc() {
+
+ @Override
+ public void select(String rowKey) {
+ SingleSelectionModel.this.select(getItemId(rowKey), false);
+ }
+ });
+ }
+
+ @Override
+ public boolean select(final Object itemId) {
+ return select(itemId, true);
+ }
+
+ protected boolean select(final Object itemId, boolean refresh) {
+ if (itemId == null) {
+ return deselect(getSelectedRow());
+ }
+
+ checkItemIdExists(itemId);
+
+ final Object selectedRow = getSelectedRow();
+ final boolean modified = selection.add(itemId);
+ if (modified) {
+ final Collection<Object> deselected;
+ if (selectedRow != null) {
+ deselectInternal(selectedRow, false, true);
+ deselected = Collections.singleton(selectedRow);
+ } else {
+ deselected = Collections.emptySet();
+ }
+
+ fireSelectionEvent(deselected, selection);
+ }
+
+ if (refresh) {
+ refreshRow(itemId);
+ }
+
+ return modified;
+ }
+
+ private boolean deselect(final Object itemId) {
+ return deselectInternal(itemId, true, true);
+ }
+
+ private boolean deselectInternal(final Object itemId,
+ boolean fireEventIfNeeded, boolean refresh) {
+ final boolean modified = selection.remove(itemId);
+ if (modified) {
+ if (refresh) {
+ refreshRow(itemId);
+ }
+ if (fireEventIfNeeded) {
+ fireSelectionEvent(Collections.singleton(itemId),
+ Collections.emptySet());
+ }
+ }
+ return modified;
+ }
+
+ @Override
+ public Object getSelectedRow() {
+ if (selection.isEmpty()) {
+ return null;
+ } else {
+ return selection.iterator().next();
+ }
+ }
+
+ /**
+ * Resets the selection state.
+ * <p>
+ * If an item is selected, it will become deselected.
+ */
+ @Override
+ public void reset() {
+ deselect(getSelectedRow());
+ }
+
+ @Override
+ public void setDeselectAllowed(boolean deselectAllowed) {
+ getState().deselectAllowed = deselectAllowed;
+ }
+
+ @Override
+ public boolean isDeselectAllowed() {
+ return getState().deselectAllowed;
+ }
+
+ @Override
+ protected SingleSelectionModelState getState() {
+ return (SingleSelectionModelState) super.getState();
+ }
+ }
+
+ /**
+ * A default implementation for a {@link SelectionModel.None}
+ */
+ public static class NoSelectionModel extends AbstractSelectionModel
+ implements SelectionModel.None {
+
+ @Override
+ public boolean isSelected(final Object itemId) {
+ return false;
+ }
+
+ @Override
+ public Collection<Object> getSelectedRows() {
+ return Collections.emptyList();
+ }
+
+ /**
+ * Semantically resets the selection model.
+ * <p>
+ * Effectively a no-op.
+ */
+ @Override
+ public void reset() {
+ // NOOP
+ }
+ }
+
+ /**
+ * A default implementation of a {@link SelectionModel.Multi}
+ */
+ public static class MultiSelectionModel extends AbstractSelectionModel
+ implements SelectionModel.Multi {
+
+ /**
+ * The default selection size limit.
+ *
+ * @see #setSelectionLimit(int)
+ */
+ public static final int DEFAULT_MAX_SELECTIONS = 1000;
+
+ private int selectionLimit = DEFAULT_MAX_SELECTIONS;
+
+ @Override
+ protected void extend(AbstractClientConnector target) {
+ super.extend(target);
+ registerRpc(new MultiSelectionModelServerRpc() {
+
+ @Override
+ public void select(List<String> rowKeys) {
+ List<Object> items = new ArrayList<>();
+ for (String rowKey : rowKeys) {
+ items.add(getItemId(rowKey));
+ }
+ MultiSelectionModel.this.select(items, false);
+ }
+
+ @Override
+ public void deselect(List<String> rowKeys) {
+ List<Object> items = new ArrayList<>();
+ for (String rowKey : rowKeys) {
+ items.add(getItemId(rowKey));
+ }
+ MultiSelectionModel.this.deselect(items, false);
+ }
+
+ @Override
+ public void selectAll() {
+ MultiSelectionModel.this.selectAll(false);
+ }
+
+ @Override
+ public void deselectAll() {
+ MultiSelectionModel.this.deselectAll(false);
+ }
+ });
+ }
+
+ @Override
+ public boolean select(final Object... itemIds)
+ throws IllegalArgumentException {
+ if (itemIds != null) {
+ // select will fire the event
+ return select(Arrays.asList(itemIds));
+ } else {
+ throw new IllegalArgumentException(
+ "Vararg array of itemIds may not be null");
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ * <p>
+ * All items might not be selected if the limit set using
+ * {@link #setSelectionLimit(int)} is exceeded.
+ */
+ @Override
+ public boolean select(final Collection<?> itemIds)
+ throws IllegalArgumentException {
+ return select(itemIds, true);
+ }
+
+ protected boolean select(final Collection<?> itemIds, boolean refresh) {
+ if (itemIds == null) {
+ throw new IllegalArgumentException("itemIds may not be null");
+ }
+
+ // Sanity check
+ checkItemIdsExist(itemIds);
+
+ final boolean selectionWillChange = !selection.containsAll(itemIds)
+ && selection.size() < selectionLimit;
+ if (selectionWillChange) {
+ final HashSet<Object> oldSelection = new HashSet<>(selection);
+ if (selection.size() + itemIds.size() >= selectionLimit) {
+ // Add one at a time if there's a risk of overflow
+ Iterator<?> iterator = itemIds.iterator();
+ while (iterator.hasNext()
+ && selection.size() < selectionLimit) {
+ selection.add(iterator.next());
+ }
+ } else {
+ selection.addAll(itemIds);
+ }
+ fireSelectionEvent(oldSelection, selection);
+ }
+
+ updateAllSelectedState();
+
+ if (refresh) {
+ for (Object itemId : itemIds) {
+ refreshRow(itemId);
+ }
+ }
+
+ return selectionWillChange;
+ }
+
+ /**
+ * Sets the maximum number of rows that can be selected at once. This is
+ * a mechanism to prevent exhausting server memory in situations where
+ * users select lots of rows. If the limit is reached, newly selected
+ * rows will not become recorded.
+ * <p>
+ * Old selections are not discarded if the current number of selected
+ * row exceeds the new limit.
+ * <p>
+ * The default limit is {@value #DEFAULT_MAX_SELECTIONS} rows.
+ *
+ * @param selectionLimit
+ * the non-negative selection limit to set
+ * @throws IllegalArgumentException
+ * if the limit is negative
+ */
+ public void setSelectionLimit(int selectionLimit) {
+ if (selectionLimit < 0) {
+ throw new IllegalArgumentException(
+ "The selection limit must be non-negative");
+ }
+ this.selectionLimit = selectionLimit;
+ }
+
+ /**
+ * Gets the selection limit.
+ *
+ * @see #setSelectionLimit(int)
+ *
+ * @return the selection limit
+ */
+ public int getSelectionLimit() {
+ return selectionLimit;
+ }
+
+ @Override
+ public boolean deselect(final Object... itemIds)
+ throws IllegalArgumentException {
+ if (itemIds != null) {
+ // deselect will fire the event
+ return deselect(Arrays.asList(itemIds));
+ } else {
+ throw new IllegalArgumentException(
+ "Vararg array of itemIds may not be null");
+ }
+ }
+
+ @Override
+ public boolean deselect(final Collection<?> itemIds)
+ throws IllegalArgumentException {
+ return deselect(itemIds, true);
+ }
+
+ protected boolean deselect(final Collection<?> itemIds,
+ boolean refresh) {
+ if (itemIds == null) {
+ throw new IllegalArgumentException("itemIds may not be null");
+ }
+
+ final boolean hasCommonElements = !Collections.disjoint(itemIds,
+ selection);
+ if (hasCommonElements) {
+ final HashSet<Object> oldSelection = new HashSet<>(selection);
+ selection.removeAll(itemIds);
+ fireSelectionEvent(oldSelection, selection);
+ }
+
+ updateAllSelectedState();
+
+ if (refresh) {
+ for (Object itemId : itemIds) {
+ refreshRow(itemId);
+ }
+ }
+
+ return hasCommonElements;
+ }
+
+ @Override
+ public boolean selectAll() {
+ return selectAll(true);
+ }
+
+ protected boolean selectAll(boolean refresh) {
+ // select will fire the event
+ final Indexed container = getParentGrid().getContainerDataSource();
+ if (container != null) {
+ return select(container.getItemIds(), refresh);
+ } else if (selection.isEmpty()) {
+ return false;
+ } else {
+ /*
+ * this should never happen (no container but has a selection),
+ * but I guess the only theoretically correct course of
+ * action...
+ */
+ return deselectAll(false);
+ }
+ }
+
+ @Override
+ public boolean deselectAll() {
+ return deselectAll(true);
+ }
+
+ protected boolean deselectAll(boolean refresh) {
+ // deselect will fire the event
+ return deselect(getSelectedRows(), refresh);
+ }
+
+ /**
+ * {@inheritDoc}
+ * <p>
+ * The returned Collection is in <strong>order of selection</strong>
+ * &ndash; the item that was first selected will be first in the
+ * collection, and so on. Should an item have been selected twice
+ * without being deselected in between, it will have remained in its
+ * original position.
+ */
+ @Override
+ public Collection<Object> getSelectedRows() {
+ // overridden only for JavaDoc
+ return super.getSelectedRows();
+ }
+
+ /**
+ * Resets the selection model.
+ * <p>
+ * Equivalent to calling {@link #deselectAll()}
+ */
+ @Override
+ public void reset() {
+ deselectAll();
+ }
+
+ @Override
+ public boolean setSelected(Collection<?> itemIds)
+ throws IllegalArgumentException {
+ if (itemIds == null) {
+ throw new IllegalArgumentException("itemIds may not be null");
+ }
+
+ checkItemIdsExist(itemIds);
+
+ boolean changed = false;
+ Set<Object> selectedRows = new HashSet<>(itemIds);
+ final Collection<Object> oldSelection = getSelectedRows();
+ Set<Object> added = getDifference(selectedRows, selection);
+ if (!added.isEmpty()) {
+ changed = true;
+ selection.addAll(added);
+ for (Object id : added) {
+ refreshRow(id);
+ }
+ }
+
+ Set<Object> removed = getDifference(selection, selectedRows);
+ if (!removed.isEmpty()) {
+ changed = true;
+ selection.removeAll(removed);
+ for (Object id : removed) {
+ refreshRow(id);
+ }
+ }
+
+ if (changed) {
+ fireSelectionEvent(oldSelection, selection);
+ }
+
+ updateAllSelectedState();
+
+ return changed;
+ }
+
+ /**
+ * Compares two sets and returns a set containing all values that are
+ * present in the first, but not in the second.
+ *
+ * @param set1
+ * first item set
+ * @param set2
+ * second item set
+ * @return all values from set1 which are not present in set2
+ */
+ private static Set<Object> getDifference(Set<Object> set1,
+ Set<Object> set2) {
+ Set<Object> diff = new HashSet<>(set1);
+ diff.removeAll(set2);
+ return diff;
+ }
+
+ @Override
+ public boolean setSelected(Object... itemIds)
+ throws IllegalArgumentException {
+ if (itemIds != null) {
+ return setSelected(Arrays.asList(itemIds));
+ } else {
+ throw new IllegalArgumentException(
+ "Vararg array of itemIds may not be null");
+ }
+ }
+
+ private void updateAllSelectedState() {
+ int totalRowCount = getParentGrid().datasource.size();
+ int rows = Math.min(totalRowCount, selectionLimit);
+ if (getState().allSelected != selection.size() >= rows) {
+ getState().allSelected = selection.size() >= rows;
+ }
+ }
+
+ @Override
+ protected MultiSelectionModelState getState() {
+ return (MultiSelectionModelState) super.getState();
+ }
+ }
+
+ /**
+ * A data class which contains information which identifies a row in a
+ * {@link Grid}.
+ * <p>
+ * Since this class follows the <code>Flyweight</code>-pattern any instance
+ * of this object is subject to change without the user knowing it and so
+ * should not be stored anywhere outside of the method providing these
+ * instances.
+ */
+ public static class RowReference implements Serializable {
+ private final Grid grid;
+
+ private Object itemId;
+
+ /**
+ * Creates a new row reference for the given grid.
+ *
+ * @param grid
+ * the grid that the row belongs to
+ */
+ public RowReference(Grid grid) {
+ this.grid = grid;
+ }
+
+ /**
+ * Sets the identifying information for this row
+ *
+ * @param itemId
+ * the item id of the row
+ */
+ public void set(Object itemId) {
+ this.itemId = itemId;
+ }
+
+ /**
+ * Gets the grid that contains the referenced row.
+ *
+ * @return the grid that contains referenced row
+ */
+ public Grid getGrid() {
+ return grid;
+ }
+
+ /**
+ * Gets the item id of the row.
+ *
+ * @return the item id of the row
+ */
+ public Object getItemId() {
+ return itemId;
+ }
+
+ /**
+ * Gets the item for the row.
+ *
+ * @return the item for the row
+ */
+ public Item getItem() {
+ return grid.getContainerDataSource().getItem(itemId);
+ }
+ }
+
+ /**
+ * A data class which contains information which identifies a cell in a
+ * {@link Grid}.
+ * <p>
+ * Since this class follows the <code>Flyweight</code>-pattern any instance
+ * of this object is subject to change without the user knowing it and so
+ * should not be stored anywhere outside of the method providing these
+ * instances.
+ */
+ public static class CellReference implements Serializable {
+ private final RowReference rowReference;
+
+ private Object propertyId;
+
+ public CellReference(RowReference rowReference) {
+ this.rowReference = rowReference;
+ }
+
+ /**
+ * Sets the identifying information for this cell
+ *
+ * @param propertyId
+ * the property id of the column
+ */
+ public void set(Object propertyId) {
+ this.propertyId = propertyId;
+ }
+
+ /**
+ * Gets the grid that contains the referenced cell.
+ *
+ * @return the grid that contains referenced cell
+ */
+ public Grid getGrid() {
+ return rowReference.getGrid();
+ }
+
+ /**
+ * @return the property id of the column
+ */
+ public Object getPropertyId() {
+ return propertyId;
+ }
+
+ /**
+ * @return the property for the cell
+ */
+ public Property<?> getProperty() {
+ return getItem().getItemProperty(propertyId);
+ }
+
+ /**
+ * Gets the item id of the row of the cell.
+ *
+ * @return the item id of the row
+ */
+ public Object getItemId() {
+ return rowReference.getItemId();
+ }
+
+ /**
+ * Gets the item for the row of the cell.
+ *
+ * @return the item for the row
+ */
+ public Item getItem() {
+ return rowReference.getItem();
+ }
+
+ /**
+ * Gets the value of the cell.
+ *
+ * @return the value of the cell
+ */
+ public Object getValue() {
+ return getProperty().getValue();
+ }
+ }
+
+ /**
+ * A callback interface for generating custom style names for Grid rows.
+ *
+ * @see Grid#setRowStyleGenerator(RowStyleGenerator)
+ */
+ public interface RowStyleGenerator extends Serializable {
+
+ /**
+ * Called by Grid to generate a style name for a row.
+ *
+ * @param row
+ * the row to generate a style for
+ * @return the style name to add to this row, or {@code null} to not set
+ * any style
+ */
+ public String getStyle(RowReference row);
+ }
+
+ /**
+ * A callback interface for generating custom style names for Grid cells.
+ *
+ * @see Grid#setCellStyleGenerator(CellStyleGenerator)
+ */
+ public interface CellStyleGenerator extends Serializable {
+
+ /**
+ * Called by Grid to generate a style name for a column.
+ *
+ * @param cell
+ * the cell to generate a style for
+ * @return the style name to add to this cell, or {@code null} to not
+ * set any style
+ */
+ public String getStyle(CellReference cell);
+ }
+
+ /**
+ * A callback interface for generating optional descriptions (tooltips) for
+ * Grid rows. If a description is generated for a row, it is used for all
+ * the cells in the row for which a {@link CellDescriptionGenerator cell
+ * description} is not generated.
+ *
+ * @see Grid#setRowDescriptionGenerator
+ *
+ * @since 7.6
+ */
+ public interface RowDescriptionGenerator extends Serializable {
+
+ /**
+ * Called by Grid to generate a description (tooltip) for a row. The
+ * description may contain HTML which is rendered directly; if this is
+ * not desired the returned string must be escaped by the implementing
+ * method.
+ *
+ * @param row
+ * the row to generate a description for
+ * @return the row description or {@code null} for no description
+ */
+ public String getDescription(RowReference row);
+ }
+
+ /**
+ * A callback interface for generating optional descriptions (tooltips) for
+ * Grid cells. If a cell has both a {@link RowDescriptionGenerator row
+ * description} and a cell description, the latter has precedence.
+ *
+ * @see Grid#setCellDescriptionGenerator(CellDescriptionGenerator)
+ *
+ * @since 7.6
+ */
+ public interface CellDescriptionGenerator extends Serializable {
+
+ /**
+ * Called by Grid to generate a description (tooltip) for a cell. The
+ * description may contain HTML which is rendered directly; if this is
+ * not desired the returned string must be escaped by the implementing
+ * method.
+ *
+ * @param cell
+ * the cell to generate a description for
+ * @return the cell description or {@code null} for no description
+ */
+ public String getDescription(CellReference cell);
+ }
+
+ /**
+ * Class for generating all row and cell related data for the essential
+ * parts of Grid.
+ */
+ private class RowDataGenerator implements DataGenerator {
+
+ private void put(String key, String value, JsonObject object) {
+ if (value != null && !value.isEmpty()) {
+ object.put(key, value);
+ }
+ }
+
+ @Override
+ public void generateData(Object itemId, Item item, JsonObject rowData) {
+ RowReference row = new RowReference(Grid.this);
+ row.set(itemId);
+
+ if (rowStyleGenerator != null) {
+ String style = rowStyleGenerator.getStyle(row);
+ put(GridState.JSONKEY_ROWSTYLE, style, rowData);
+ }
+
+ if (rowDescriptionGenerator != null) {
+ String description = rowDescriptionGenerator
+ .getDescription(row);
+ put(GridState.JSONKEY_ROWDESCRIPTION, description, rowData);
+
+ }
+
+ JsonObject cellStyles = Json.createObject();
+ JsonObject cellData = Json.createObject();
+ JsonObject cellDescriptions = Json.createObject();
+
+ CellReference cell = new CellReference(row);
+
+ for (Column column : getColumns()) {
+ cell.set(column.getPropertyId());
+
+ writeData(cell, cellData);
+ writeStyles(cell, cellStyles);
+ writeDescriptions(cell, cellDescriptions);
+ }
+
+ if (cellDescriptionGenerator != null
+ && cellDescriptions.keys().length > 0) {
+ rowData.put(GridState.JSONKEY_CELLDESCRIPTION,
+ cellDescriptions);
+ }
+
+ if (cellStyleGenerator != null && cellStyles.keys().length > 0) {
+ rowData.put(GridState.JSONKEY_CELLSTYLES, cellStyles);
+ }
+
+ rowData.put(GridState.JSONKEY_DATA, cellData);
+ }
+
+ private void writeStyles(CellReference cell, JsonObject styles) {
+ if (cellStyleGenerator != null) {
+ String style = cellStyleGenerator.getStyle(cell);
+ put(columnKeys.key(cell.getPropertyId()), style, styles);
+ }
+ }
+
+ private void writeDescriptions(CellReference cell,
+ JsonObject descriptions) {
+ if (cellDescriptionGenerator != null) {
+ String description = cellDescriptionGenerator
+ .getDescription(cell);
+ put(columnKeys.key(cell.getPropertyId()), description,
+ descriptions);
+ }
+ }
+
+ private void writeData(CellReference cell, JsonObject data) {
+ Column column = getColumn(cell.getPropertyId());
+ Converter<?, ?> converter = column.getConverter();
+ Renderer<?> renderer = column.getRenderer();
+
+ Item item = cell.getItem();
+ Object modelValue = item.getItemProperty(cell.getPropertyId())
+ .getValue();
+
+ data.put(columnKeys.key(cell.getPropertyId()), AbstractRenderer
+ .encodeValue(modelValue, renderer, converter, getLocale()));
+ }
+
+ @Override
+ public void destroyData(Object itemId) {
+ // NO-OP
+ }
+ }
+
+ /**
+ * Abstract base class for Grid header and footer sections.
+ *
+ * @since 7.6
+ * @param <ROWTYPE>
+ * the type of the rows in the section
+ */
+ public abstract static class StaticSection<ROWTYPE extends StaticSection.StaticRow<?>>
+ implements Serializable {
+
+ /**
+ * Abstract base class for Grid header and footer rows.
+ *
+ * @param <CELLTYPE>
+ * the type of the cells in the row
+ */
+ public abstract static class StaticRow<CELLTYPE extends StaticCell>
+ implements Serializable {
+
+ private RowState rowState = new RowState();
+ protected StaticSection<?> section;
+ private Map<Object, CELLTYPE> cells = new LinkedHashMap<>();
+ private Map<Set<CELLTYPE>, CELLTYPE> cellGroups = new HashMap<>();
+
+ protected StaticRow(StaticSection<?> section) {
+ this.section = section;
+ }
+
+ protected void addCell(Object propertyId) {
+ CELLTYPE cell = createCell();
+ cell.setColumnId(
+ section.grid.getColumn(propertyId).getState().id);
+ cells.put(propertyId, cell);
+ rowState.cells.add(cell.getCellState());
+ }
+
+ protected void removeCell(Object propertyId) {
+ CELLTYPE cell = cells.remove(propertyId);
+ if (cell != null) {
+ Set<CELLTYPE> cellGroupForCell = getCellGroupForCell(cell);
+ if (cellGroupForCell != null) {
+ removeCellFromGroup(cell, cellGroupForCell);
+ }
+ rowState.cells.remove(cell.getCellState());
+ }
+ }
+
+ private void removeCellFromGroup(CELLTYPE cell,
+ Set<CELLTYPE> cellGroup) {
+ String columnId = cell.getColumnId();
+ for (Set<String> group : rowState.cellGroups.keySet()) {
+ if (group.contains(columnId)) {
+ if (group.size() > 2) {
+ // Update map key correctly
+ CELLTYPE mergedCell = cellGroups.remove(cellGroup);
+ cellGroup.remove(cell);
+ cellGroups.put(cellGroup, mergedCell);
+
+ group.remove(columnId);
+ } else {
+ rowState.cellGroups.remove(group);
+ cellGroups.remove(cellGroup);
+ }
+ return;
+ }
+ }
+ }
+
+ /**
+ * Creates and returns a new instance of the cell type.
+ *
+ * @return the created cell
+ */
+ protected abstract CELLTYPE createCell();
+
+ protected RowState getRowState() {
+ return rowState;
+ }
+
+ /**
+ * Returns the cell for the given property id on this row. If the
+ * column is merged returned cell is the cell for the whole group.
+ *
+ * @param propertyId
+ * the property id of the column
+ * @return the cell for the given property, merged cell for merged
+ * properties, null if not found
+ */
+ public CELLTYPE getCell(Object propertyId) {
+ CELLTYPE cell = cells.get(propertyId);
+ Set<CELLTYPE> cellGroup = getCellGroupForCell(cell);
+ if (cellGroup != null) {
+ cell = cellGroups.get(cellGroup);
+ }
+ return cell;
+ }
+
+ /**
+ * Merges columns cells in a row
+ *
+ * @param propertyIds
+ * The property ids of columns to merge
+ * @return The remaining visible cell after the merge
+ */
+ public CELLTYPE join(Object... propertyIds) {
+ assert propertyIds.length > 1 : "You need to merge at least 2 properties";
+
+ Set<CELLTYPE> cells = new HashSet<>();
+ for (int i = 0; i < propertyIds.length; ++i) {
+ cells.add(getCell(propertyIds[i]));
+ }
+
+ return join(cells);
+ }
+
+ /**
+ * Merges columns cells in a row
+ *
+ * @param cells
+ * The cells to merge. Must be from the same row.
+ * @return The remaining visible cell after the merge
+ */
+ public CELLTYPE join(CELLTYPE... cells) {
+ assert cells.length > 1 : "You need to merge at least 2 cells";
+
+ return join(new HashSet<>(Arrays.asList(cells)));
+ }
+
+ protected CELLTYPE join(Set<CELLTYPE> cells) {
+ for (CELLTYPE cell : cells) {
+ if (getCellGroupForCell(cell) != null) {
+ throw new IllegalArgumentException(
+ "Cell already merged");
+ } else if (!this.cells.containsValue(cell)) {
+ throw new IllegalArgumentException(
+ "Cell does not exist on this row");
+ }
+ }
+
+ // Create new cell data for the group
+ CELLTYPE newCell = createCell();
+
+ Set<String> columnGroup = new HashSet<>();
+ for (CELLTYPE cell : cells) {
+ columnGroup.add(cell.getColumnId());
+ }
+ rowState.cellGroups.put(columnGroup, newCell.getCellState());
+ cellGroups.put(cells, newCell);
+ return newCell;
+ }
+
+ private Set<CELLTYPE> getCellGroupForCell(CELLTYPE cell) {
+ for (Set<CELLTYPE> group : cellGroups.keySet()) {
+ if (group.contains(cell)) {
+ return group;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns the custom style name for this row.
+ *
+ * @return the style name or null if no style name has been set
+ */
+ public String getStyleName() {
+ return getRowState().styleName;
+ }
+
+ /**
+ * Sets a custom style name for this row.
+ *
+ * @param styleName
+ * the style name to set or null to not use any style
+ * name
+ */
+ public void setStyleName(String styleName) {
+ getRowState().styleName = styleName;
+ }
+
+ /**
+ * Writes the declarative design to the given table row element.
+ *
+ * @since 7.5.0
+ * @param trElement
+ * Element to write design to
+ * @param designContext
+ * the design context
+ */
+ protected void writeDesign(Element trElement,
+ DesignContext designContext) {
+ Set<CELLTYPE> visited = new HashSet<>();
+ for (Grid.Column column : section.grid.getColumns()) {
+ CELLTYPE cell = getCell(column.getPropertyId());
+ if (visited.contains(cell)) {
+ continue;
+ }
+ visited.add(cell);
+
+ Element cellElement = trElement
+ .appendElement(getCellTagName());
+ cell.writeDesign(cellElement, designContext);
+
+ for (Entry<Set<CELLTYPE>, CELLTYPE> entry : cellGroups
+ .entrySet()) {
+ if (entry.getValue() == cell) {
+ cellElement.attr("colspan",
+ "" + entry.getKey().size());
+ break;
+ }
+ }
+ }
+ }
+
+ /**
+ * Reads the declarative design from the given table row element.
+ *
+ * @since 7.5.0
+ * @param trElement
+ * Element to read design from
+ * @param designContext
+ * the design context
+ * @throws DesignException
+ * if the given table row contains unexpected children
+ */
+ protected void readDesign(Element trElement,
+ DesignContext designContext) throws DesignException {
+ Elements cellElements = trElement.children();
+ int totalColSpans = 0;
+ for (int i = 0; i < cellElements.size(); ++i) {
+ Element element = cellElements.get(i);
+ if (!element.tagName().equals(getCellTagName())) {
+ throw new DesignException(
+ "Unexpected element in tr while expecting "
+ + getCellTagName() + ": "
+ + element.tagName());
+ }
+
+ int columnIndex = i + totalColSpans;
+
+ int colspan = DesignAttributeHandler.readAttribute(
+ "colspan", element.attributes(), 1, int.class);
+
+ Set<CELLTYPE> cells = new HashSet<>();
+ for (int c = 0; c < colspan; ++c) {
+ cells.add(getCell(section.grid.getColumns()
+ .get(columnIndex + c).getPropertyId()));
+ }
+
+ if (colspan > 1) {
+ totalColSpans += colspan - 1;
+ join(cells).readDesign(element, designContext);
+ } else {
+ cells.iterator().next().readDesign(element,
+ designContext);
+ }
+ }
+ }
+
+ abstract protected String getCellTagName();
+
+ void detach() {
+ for (CELLTYPE cell : cells.values()) {
+ cell.detach();
+ }
+ }
+ }
+
+ /**
+ * A header or footer cell. Has a simple textual caption.
+ */
+ abstract static class StaticCell implements Serializable {
+
+ private CellState cellState = new CellState();
+ private StaticRow<?> row;
+
+ protected StaticCell(StaticRow<?> row) {
+ this.row = row;
+ }
+
+ void setColumnId(String id) {
+ cellState.columnId = id;
+ }
+
+ String getColumnId() {
+ return cellState.columnId;
+ }
+
+ /**
+ * Gets the row where this cell is.
+ *
+ * @return row for this cell
+ */
+ public StaticRow<?> getRow() {
+ return row;
+ }
+
+ protected CellState getCellState() {
+ return cellState;
+ }
+
+ /**
+ * Sets the text displayed in this cell.
+ *
+ * @param text
+ * a plain text caption
+ */
+ public void setText(String text) {
+ removeComponentIfPresent();
+ cellState.text = text;
+ cellState.type = GridStaticCellType.TEXT;
+ row.section.markAsDirty();
+ }
+
+ /**
+ * Returns the text displayed in this cell.
+ *
+ * @return the plain text caption
+ */
+ public String getText() {
+ if (cellState.type != GridStaticCellType.TEXT) {
+ throw new IllegalStateException(
+ "Cannot fetch Text from a cell with type "
+ + cellState.type);
+ }
+ return cellState.text;
+ }
+
+ /**
+ * Returns the HTML content displayed in this cell.
+ *
+ * @return the html
+ *
+ */
+ public String getHtml() {
+ if (cellState.type != GridStaticCellType.HTML) {
+ throw new IllegalStateException(
+ "Cannot fetch HTML from a cell with type "
+ + cellState.type);
+ }
+ return cellState.html;
+ }
+
+ /**
+ * Sets the HTML content displayed in this cell.
+ *
+ * @param html
+ * the html to set
+ */
+ public void setHtml(String html) {
+ removeComponentIfPresent();
+ cellState.html = html;
+ cellState.type = GridStaticCellType.HTML;
+ row.section.markAsDirty();
+ }
+
+ /**
+ * Returns the component displayed in this cell.
+ *
+ * @return the component
+ */
+ public Component getComponent() {
+ if (cellState.type != GridStaticCellType.WIDGET) {
+ throw new IllegalStateException(
+ "Cannot fetch Component from a cell with type "
+ + cellState.type);
+ }
+ return (Component) cellState.connector;
+ }
+
+ /**
+ * Sets the component displayed in this cell.
+ *
+ * @param component
+ * the component to set
+ */
+ public void setComponent(Component component) {
+ removeComponentIfPresent();
+ component.setParent(row.section.grid);
+ cellState.connector = component;
+ cellState.type = GridStaticCellType.WIDGET;
+ row.section.markAsDirty();
+ }
+
+ /**
+ * Returns the type of content stored in this cell.
+ *
+ * @return cell content type
+ */
+ public GridStaticCellType getCellType() {
+ return cellState.type;
+ }
+
+ /**
+ * Returns the custom style name for this cell.
+ *
+ * @return the style name or null if no style name has been set
+ */
+ public String getStyleName() {
+ return cellState.styleName;
+ }
+
+ /**
+ * Sets a custom style name for this cell.
+ *
+ * @param styleName
+ * the style name to set or null to not use any style
+ * name
+ */
+ public void setStyleName(String styleName) {
+ cellState.styleName = styleName;
+ row.section.markAsDirty();
+ }
+
+ private void removeComponentIfPresent() {
+ Component component = (Component) cellState.connector;
+ if (component != null) {
+ component.setParent(null);
+ cellState.connector = null;
+ }
+ }
+
+ /**
+ * Writes the declarative design to the given table cell element.
+ *
+ * @since 7.5.0
+ * @param cellElement
+ * Element to write design to
+ * @param designContext
+ * the design context
+ */
+ protected void writeDesign(Element cellElement,
+ DesignContext designContext) {
+ switch (cellState.type) {
+ case TEXT:
+ cellElement.attr("plain-text", true);
+ cellElement.appendText(getText());
+ break;
+ case HTML:
+ cellElement.append(getHtml());
+ break;
+ case WIDGET:
+ cellElement.appendChild(
+ designContext.createElement(getComponent()));
+ break;
+ }
+ }
+
+ /**
+ * Reads the declarative design from the given table cell element.
+ *
+ * @since 7.5.0
+ * @param cellElement
+ * Element to read design from
+ * @param designContext
+ * the design context
+ */
+ protected void readDesign(Element cellElement,
+ DesignContext designContext) {
+ if (!cellElement.hasAttr("plain-text")) {
+ if (cellElement.children().size() > 0
+ && cellElement.child(0).tagName().contains("-")) {
+ setComponent(
+ designContext.readDesign(cellElement.child(0)));
+ } else {
+ setHtml(cellElement.html());
+ }
+ } else {
+ // text – need to unescape HTML entities
+ setText(DesignFormatter
+ .decodeFromTextNode(cellElement.html()));
+ }
+ }
+
+ void detach() {
+ removeComponentIfPresent();
+ }
+ }
+
+ protected Grid grid;
+ protected List<ROWTYPE> rows = new ArrayList<>();
+
+ /**
+ * Sets the visibility of the whole section.
+ *
+ * @param visible
+ * true to show this section, false to hide
+ */
+ public void setVisible(boolean visible) {
+ if (getSectionState().visible != visible) {
+ getSectionState().visible = visible;
+ markAsDirty();
+ }
+ }
+
+ /**
+ * Returns the visibility of this section.
+ *
+ * @return true if visible, false otherwise.
+ */
+ public boolean isVisible() {
+ return getSectionState().visible;
+ }
+
+ /**
+ * Removes the row at the given position.
+ *
+ * @param rowIndex
+ * the position of the row
+ *
+ * @throws IllegalArgumentException
+ * if no row exists at given index
+ * @see #removeRow(StaticRow)
+ * @see #addRowAt(int)
+ * @see #appendRow()
+ * @see #prependRow()
+ */
+ public ROWTYPE removeRow(int rowIndex) {
+ if (rowIndex >= rows.size() || rowIndex < 0) {
+ throw new IllegalArgumentException(
+ "No row at given index " + rowIndex);
+ }
+ ROWTYPE row = rows.remove(rowIndex);
+ row.detach();
+ getSectionState().rows.remove(rowIndex);
+
+ markAsDirty();
+ return row;
+ }
+
+ /**
+ * Removes the given row from the section.
+ *
+ * @param row
+ * the row to be removed
+ *
+ * @throws IllegalArgumentException
+ * if the row does not exist in this section
+ * @see #removeRow(int)
+ * @see #addRowAt(int)
+ * @see #appendRow()
+ * @see #prependRow()
+ */
+ public void removeRow(ROWTYPE row) {
+ try {
+ removeRow(rows.indexOf(row));
+ } catch (IndexOutOfBoundsException e) {
+ throw new IllegalArgumentException(
+ "Section does not contain the given row");
+ }
+ }
+
+ /**
+ * Gets row at given index.
+ *
+ * @param rowIndex
+ * 0 based index for row. Counted from top to bottom
+ * @return row at given index
+ */
+ public ROWTYPE getRow(int rowIndex) {
+ if (rowIndex >= rows.size() || rowIndex < 0) {
+ throw new IllegalArgumentException(
+ "No row at given index " + rowIndex);
+ }
+ return rows.get(rowIndex);
+ }
+
+ /**
+ * Adds a new row at the top of this section.
+ *
+ * @return the new row
+ * @see #appendRow()
+ * @see #addRowAt(int)
+ * @see #removeRow(StaticRow)
+ * @see #removeRow(int)
+ */
+ public ROWTYPE prependRow() {
+ return addRowAt(0);
+ }
+
+ /**
+ * Adds a new row at the bottom of this section.
+ *
+ * @return the new row
+ * @see #prependRow()
+ * @see #addRowAt(int)
+ * @see #removeRow(StaticRow)
+ * @see #removeRow(int)
+ */
+ public ROWTYPE appendRow() {
+ return addRowAt(rows.size());
+ }
+
+ /**
+ * Inserts a new row at the given position.
+ *
+ * @param index
+ * the position at which to insert the row
+ * @return the new row
+ *
+ * @throws IndexOutOfBoundsException
+ * if the index is out of bounds
+ * @see #appendRow()
+ * @see #prependRow()
+ * @see #removeRow(StaticRow)
+ * @see #removeRow(int)
+ */
+ public ROWTYPE addRowAt(int index) {
+ if (index > rows.size() || index < 0) {
+ throw new IllegalArgumentException(
+ "Unable to add row at index " + index);
+ }
+ ROWTYPE row = createRow();
+ rows.add(index, row);
+ getSectionState().rows.add(index, row.getRowState());
+
+ for (Object id : grid.columns.keySet()) {
+ row.addCell(id);
+ }
+
+ markAsDirty();
+ return row;
+ }
+
+ /**
+ * Gets the amount of rows in this section.
+ *
+ * @return row count
+ */
+ public int getRowCount() {
+ return rows.size();
+ }
+
+ protected abstract GridStaticSectionState getSectionState();
+
+ protected abstract ROWTYPE createRow();
+
+ /**
+ * Informs the grid that state has changed and it should be redrawn.
+ */
+ protected void markAsDirty() {
+ grid.markAsDirty();
+ }
+
+ /**
+ * Removes a column for given property id from the section.
+ *
+ * @param propertyId
+ * property to be removed
+ */
+ protected void removeColumn(Object propertyId) {
+ for (ROWTYPE row : rows) {
+ row.removeCell(propertyId);
+ }
+ }
+
+ /**
+ * Adds a column for given property id to the section.
+ *
+ * @param propertyId
+ * property to be added
+ */
+ protected void addColumn(Object propertyId) {
+ for (ROWTYPE row : rows) {
+ row.addCell(propertyId);
+ }
+ }
+
+ /**
+ * Performs a sanity check that section is in correct state.
+ *
+ * @throws IllegalStateException
+ * if merged cells are not i n continuous range
+ */
+ protected void sanityCheck() throws IllegalStateException {
+ List<String> columnOrder = grid.getState().columnOrder;
+ for (ROWTYPE row : rows) {
+ for (Set<String> cellGroup : row.getRowState().cellGroups
+ .keySet()) {
+ if (!checkCellGroupAndOrder(columnOrder, cellGroup)) {
+ throw new IllegalStateException(
+ "Not all merged cells were in a continuous range.");
+ }
+ }
+ }
+ }
+
+ private boolean checkCellGroupAndOrder(List<String> columnOrder,
+ Set<String> cellGroup) {
+ if (!columnOrder.containsAll(cellGroup)) {
+ return false;
+ }
+
+ for (int i = 0; i < columnOrder.size(); ++i) {
+ if (!cellGroup.contains(columnOrder.get(i))) {
+ continue;
+ }
+
+ for (int j = 1; j < cellGroup.size(); ++j) {
+ if (!cellGroup.contains(columnOrder.get(i + j))) {
+ return false;
+ }
+ }
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Writes the declarative design to the given table section element.
+ *
+ * @since 7.5.0
+ * @param tableSectionElement
+ * Element to write design to
+ * @param designContext
+ * the design context
+ */
+ protected void writeDesign(Element tableSectionElement,
+ DesignContext designContext) {
+ for (ROWTYPE row : rows) {
+ row.writeDesign(tableSectionElement.appendElement("tr"),
+ designContext);
+ }
+ }
+
+ /**
+ * Writes the declarative design from the given table section element.
+ *
+ * @since 7.5.0
+ * @param tableSectionElement
+ * Element to read design from
+ * @param designContext
+ * the design context
+ * @throws DesignException
+ * if the table section contains unexpected children
+ */
+ protected void readDesign(Element tableSectionElement,
+ DesignContext designContext) throws DesignException {
+ while (rows.size() > 0) {
+ removeRow(0);
+ }
+
+ for (Element row : tableSectionElement.children()) {
+ if (!row.tagName().equals("tr")) {
+ throw new DesignException("Unexpected element in "
+ + tableSectionElement.tagName() + ": "
+ + row.tagName());
+ }
+ appendRow().readDesign(row, designContext);
+ }
+ }
+ }
+
+ /**
+ * Represents the header section of a Grid.
+ */
+ protected static class Header extends StaticSection<HeaderRow> {
+
+ private HeaderRow defaultRow = null;
+ private final GridStaticSectionState headerState = new GridStaticSectionState();
+
+ protected Header(Grid grid) {
+ this.grid = grid;
+ grid.getState(true).header = headerState;
+ HeaderRow row = createRow();
+ rows.add(row);
+ setDefaultRow(row);
+ getSectionState().rows.add(row.getRowState());
+ }
+
+ /**
+ * Sets the default row of this header. The default row is a special
+ * header row providing a user interface for sorting columns.
+ *
+ * @param row
+ * the new default row, or null for no default row
+ *
+ * @throws IllegalArgumentException
+ * this header does not contain the row
+ */
+ public void setDefaultRow(HeaderRow row) {
+ if (row == defaultRow) {
+ return;
+ }
+
+ if (row != null && !rows.contains(row)) {
+ throw new IllegalArgumentException(
+ "Cannot set a default row that does not exist in the section");
+ }
+
+ if (defaultRow != null) {
+ defaultRow.setDefaultRow(false);
+ }
+
+ if (row != null) {
+ row.setDefaultRow(true);
+ }
+
+ defaultRow = row;
+ markAsDirty();
+ }
+
+ /**
+ * Returns the current default row of this header. The default row is a
+ * special header row providing a user interface for sorting columns.
+ *
+ * @return the default row or null if no default row set
+ */
+ public HeaderRow getDefaultRow() {
+ return defaultRow;
+ }
+
+ @Override
+ protected GridStaticSectionState getSectionState() {
+ return headerState;
+ }
+
+ @Override
+ protected HeaderRow createRow() {
+ return new HeaderRow(this);
+ }
+
+ @Override
+ public HeaderRow removeRow(int rowIndex) {
+ HeaderRow row = super.removeRow(rowIndex);
+ if (row == defaultRow) {
+ // Default Header Row was just removed.
+ setDefaultRow(null);
+ }
+ return row;
+ }
+
+ @Override
+ protected void sanityCheck() throws IllegalStateException {
+ super.sanityCheck();
+
+ boolean hasDefaultRow = false;
+ for (HeaderRow row : rows) {
+ if (row.getRowState().defaultRow) {
+ if (!hasDefaultRow) {
+ hasDefaultRow = true;
+ } else {
+ throw new IllegalStateException(
+ "Multiple default rows in header");
+ }
+ }
+ }
+ }
+
+ @Override
+ protected void readDesign(Element tableSectionElement,
+ DesignContext designContext) {
+ super.readDesign(tableSectionElement, designContext);
+
+ if (defaultRow == null && !rows.isEmpty()) {
+ grid.setDefaultHeaderRow(rows.get(0));
+ }
+ }
+ }
+
+ /**
+ * Represents a header row in Grid.
+ */
+ public static class HeaderRow extends StaticSection.StaticRow<HeaderCell> {
+
+ protected HeaderRow(StaticSection<?> section) {
+ super(section);
+ }
+
+ private void setDefaultRow(boolean value) {
+ getRowState().defaultRow = value;
+ }
+
+ private boolean isDefaultRow() {
+ return getRowState().defaultRow;
+ }
+
+ @Override
+ protected HeaderCell createCell() {
+ return new HeaderCell(this);
+ }
+
+ @Override
+ protected String getCellTagName() {
+ return "th";
+ }
+
+ @Override
+ protected void writeDesign(Element trElement,
+ DesignContext designContext) {
+ super.writeDesign(trElement, designContext);
+
+ if (section.grid.getDefaultHeaderRow() == this) {
+ DesignAttributeHandler.writeAttribute("default",
+ trElement.attributes(), true, null, boolean.class);
+ }
+ }
+
+ @Override
+ protected void readDesign(Element trElement,
+ DesignContext designContext) {
+ super.readDesign(trElement, designContext);
+
+ boolean defaultRow = DesignAttributeHandler.readAttribute("default",
+ trElement.attributes(), false, boolean.class);
+ if (defaultRow) {
+ section.grid.setDefaultHeaderRow(this);
+ }
+ }
+ }
+
+ /**
+ * Represents a header cell in Grid. Can be a merged cell for multiple
+ * columns.
+ */
+ public static class HeaderCell extends StaticSection.StaticCell {
+
+ protected HeaderCell(HeaderRow row) {
+ super(row);
+ }
+ }
+
+ /**
+ * Represents the footer section of a Grid. By default Footer is not
+ * visible.
+ */
+ protected static class Footer extends StaticSection<FooterRow> {
+
+ private final GridStaticSectionState footerState = new GridStaticSectionState();
+
+ protected Footer(Grid grid) {
+ this.grid = grid;
+ grid.getState(true).footer = footerState;
+ }
+
+ @Override
+ protected GridStaticSectionState getSectionState() {
+ return footerState;
+ }
+
+ @Override
+ protected FooterRow createRow() {
+ return new FooterRow(this);
+ }
+
+ @Override
+ protected void sanityCheck() throws IllegalStateException {
+ super.sanityCheck();
+ }
+ }
+
+ /**
+ * Represents a footer row in Grid.
+ */
+ public static class FooterRow extends StaticSection.StaticRow<FooterCell> {
+
+ protected FooterRow(StaticSection<?> section) {
+ super(section);
+ }
+
+ @Override
+ protected FooterCell createCell() {
+ return new FooterCell(this);
+ }
+
+ @Override
+ protected String getCellTagName() {
+ return "td";
+ }
+
+ }
+
+ /**
+ * Represents a footer cell in Grid.
+ */
+ public static class FooterCell extends StaticSection.StaticCell {
+
+ protected FooterCell(FooterRow row) {
+ super(row);
+ }
+ }
+
+ /**
+ * A column in the grid. Can be obtained by calling
+ * {@link Grid#getColumn(Object propertyId)}.
+ */
+ public static class Column implements Serializable {
+
+ /**
+ * The state of the column shared to the client
+ */
+ private final GridColumnState state;
+
+ /**
+ * The grid this column is associated with
+ */
+ private final Grid grid;
+
+ /**
+ * Backing property for column
+ */
+ private final Object propertyId;
+
+ private Converter<?, Object> converter;
+
+ /**
+ * A check for allowing the
+ * {@link #Column(Grid, GridColumnState, Object) constructor} to call
+ * {@link #setConverter(Converter)} with a <code>null</code>, even
+ * if model and renderer aren't compatible.
+ */
+ private boolean isFirstConverterAssignment = true;
+
+ /**
+ * Internally used constructor.
+ *
+ * @param grid
+ * The grid this column belongs to. Should not be null.
+ * @param state
+ * the shared state of this column
+ * @param propertyId
+ * the backing property id for this column
+ */
+ Column(Grid grid, GridColumnState state, Object propertyId) {
+ this.grid = grid;
+ this.state = state;
+ this.propertyId = propertyId;
+ internalSetRenderer(new TextRenderer());
+ }
+
+ /**
+ * Returns the serializable state of this column that is sent to the
+ * client side connector.
+ *
+ * @return the internal state of the column
+ */
+ GridColumnState getState() {
+ return state;
+ }
+
+ /**
+ * Returns the property id for the backing property of this Column
+ *
+ * @return property id
+ */
+ public Object getPropertyId() {
+ return propertyId;
+ }
+
+ /**
+ * Returns the caption of the header. By default the header caption is
+ * the property id of the column.
+ *
+ * @return the text in the default row of header.
+ *
+ * @throws IllegalStateException
+ * if the column no longer is attached to the grid
+ */
+ public String getHeaderCaption() throws IllegalStateException {
+ checkColumnIsAttached();
+
+ return state.headerCaption;
+ }
+
+ /**
+ * Sets the caption of the header. This caption is also used as the
+ * hiding toggle caption, unless it is explicitly set via
+ * {@link #setHidingToggleCaption(String)}.
+ *
+ * @param caption
+ * the text to show in the caption
+ * @return the column itself
+ *
+ * @throws IllegalStateException
+ * if the column is no longer attached to any grid
+ */
+ public Column setHeaderCaption(String caption)
+ throws IllegalStateException {
+ checkColumnIsAttached();
+ if (caption == null) {
+ caption = ""; // Render null as empty
+ }
+ state.headerCaption = caption;
+
+ HeaderRow row = grid.getHeader().getDefaultRow();
+ if (row != null) {
+ row.getCell(grid.getPropertyIdByColumnId(state.id))
+ .setText(caption);
+ }
+ return this;
+ }
+
+ /**
+ * Gets the caption of the hiding toggle for this column.
+ *
+ * @since 7.5.0
+ * @see #setHidingToggleCaption(String)
+ * @return the caption for the hiding toggle for this column
+ * @throws IllegalStateException
+ * if the column is no longer attached to any grid
+ */
+ public String getHidingToggleCaption() throws IllegalStateException {
+ checkColumnIsAttached();
+ return state.hidingToggleCaption;
+ }
+
+ /**
+ * Sets the caption of the hiding toggle for this column. Shown in the
+ * toggle for this column in the grid's sidebar when the column is
+ * {@link #isHidable() hidable}.
+ * <p>
+ * The default value is <code>null</code>, and in that case the column's
+ * {@link #getHeaderCaption() header caption} is used.
+ * <p>
+ * <em>NOTE:</em> setting this to empty string might cause the hiding
+ * toggle to not render correctly.
+ *
+ * @since 7.5.0
+ * @param hidingToggleCaption
+ * the text to show in the column hiding toggle
+ * @return the column itself
+ * @throws IllegalStateException
+ * if the column is no longer attached to any grid
+ */
+ public Column setHidingToggleCaption(String hidingToggleCaption)
+ throws IllegalStateException {
+ checkColumnIsAttached();
+ state.hidingToggleCaption = hidingToggleCaption;
+ grid.markAsDirty();
+ return this;
+ }
+
+ /**
+ * Returns the width (in pixels). By default a column is 100px wide.
+ *
+ * @return the width in pixels of the column
+ * @throws IllegalStateException
+ * if the column is no longer attached to any grid
+ */
+ public double getWidth() throws IllegalStateException {
+ checkColumnIsAttached();
+ return state.width;
+ }
+
+ /**
+ * Sets the width (in pixels).
+ * <p>
+ * This overrides any configuration set by any of
+ * {@link #setExpandRatio(int)}, {@link #setMinimumWidth(double)} or
+ * {@link #setMaximumWidth(double)}.
+ *
+ * @param pixelWidth
+ * the new pixel width of the column
+ * @return the column itself
+ *
+ * @throws IllegalStateException
+ * if the column is no longer attached to any grid
+ * @throws IllegalArgumentException
+ * thrown if pixel width is less than zero
+ */
+ public Column setWidth(double pixelWidth)
+ throws IllegalStateException, IllegalArgumentException {
+ checkColumnIsAttached();
+ if (pixelWidth < 0) {
+ throw new IllegalArgumentException(
+ "Pixel width should be greated than 0 (in " + toString()
+ + ")");
+ }
+ if (state.width != pixelWidth) {
+ state.width = pixelWidth;
+ grid.markAsDirty();
+ grid.fireColumnResizeEvent(this, false);
+ }
+ return this;
+ }
+
+ /**
+ * Returns whether this column has an undefined width.
+ *
+ * @since 7.6
+ * @return whether the width is undefined
+ * @throws IllegalStateException
+ * if the column is no longer attached to any grid
+ */
+ public boolean isWidthUndefined() {
+ checkColumnIsAttached();
+ return state.width < 0;
+ }
+
+ /**
+ * Marks the column width as undefined. An undefined width means the
+ * grid is free to resize the column based on the cell contents and
+ * available space in the grid.
+ *
+ * @return the column itself
+ */
+ public Column setWidthUndefined() {
+ checkColumnIsAttached();
+ if (!isWidthUndefined()) {
+ state.width = -1;
+ grid.markAsDirty();
+ grid.fireColumnResizeEvent(this, false);
+ }
+ return this;
+ }
+
+ /**
+ * Checks if column is attached and throws an
+ * {@link IllegalStateException} if it is not
+ *
+ * @throws IllegalStateException
+ * if the column is no longer attached to any grid
+ */
+ protected void checkColumnIsAttached() throws IllegalStateException {
+ if (grid.getColumnByColumnId(state.id) == null) {
+ throw new IllegalStateException("Column no longer exists.");
+ }
+ }
+
+ /**
+ * Sets this column as the last frozen column in its grid.
+ *
+ * @return the column itself
+ *
+ * @throws IllegalArgumentException
+ * if the column is no longer attached to any grid
+ * @see Grid#setFrozenColumnCount(int)
+ */
+ public Column setLastFrozenColumn() {
+ checkColumnIsAttached();
+ grid.setFrozenColumnCount(
+ grid.getState(false).columnOrder.indexOf(getState().id)
+ + 1);
+ return this;
+ }
+
+ /**
+ * Sets the renderer for this column.
+ * <p>
+ * If a suitable converter isn't defined explicitly, the session
+ * converter factory is used to find a compatible converter.
+ *
+ * @param renderer
+ * the renderer to use
+ * @return the column itself
+ *
+ * @throws IllegalArgumentException
+ * if no compatible converter could be found
+ *
+ * @see VaadinSession#getConverterFactory()
+ * @see ConverterUtil#getConverter(Class, Class, VaadinSession)
+ * @see #setConverter(Converter)
+ */
+ public Column setRenderer(Renderer<?> renderer) {
+ if (!internalSetRenderer(renderer)) {
+ throw new IllegalArgumentException(
+ "Could not find a converter for converting from the model type "
+ + getModelType()
+ + " to the renderer presentation type "
+ + renderer.getPresentationType() + " (in "
+ + toString() + ")");
+ }
+ return this;
+ }
+
+ /**
+ * Sets the renderer for this column and the converter used to convert
+ * from the property value type to the renderer presentation type.
+ *
+ * @param renderer
+ * the renderer to use, cannot be null
+ * @param converter
+ * the converter to use
+ * @return the column itself
+ *
+ * @throws IllegalArgumentException
+ * if the renderer is already associated with a grid column
+ */
+ public <T> Column setRenderer(Renderer<T> renderer,
+ Converter<? extends T, ?> converter) {
+ if (renderer.getParent() != null) {
+ throw new IllegalArgumentException(
+ "Cannot set a renderer that is already connected to a grid column (in "
+ + toString() + ")");
+ }
+
+ if (getRenderer() != null) {
+ grid.removeExtension(getRenderer());
+ }
+
+ grid.addRenderer(renderer);
+ state.rendererConnector = renderer;
+ setConverter(converter);
+ return this;
+ }
+
+ /**
+ * Sets the converter used to convert from the property value type to
+ * the renderer presentation type.
+ *
+ * @param converter
+ * the converter to use, or {@code null} to not use any
+ * converters
+ * @return the column itself
+ *
+ * @throws IllegalArgumentException
+ * if the types are not compatible
+ */
+ public Column setConverter(Converter<?, ?> converter)
+ throws IllegalArgumentException {
+ Class<?> modelType = getModelType();
+ if (converter != null) {
+ if (!converter.getModelType().isAssignableFrom(modelType)) {
+ throw new IllegalArgumentException(
+ "The converter model type "
+ + converter.getModelType()
+ + " is not compatible with the property type "
+ + modelType + " (in " + toString() + ")");
+
+ } else if (!getRenderer().getPresentationType()
+ .isAssignableFrom(converter.getPresentationType())) {
+ throw new IllegalArgumentException(
+ "The converter presentation type "
+ + converter.getPresentationType()
+ + " is not compatible with the renderer presentation type "
+ + getRenderer().getPresentationType()
+ + " (in " + toString() + ")");
+ }
+ }
+
+ else {
+ /*
+ * Since the converter is null (i.e. will be removed), we need
+ * to know that the renderer and model are compatible. If not,
+ * we can't allow for this to happen.
+ *
+ * The constructor is allowed to call this method with null
+ * without any compatibility checks, therefore we have a special
+ * case for it.
+ */
+
+ Class<?> rendererPresentationType = getRenderer()
+ .getPresentationType();
+ if (!isFirstConverterAssignment && !rendererPresentationType
+ .isAssignableFrom(modelType)) {
+ throw new IllegalArgumentException(
+ "Cannot remove converter, "
+ + "as renderer's presentation type "
+ + rendererPresentationType.getName()
+ + " and column's " + "model "
+ + modelType.getName() + " type aren't "
+ + "directly compatible with each other (in "
+ + toString() + ")");
+ }
+ }
+
+ isFirstConverterAssignment = false;
+
+ @SuppressWarnings("unchecked")
+ Converter<?, Object> castConverter = (Converter<?, Object>) converter;
+ this.converter = castConverter;
+
+ return this;
+ }
+
+ /**
+ * Returns the renderer instance used by this column.
+ *
+ * @return the renderer
+ */
+ public Renderer<?> getRenderer() {
+ return (Renderer<?>) getState().rendererConnector;
+ }
+
+ /**
+ * Returns the converter instance used by this column.
+ *
+ * @return the converter
+ */
+ public Converter<?, ?> getConverter() {
+ return converter;
+ }
+
+ private <T> boolean internalSetRenderer(Renderer<T> renderer) {
+
+ Converter<? extends T, ?> converter;
+ if (isCompatibleWithProperty(renderer, getConverter())) {
+ // Use the existing converter (possibly none) if types
+ // compatible
+ converter = (Converter<? extends T, ?>) getConverter();
+ } else {
+ converter = ConverterUtil.getConverter(
+ renderer.getPresentationType(), getModelType(),
+ getSession());
+ }
+ setRenderer(renderer, converter);
+ return isCompatibleWithProperty(renderer, converter);
+ }
+
+ private VaadinSession getSession() {
+ UI ui = grid.getUI();
+ return ui != null ? ui.getSession() : null;
+ }
+
+ private boolean isCompatibleWithProperty(Renderer<?> renderer,
+ Converter<?, ?> converter) {
+ Class<?> type;
+ if (converter == null) {
+ type = getModelType();
+ } else {
+ type = converter.getPresentationType();
+ }
+ return renderer.getPresentationType().isAssignableFrom(type);
+ }
+
+ private Class<?> getModelType() {
+ return grid.getContainerDataSource()
+ .getType(grid.getPropertyIdByColumnId(state.id));
+ }
+
+ /**
+ * Sets whether this column is sortable by the user. The grid can be
+ * sorted by a sortable column by clicking or tapping the column's
+ * default header. Programmatic sorting using the Grid#sort methods is
+ * not affected by this setting.
+ *
+ * @param sortable
+ * {@code true} if the user should be able to sort the
+ * column, {@code false} otherwise
+ * @return the column itself
+ *
+ * @throws IllegalStateException
+ * if the data source of the Grid does not implement
+ * {@link Sortable}
+ * @throws IllegalStateException
+ * if the data source does not support sorting by the
+ * property associated with this column
+ */
+ public Column setSortable(boolean sortable) {
+ checkColumnIsAttached();
+
+ if (sortable) {
+ if (!(grid.datasource instanceof Sortable)) {
+ throw new IllegalStateException("Can't set column "
+ + toString()
+ + " sortable. The Container of Grid does not implement Sortable");
+ } else if (!((Sortable) grid.datasource)
+ .getSortableContainerPropertyIds()
+ .contains(propertyId)) {
+ throw new IllegalStateException(
+ "Can't set column " + toString()
+ + " sortable. Container doesn't support sorting by property "
+ + propertyId);
+ }
+ }
+
+ state.sortable = sortable;
+ grid.markAsDirty();
+ return this;
+ }
+
+ /**
+ * Returns whether the user can sort the grid by this column.
+ * <p>
+ * <em>Note:</em> it is possible to sort by this column programmatically
+ * using the Grid#sort methods regardless of the returned value.
+ *
+ * @return {@code true} if the column is sortable by the user,
+ * {@code false} otherwise
+ */
+ public boolean isSortable() {
+ return state.sortable;
+ }
+
+ @Override
+ public String toString() {
+ return getClass().getSimpleName() + "[propertyId:"
+ + grid.getPropertyIdByColumnId(state.id) + "]";
+ }
+
+ /**
+ * Sets the ratio with which the column expands.
+ * <p>
+ * By default, all columns expand equally (treated as if all of them had
+ * an expand ratio of 1). Once at least one column gets a defined expand
+ * ratio, the implicit expand ratio is removed, and only the defined
+ * expand ratios are taken into account.
+ * <p>
+ * If a column has a defined width ({@link #setWidth(double)}), it
+ * overrides this method's effects.
+ * <p>
+ * <em>Example:</em> A grid with three columns, with expand ratios 0, 1
+ * and 2, respectively. The column with a <strong>ratio of 0 is exactly
+ * as wide as its contents requires</strong>. The column with a ratio of
+ * 1 is as wide as it needs, <strong>plus a third of any excess
+ * space</strong>, because we have 3 parts total, and this column
+ * reserves only one of those. The column with a ratio of 2, is as wide
+ * as it needs to be, <strong>plus two thirds</strong> of the excess
+ * width.
+ *
+ * @param expandRatio
+ * the expand ratio of this column. {@code 0} to not have it
+ * expand at all. A negative number to clear the expand
+ * value.
+ * @throws IllegalStateException
+ * if the column is no longer attached to any grid
+ * @see #setWidth(double)
+ */
+ public Column setExpandRatio(int expandRatio)
+ throws IllegalStateException {
+ checkColumnIsAttached();
+
+ getState().expandRatio = expandRatio;
+ grid.markAsDirty();
+ return this;
+ }
+
+ /**
+ * Returns the column's expand ratio.
+ *
+ * @return the column's expand ratio
+ * @see #setExpandRatio(int)
+ */
+ public int getExpandRatio() {
+ return getState().expandRatio;
+ }
+
+ /**
+ * Clears the expand ratio for this column.
+ * <p>
+ * Equal to calling {@link #setExpandRatio(int) setExpandRatio(-1)}
+ *
+ * @throws IllegalStateException
+ * if the column is no longer attached to any grid
+ */
+ public Column clearExpandRatio() throws IllegalStateException {
+ return setExpandRatio(-1);
+ }
+
+ /**
+ * Sets the minimum width for this column.
+ * <p>
+ * This defines the minimum guaranteed pixel width of the column
+ * <em>when it is set to expand</em>.
+ *
+ * @throws IllegalStateException
+ * if the column is no longer attached to any grid
+ * @see #setExpandRatio(int)
+ */
+ public Column setMinimumWidth(double pixels)
+ throws IllegalStateException {
+ checkColumnIsAttached();
+
+ final double maxwidth = getMaximumWidth();
+ if (pixels >= 0 && pixels > maxwidth && maxwidth >= 0) {
+ throw new IllegalArgumentException("New minimum width ("
+ + pixels + ") was greater than maximum width ("
+ + maxwidth + ")");
+ }
+ getState().minWidth = pixels;
+ grid.markAsDirty();
+ return this;
+ }
+
+ /**
+ * Return the minimum width for this column.
+ *
+ * @return the minimum width for this column
+ * @see #setMinimumWidth(double)
+ */
+ public double getMinimumWidth() {
+ return getState().minWidth;
+ }
+
+ /**
+ * Sets the maximum width for this column.
+ * <p>
+ * This defines the maximum allowed pixel width of the column <em>when
+ * it is set to expand</em>.
+ *
+ * @param pixels
+ * the maximum width
+ * @throws IllegalStateException
+ * if the column is no longer attached to any grid
+ * @see #setExpandRatio(int)
+ */
+ public Column setMaximumWidth(double pixels) {
+ checkColumnIsAttached();
+
+ final double minwidth = getMinimumWidth();
+ if (pixels >= 0 && pixels < minwidth && minwidth >= 0) {
+ throw new IllegalArgumentException("New maximum width ("
+ + pixels + ") was less than minimum width (" + minwidth
+ + ")");
+ }
+
+ getState().maxWidth = pixels;
+ grid.markAsDirty();
+ return this;
+ }
+
+ /**
+ * Returns the maximum width for this column.
+ *
+ * @return the maximum width for this column
+ * @see #setMaximumWidth(double)
+ */
+ public double getMaximumWidth() {
+ return getState().maxWidth;
+ }
+
+ /**
+ * Sets whether the properties corresponding to this column should be
+ * editable when the item editor is active. By default columns are
+ * editable.
+ * <p>
+ * Values in non-editable columns are currently not displayed when the
+ * editor is active, but this will probably change in the future. They
+ * are not automatically assigned an editor field and, if one is
+ * manually assigned, it is not used. Columns that cannot (or should
+ * not) be edited even in principle should be set non-editable.
+ *
+ * @param editable
+ * {@code true} if this column should be editable,
+ * {@code false} otherwise
+ * @return this column
+ *
+ * @throws IllegalStateException
+ * if the editor is currently active
+ *
+ * @see Grid#editItem(Object)
+ * @see Grid#isEditorActive()
+ */
+ public Column setEditable(boolean editable) {
+ checkColumnIsAttached();
+ if (grid.isEditorActive()) {
+ throw new IllegalStateException(
+ "Cannot change column editable status while the editor is active");
+ }
+ getState().editable = editable;
+ grid.markAsDirty();
+ return this;
+ }
+
+ /**
+ * Returns whether the properties corresponding to this column should be
+ * editable when the item editor is active.
+ *
+ * @return {@code true} if this column is editable, {@code false}
+ * otherwise
+ *
+ * @see Grid#editItem(Object)
+ * @see #setEditable(boolean)
+ */
+
+ public boolean isEditable() {
+ return getState().editable;
+ }
+
+ /**
+ * Sets the field component used to edit the properties in this column
+ * when the item editor is active. If an item has not been set, then the
+ * binding is postponed until the item is set using
+ * {@link #editItem(Object)}.
+ * <p>
+ * Setting the field to <code>null</code> clears any previously set
+ * field, causing a new field to be created the next time the item
+ * editor is opened.
+ *
+ * @param editor
+ * the editor field
+ * @return this column
+ */
+ public Column setEditorField(Field<?> editor) {
+ grid.setEditorField(getPropertyId(), editor);
+ return this;
+ }
+
+ /**
+ * Returns the editor field used to edit the properties in this column
+ * when the item editor is active. Returns null if the column is not
+ * {@link Column#isEditable() editable}.
+ * <p>
+ * When {@link #editItem(Object) editItem} is called, fields are
+ * automatically created and bound for any unbound properties.
+ * <p>
+ * Getting a field before the editor has been opened depends on special
+ * support from the {@link FieldGroup} in use. Using this method with a
+ * user-provided <code>FieldGroup</code> might cause
+ * {@link com.vaadin.v7.data.fieldgroup.FieldGroup.BindException
+ * BindException} to be thrown.
+ *
+ * @return the bound field; or <code>null</code> if the respective
+ * column is not editable
+ *
+ * @throws IllegalArgumentException
+ * if there is no column for the provided property id
+ * @throws FieldGroup.BindException
+ * if no field has been configured and there is a problem
+ * building or binding
+ */
+ public Field<?> getEditorField() {
+ return grid.getEditorField(getPropertyId());
+ }
+
+ /**
+ * Hides or shows the column. By default columns are visible before
+ * explicitly hiding them.
+ *
+ * @since 7.5.0
+ * @param hidden
+ * <code>true</code> to hide the column, <code>false</code>
+ * to show
+ * @return this column
+ */
+ public Column setHidden(boolean hidden) {
+ if (hidden != getState().hidden) {
+ getState().hidden = hidden;
+ grid.markAsDirty();
+ grid.fireColumnVisibilityChangeEvent(this, hidden, false);
+ }
+ return this;
+ }
+
+ /**
+ * Returns whether this column is hidden. Default is {@code false}.
+ *
+ * @since 7.5.0
+ * @return <code>true</code> if the column is currently hidden,
+ * <code>false</code> otherwise
+ */
+ public boolean isHidden() {
+ return getState().hidden;
+ }
+
+ /**
+ * Sets whether this column can be hidden by the user. Hidable columns
+ * can be hidden and shown via the sidebar menu.
+ *
+ * @since 7.5.0
+ * @param hidable
+ * <code>true</code> iff the column may be hidable by the
+ * user via UI interaction
+ * @return this column
+ */
+ public Column setHidable(boolean hidable) {
+ if (hidable != getState().hidable) {
+ getState().hidable = hidable;
+ grid.markAsDirty();
+ }
+ return this;
+ }
+
+ /**
+ * Returns whether this column can be hidden by the user. Default is
+ * {@code false}.
+ * <p>
+ * <em>Note:</em> the column can be programmatically hidden using
+ * {@link #setHidden(boolean)} regardless of the returned value.
+ *
+ * @since 7.5.0
+ * @return <code>true</code> if the user can hide the column,
+ * <code>false</code> if not
+ */
+ public boolean isHidable() {
+ return getState().hidable;
+ }
+
+ /**
+ * Sets whether this column can be resized by the user.
+ *
+ * @since 7.6
+ * @param resizable
+ * {@code true} if this column should be resizable,
+ * {@code false} otherwise
+ */
+ public Column setResizable(boolean resizable) {
+ if (resizable != getState().resizable) {
+ getState().resizable = resizable;
+ grid.markAsDirty();
+ }
+ return this;
+ }
+
+ /**
+ * Returns whether this column can be resized by the user. Default is
+ * {@code true}.
+ * <p>
+ * <em>Note:</em> the column can be programmatically resized using
+ * {@link #setWidth(double)} and {@link #setWidthUndefined()} regardless
+ * of the returned value.
+ *
+ * @since 7.6
+ * @return {@code true} if this column is resizable, {@code false}
+ * otherwise
+ */
+ public boolean isResizable() {
+ return getState().resizable;
+ }
+
+ /**
+ * Writes the design attributes for this column into given element.
+ *
+ * @since 7.5.0
+ *
+ * @param design
+ * Element to write attributes into
+ *
+ * @param designContext
+ * the design context
+ */
+ protected void writeDesign(Element design,
+ DesignContext designContext) {
+ Attributes attributes = design.attributes();
+ GridColumnState def = new GridColumnState();
+
+ DesignAttributeHandler.writeAttribute("property-id", attributes,
+ getPropertyId(), null, Object.class);
+
+ // Sortable is a special attribute that depends on the container.
+ DesignAttributeHandler.writeAttribute("sortable", attributes,
+ isSortable(), null, boolean.class);
+ DesignAttributeHandler.writeAttribute("editable", attributes,
+ isEditable(), def.editable, boolean.class);
+ DesignAttributeHandler.writeAttribute("resizable", attributes,
+ isResizable(), def.resizable, boolean.class);
+
+ DesignAttributeHandler.writeAttribute("hidable", attributes,
+ isHidable(), def.hidable, boolean.class);
+ DesignAttributeHandler.writeAttribute("hidden", attributes,
+ isHidden(), def.hidden, boolean.class);
+ DesignAttributeHandler.writeAttribute("hiding-toggle-caption",
+ attributes, getHidingToggleCaption(), null, String.class);
+
+ DesignAttributeHandler.writeAttribute("width", attributes,
+ getWidth(), def.width, Double.class);
+ DesignAttributeHandler.writeAttribute("min-width", attributes,
+ getMinimumWidth(), def.minWidth, Double.class);
+ DesignAttributeHandler.writeAttribute("max-width", attributes,
+ getMaximumWidth(), def.maxWidth, Double.class);
+ DesignAttributeHandler.writeAttribute("expand", attributes,
+ getExpandRatio(), def.expandRatio, Integer.class);
+ }
+
+ /**
+ * Reads the design attributes for this column from given element.
+ *
+ * @since 7.5.0
+ * @param design
+ * Element to read attributes from
+ * @param designContext
+ * the design context
+ */
+ protected void readDesign(Element design, DesignContext designContext) {
+ Attributes attributes = design.attributes();
+
+ if (design.hasAttr("sortable")) {
+ setSortable(DesignAttributeHandler.readAttribute("sortable",
+ attributes, boolean.class));
+ }
+ if (design.hasAttr("editable")) {
+ setEditable(DesignAttributeHandler.readAttribute("editable",
+ attributes, boolean.class));
+ }
+ if (design.hasAttr("resizable")) {
+ setResizable(DesignAttributeHandler.readAttribute("resizable",
+ attributes, boolean.class));
+ }
+
+ if (design.hasAttr("hidable")) {
+ setHidable(DesignAttributeHandler.readAttribute("hidable",
+ attributes, boolean.class));
+ }
+ if (design.hasAttr("hidden")) {
+ setHidden(DesignAttributeHandler.readAttribute("hidden",
+ attributes, boolean.class));
+ }
+ if (design.hasAttr("hiding-toggle-caption")) {
+ setHidingToggleCaption(DesignAttributeHandler.readAttribute(
+ "hiding-toggle-caption", attributes, String.class));
+ }
+
+ // Read size info where necessary.
+ if (design.hasAttr("width")) {
+ setWidth(DesignAttributeHandler.readAttribute("width",
+ attributes, Double.class));
+ }
+ if (design.hasAttr("min-width")) {
+ setMinimumWidth(DesignAttributeHandler
+ .readAttribute("min-width", attributes, Double.class));
+ }
+ if (design.hasAttr("max-width")) {
+ setMaximumWidth(DesignAttributeHandler
+ .readAttribute("max-width", attributes, Double.class));
+ }
+ if (design.hasAttr("expand")) {
+ if (design.attr("expand").isEmpty()) {
+ setExpandRatio(1);
+ } else {
+ setExpandRatio(DesignAttributeHandler.readAttribute(
+ "expand", attributes, Integer.class));
+ }
+ }
+ }
+ }
+
+ /**
+ * An abstract base class for server-side
+ * {@link com.vaadin.v7.ui.renderers.Renderer Grid renderers}. This class
+ * currently extends the AbstractExtension superclass, but this fact should
+ * be regarded as an implementation detail and subject to change in a future
+ * major or minor Vaadin revision.
+ *
+ * @param <T>
+ * the type this renderer knows how to present
+ */
+ public static abstract class AbstractRenderer<T>
+ extends AbstractGridExtension implements Renderer<T> {
+
+ private final Class<T> presentationType;
+
+ private final String nullRepresentation;
+
+ protected AbstractRenderer(Class<T> presentationType,
+ String nullRepresentation) {
+ this.presentationType = presentationType;
+ this.nullRepresentation = nullRepresentation;
+ }
+
+ protected AbstractRenderer(Class<T> presentationType) {
+ this(presentationType, null);
+ }
+
+ /**
+ * This method is inherited from AbstractExtension but should never be
+ * called directly with an AbstractRenderer.
+ */
+ @Deprecated
+ @Override
+ protected Class<Grid> getSupportedParentType() {
+ return Grid.class;
+ }
+
+ /**
+ * This method is inherited from AbstractExtension but should never be
+ * called directly with an AbstractRenderer.
+ */
+ @Deprecated
+ @Override
+ protected void extend(AbstractClientConnector target) {
+ super.extend(target);
+ }
+
+ @Override
+ public Class<T> getPresentationType() {
+ return presentationType;
+ }
+
+ @Override
+ public JsonValue encode(T value) {
+ if (value == null) {
+ return encode(getNullRepresentation(), String.class);
+ } else {
+ return encode(value, getPresentationType());
+ }
+ }
+
+ /**
+ * Null representation for the renderer
+ *
+ * @return a textual representation of {@code null}
+ */
+ protected String getNullRepresentation() {
+ return nullRepresentation;
+ }
+
+ /**
+ * Encodes the given value to JSON.
+ * <p>
+ * This is a helper method that can be invoked by an
+ * {@link #encode(Object) encode(T)} override if serializing a value of
+ * type other than {@link #getPresentationType() the presentation type}
+ * is desired. For instance, a {@code Renderer<Date>} could first turn a
+ * date value into a formatted string and return
+ * {@code encode(dateString, String.class)}.
+ *
+ * @param value
+ * the value to be encoded
+ * @param type
+ * the type of the value
+ * @return a JSON representation of the given value
+ */
+ protected <U> JsonValue encode(U value, Class<U> type) {
+ return JsonCodec
+ .encode(value, null, type, getUI().getConnectorTracker())
+ .getEncodedValue();
+ }
+
+ /**
+ * Converts and encodes the given data model property value using the
+ * given converter and renderer. This method is public only for testing
+ * purposes.
+ *
+ * @since 7.6
+ * @param renderer
+ * the renderer to use
+ * @param converter
+ * the converter to use
+ * @param modelValue
+ * the value to convert and encode
+ * @param locale
+ * the locale to use in conversion
+ * @return an encoded value ready to be sent to the client
+ */
+ public static <T> JsonValue encodeValue(Object modelValue,
+ Renderer<T> renderer, Converter<?, ?> converter,
+ Locale locale) {
+ Class<T> presentationType = renderer.getPresentationType();
+ T presentationValue;
+
+ if (converter == null) {
+ try {
+ presentationValue = presentationType.cast(modelValue);
+ } catch (ClassCastException e) {
+ if (presentationType == String.class) {
+ // If there is no converter, just fallback to using
+ // toString(). modelValue can't be null as
+ // Class.cast(null) will always succeed
+ presentationValue = (T) modelValue.toString();
+ } else {
+ throw new Converter.ConversionException(
+ "Unable to convert value of type "
+ + modelValue.getClass().getName()
+ + " to presentation type "
+ + presentationType.getName()
+ + ". No converter is set and the types are not compatible.");
+ }
+ }
+ } else {
+ assert presentationType
+ .isAssignableFrom(converter.getPresentationType());
+ @SuppressWarnings("unchecked")
+ Converter<T, Object> safeConverter = (Converter<T, Object>) converter;
+ presentationValue = safeConverter.convertToPresentation(
+ modelValue, safeConverter.getPresentationType(),
+ locale);
+ }
+
+ JsonValue encodedValue;
+ try {
+ encodedValue = renderer.encode(presentationValue);
+ } catch (Exception e) {
+ getLogger().log(Level.SEVERE, "Unable to encode data", e);
+ encodedValue = renderer.encode(null);
+ }
+
+ return encodedValue;
+ }
+
+ private static Logger getLogger() {
+ return Logger.getLogger(AbstractRenderer.class.getName());
+ }
+
+ }
+
+ /**
+ * An abstract base class for server-side Grid extensions.
+ * <p>
+ * Note: If the extension is an instance of {@link DataGenerator} it will
+ * automatically register itself to {@link RpcDataProviderExtension} of
+ * extended Grid. On remove this registration is automatically removed.
+ *
+ * @since 7.5
+ */
+ public static abstract class AbstractGridExtension
+ extends AbstractExtension {
+
+ /**
+ * Constructs a new Grid extension.
+ */
+ public AbstractGridExtension() {
+ super();
+ }
+
+ /**
+ * Constructs a new Grid extension and extends given Grid.
+ *
+ * @param grid
+ * a grid instance
+ */
+ public AbstractGridExtension(Grid grid) {
+ super();
+ extend(grid);
+ }
+
+ @Override
+ protected void extend(AbstractClientConnector target) {
+ super.extend(target);
+
+ if (this instanceof DataGenerator) {
+ getParentGrid().datasourceExtension
+ .addDataGenerator((DataGenerator) this);
+ }
+ }
+
+ @Override
+ public void remove() {
+ if (this instanceof DataGenerator) {
+ getParentGrid().datasourceExtension
+ .removeDataGenerator((DataGenerator) this);
+ }
+
+ super.remove();
+ }
+
+ /**
+ * Gets the item id for a row key.
+ * <p>
+ * A key is used to identify a particular row on both a server and a
+ * client. This method can be used to get the item id for the row key
+ * that the client has sent.
+ *
+ * @param rowKey
+ * the row key for which to retrieve an item id
+ * @return the item id corresponding to {@code key}
+ */
+ protected Object getItemId(String rowKey) {
+ return getParentGrid().getKeyMapper().get(rowKey);
+ }
+
+ /**
+ * Gets the column for a column id.
+ * <p>
+ * An id is used to identify a particular column on both a server and a
+ * client. This method can be used to get the column for the column id
+ * that the client has sent.
+ *
+ * @param columnId
+ * the column id for which to retrieve a column
+ * @return the column corresponding to {@code columnId}
+ */
+ protected Column getColumn(String columnId) {
+ return getParentGrid().getColumnByColumnId(columnId);
+ }
+
+ /**
+ * Gets the parent Grid of the renderer.
+ *
+ * @return parent grid
+ * @throws IllegalStateException
+ * if parent is not Grid
+ */
+ protected Grid getParentGrid() {
+ if (getParent() instanceof Grid) {
+ Grid grid = (Grid) getParent();
+ return grid;
+ } else if (getParent() == null) {
+ throw new IllegalStateException(
+ "Renderer is not attached to any parent");
+ } else {
+ throw new IllegalStateException(
+ "Renderers can be used only with Grid. Extended "
+ + getParent().getClass().getSimpleName()
+ + " instead");
+ }
+ }
+
+ /**
+ * Resends the row data for given item id to the client.
+ *
+ * @since 7.6
+ * @param itemId
+ * row to refresh
+ */
+ protected void refreshRow(Object itemId) {
+ getParentGrid().datasourceExtension.updateRowData(itemId);
+ }
+
+ /**
+ * Informs the parent Grid that this Extension wants to add a child
+ * component to it.
+ *
+ * @since 7.6
+ * @param c
+ * component
+ */
+ protected void addComponentToGrid(Component c) {
+ getParentGrid().addComponent(c);
+ }
+
+ /**
+ * Informs the parent Grid that this Extension wants to remove a child
+ * component from it.
+ *
+ * @since 7.6
+ * @param c
+ * component
+ */
+ protected void removeComponentFromGrid(Component c) {
+ getParentGrid().removeComponent(c);
+ }
+ }
+
+ /**
+ * The data source attached to the grid
+ */
+ private Container.Indexed datasource;
+
+ /**
+ * Property id to column instance mapping
+ */
+ private final Map<Object, Column> columns = new HashMap<>();
+
+ /**
+ * Key generator for column server-to-client communication
+ */
+ private final KeyMapper<Object> columnKeys = new KeyMapper<>();
+
+ /**
+ * The current sort order
+ */
+ private final List<SortOrder> sortOrder = new ArrayList<>();
+
+ /**
+ * Property listener for listening to changes in data source properties.
+ */
+ private final PropertySetChangeListener propertyListener = new PropertySetChangeListener() {
+
+ @Override
+ public void containerPropertySetChange(PropertySetChangeEvent event) {
+ Collection<?> properties = new HashSet<Object>(
+ event.getContainer().getContainerPropertyIds());
+
+ // Find columns that need to be removed.
+ List<Column> removedColumns = new LinkedList<>();
+ for (Object propertyId : columns.keySet()) {
+ if (!properties.contains(propertyId)) {
+ removedColumns.add(getColumn(propertyId));
+ }
+ }
+
+ // Actually remove columns.
+ for (Column column : removedColumns) {
+ Object propertyId = column.getPropertyId();
+ internalRemoveColumn(propertyId);
+ columnKeys.remove(propertyId);
+ }
+ datasourceExtension.columnsRemoved(removedColumns);
+
+ // Add new columns
+ List<Column> addedColumns = new LinkedList<>();
+ for (Object propertyId : properties) {
+ if (!columns.containsKey(propertyId)) {
+ addedColumns.add(appendColumn(propertyId));
+ }
+ }
+ datasourceExtension.columnsAdded(addedColumns);
+
+ if (getFrozenColumnCount() > columns.size()) {
+ setFrozenColumnCount(columns.size());
+ }
+
+ // Unset sortable for non-sortable columns.
+ if (datasource instanceof Sortable) {
+ Collection<?> sortables = ((Sortable) datasource)
+ .getSortableContainerPropertyIds();
+ for (Object propertyId : columns.keySet()) {
+ Column column = columns.get(propertyId);
+ if (!sortables.contains(propertyId)
+ && column.isSortable()) {
+ column.setSortable(false);
+ }
+ }
+ }
+ }
+ };
+
+ private final ItemSetChangeListener editorClosingItemSetListener = new ItemSetChangeListener() {
+ @Override
+ public void containerItemSetChange(ItemSetChangeEvent event) {
+ cancelEditor();
+ }
+ };
+
+ private RpcDataProviderExtension datasourceExtension;
+
+ /**
+ * The selection model that is currently in use. Never <code>null</code>
+ * after the constructor has been run.
+ */
+ private SelectionModel selectionModel;
+
+ /**
+ * Used to know whether selection change events originate from the server or
+ * the client so the selection change handler knows whether the changes
+ * should be sent to the client.
+ */
+ private boolean applyingSelectionFromClient;
+
+ private final Header header = new Header(this);
+ private final Footer footer = new Footer(this);
+
+ private Object editedItemId = null;
+ private boolean editorActive = false;
+ private FieldGroup editorFieldGroup = new CustomFieldGroup();
+
+ private CellStyleGenerator cellStyleGenerator;
+ private RowStyleGenerator rowStyleGenerator;
+
+ private CellDescriptionGenerator cellDescriptionGenerator;
+ private RowDescriptionGenerator rowDescriptionGenerator;
+
+ /**
+ * <code>true</code> if Grid is using the internal IndexedContainer created
+ * in Grid() constructor, or <code>false</code> if the user has set their
+ * own Container.
+ *
+ * @see #setContainerDataSource(Indexed)
+ * @see #LegacyGrid()
+ */
+ private boolean defaultContainer = true;
+
+ private EditorErrorHandler editorErrorHandler = new DefaultEditorErrorHandler();
+
+ private DetailComponentManager detailComponentManager = null;
+
+ private Set<Component> extensionComponents = new HashSet<>();
+
+ private static final Method SELECTION_CHANGE_METHOD = ReflectTools
+ .findMethod(SelectionListener.class, "select",
+ SelectionEvent.class);
+
+ private static final Method SORT_ORDER_CHANGE_METHOD = ReflectTools
+ .findMethod(SortListener.class, "sort", SortEvent.class);
+
+ private static final Method COLUMN_REORDER_METHOD = ReflectTools.findMethod(
+ ColumnReorderListener.class, "columnReorder",
+ ColumnReorderEvent.class);
+
+ private static final Method COLUMN_RESIZE_METHOD = ReflectTools.findMethod(
+ ColumnResizeListener.class, "columnResize",
+ ColumnResizeEvent.class);
+
+ private static final Method COLUMN_VISIBILITY_METHOD = ReflectTools
+ .findMethod(ColumnVisibilityChangeListener.class,
+ "columnVisibilityChanged",
+ ColumnVisibilityChangeEvent.class);
+
+ /**
+ * Creates a new Grid with a new {@link IndexedContainer} as the data
+ * source.
+ */
+ public Grid() {
+ this(null, null);
+ }
+
+ /**
+ * Creates a new Grid using the given data source.
+ *
+ * @param dataSource
+ * the indexed container to use as a data source
+ */
+ public Grid(final Container.Indexed dataSource) {
+ this(null, dataSource);
+ }
+
+ /**
+ * Creates a new Grid with the given caption and a new
+ * {@link IndexedContainer} data source.
+ *
+ * @param caption
+ * the caption of the grid
+ */
+ public Grid(String caption) {
+ this(caption, null);
+ }
+
+ /**
+ * Creates a new Grid with the given caption and data source. If the data
+ * source is null, a new {@link IndexedContainer} will be used.
+ *
+ * @param caption
+ * the caption of the grid
+ * @param dataSource
+ * the indexed container to use as a data source
+ */
+ public Grid(String caption, Container.Indexed dataSource) {
+ if (dataSource == null) {
+ internalSetContainerDataSource(new IndexedContainer());
+ } else {
+ setContainerDataSource(dataSource);
+ }
+ setCaption(caption);
+ initGrid();
+ }
+
+ /**
+ * Grid initial setup
+ */
+ private void initGrid() {
+ setSelectionMode(getDefaultSelectionMode());
+
+ registerRpc(new GridServerRpc() {
+
+ @Override
+ public void sort(String[] columnIds, SortDirection[] directions,
+ boolean userOriginated) {
+ assert columnIds.length == directions.length;
+
+ List<SortOrder> order = new ArrayList<>(columnIds.length);
+ for (int i = 0; i < columnIds.length; i++) {
+ Object propertyId = getPropertyIdByColumnId(columnIds[i]);
+ order.add(new SortOrder(propertyId, directions[i]));
+ }
+ setSortOrder(order, userOriginated);
+ if (!order.equals(getSortOrder())) {
+ /*
+ * Actual sort order is not what the client expects. Make
+ * sure the client gets a state change event by clearing the
+ * diffstate and marking as dirty
+ */
+ ConnectorTracker connectorTracker = getUI()
+ .getConnectorTracker();
+ JsonObject diffState = connectorTracker
+ .getDiffState(Grid.this);
+ diffState.remove("sortColumns");
+ diffState.remove("sortDirs");
+ markAsDirty();
+ }
+ }
+
+ @Override
+ public void itemClick(String rowKey, String columnId,
+ MouseEventDetails details) {
+ Object itemId = getKeyMapper().get(rowKey);
+ Item item = datasource.getItem(itemId);
+ Object propertyId = getPropertyIdByColumnId(columnId);
+ fireEvent(new ItemClickEvent(Grid.this, item, itemId,
+ propertyId, details));
+ }
+
+ @Override
+ public void columnsReordered(List<String> newColumnOrder,
+ List<String> oldColumnOrder) {
+ final String diffStateKey = "columnOrder";
+ ConnectorTracker connectorTracker = getUI()
+ .getConnectorTracker();
+ JsonObject diffState = connectorTracker.getDiffState(Grid.this);
+ // discard the change if the columns have been reordered from
+ // the server side, as the server side is always right
+ if (getState(false).columnOrder.equals(oldColumnOrder)) {
+ // Don't mark as dirty since client has the state already
+ getState(false).columnOrder = newColumnOrder;
+ // write changes to diffState so that possible reverting the
+ // column order is sent to client
+ assert diffState
+ .hasKey(diffStateKey) : "Field name has changed";
+ Type type = null;
+ try {
+ type = (getState(false).getClass()
+ .getDeclaredField(diffStateKey)
+ .getGenericType());
+ } catch (NoSuchFieldException e) {
+ e.printStackTrace();
+ } catch (SecurityException e) {
+ e.printStackTrace();
+ }
+ EncodeResult encodeResult = JsonCodec.encode(
+ getState(false).columnOrder, diffState, type,
+ connectorTracker);
+
+ diffState.put(diffStateKey, encodeResult.getEncodedValue());
+ fireColumnReorderEvent(true);
+ } else {
+ // make sure the client is reverted to the order that the
+ // server thinks it is
+ diffState.remove(diffStateKey);
+ markAsDirty();
+ }
+ }
+
+ @Override
+ public void columnVisibilityChanged(String id, boolean hidden,
+ boolean userOriginated) {
+ final Column column = getColumnByColumnId(id);
+ final GridColumnState columnState = column.getState();
+
+ if (columnState.hidden != hidden) {
+ columnState.hidden = hidden;
+
+ final String diffStateKey = "columns";
+ ConnectorTracker connectorTracker = getUI()
+ .getConnectorTracker();
+ JsonObject diffState = connectorTracker
+ .getDiffState(Grid.this);
+
+ assert diffState
+ .hasKey(diffStateKey) : "Field name has changed";
+ Type type = null;
+ try {
+ type = (getState(false).getClass()
+ .getDeclaredField(diffStateKey)
+ .getGenericType());
+ } catch (NoSuchFieldException e) {
+ e.printStackTrace();
+ } catch (SecurityException e) {
+ e.printStackTrace();
+ }
+ EncodeResult encodeResult = JsonCodec.encode(
+ getState(false).columns, diffState, type,
+ connectorTracker);
+
+ diffState.put(diffStateKey, encodeResult.getEncodedValue());
+
+ fireColumnVisibilityChangeEvent(column, hidden,
+ userOriginated);
+ }
+ }
+
+ @Override
+ public void contextClick(int rowIndex, String rowKey,
+ String columnId, Section section,
+ MouseEventDetails details) {
+ Object itemId = null;
+ if (rowKey != null) {
+ itemId = getKeyMapper().get(rowKey);
+ }
+ fireEvent(new GridContextClickEvent(Grid.this, details, section,
+ rowIndex, itemId, getPropertyIdByColumnId(columnId)));
+ }
+
+ @Override
+ public void columnResized(String id, double pixels) {
+ final Column column = getColumnByColumnId(id);
+ if (column != null && column.isResizable()) {
+ column.getState().width = pixels;
+ fireColumnResizeEvent(column, true);
+ markAsDirty();
+ }
+ }
+ });
+
+ registerRpc(new EditorServerRpc() {
+
+ @Override
+ public void bind(int rowIndex) {
+ try {
+ Object id = getContainerDataSource().getIdByIndex(rowIndex);
+
+ final boolean opening = editedItemId == null;
+
+ final boolean moving = !opening && !editedItemId.equals(id);
+
+ final boolean allowMove = !isEditorBuffered()
+ && getEditorFieldGroup().isValid();
+
+ if (opening || !moving || allowMove) {
+ doBind(id);
+ } else {
+ failBind(null);
+ }
+ } catch (Exception e) {
+ failBind(e);
+ }
+ }
+
+ private void doBind(Object id) {
+ editedItemId = id;
+ doEditItem();
+ getEditorRpc().confirmBind(true);
+ }
+
+ private void failBind(Exception e) {
+ if (e != null) {
+ handleError(e);
+ }
+ getEditorRpc().confirmBind(false);
+ }
+
+ @Override
+ public void cancel(int rowIndex) {
+ try {
+ // For future proofing even though cannot currently fail
+ doCancelEditor();
+ } catch (Exception e) {
+ handleError(e);
+ }
+ }
+
+ @Override
+ public void save(int rowIndex) {
+ List<String> errorColumnIds = null;
+ String errorMessage = null;
+ boolean success = false;
+ try {
+ saveEditor();
+ success = true;
+ } catch (CommitException e) {
+ try {
+ CommitErrorEvent event = new CommitErrorEvent(Grid.this,
+ e);
+ getEditorErrorHandler().commitError(event);
+
+ errorMessage = event.getUserErrorMessage();
+
+ errorColumnIds = new ArrayList<>();
+ for (Column column : event.getErrorColumns()) {
+ errorColumnIds.add(column.state.id);
+ }
+ } catch (Exception ee) {
+ // A badly written error handler can throw an exception,
+ // which would lock up the Grid
+ handleError(ee);
+ }
+ } catch (Exception e) {
+ handleError(e);
+ }
+ getEditorRpc().confirmSave(success, errorMessage,
+ errorColumnIds);
+ }
+
+ private void handleError(Exception e) {
+ com.vaadin.server.ErrorEvent.findErrorHandler(Grid.this)
+ .error(new ConnectorErrorEvent(Grid.this, e));
+ }
+ });
+ }
+
+ @Override
+ public void beforeClientResponse(boolean initial) {
+ try {
+ header.sanityCheck();
+ footer.sanityCheck();
+ } catch (Exception e) {
+ e.printStackTrace();
+ setComponentError(new ErrorMessage() {
+
+ @Override
+ public ErrorLevel getErrorLevel() {
+ return ErrorLevel.CRITICAL;
+ }
+
+ @Override
+ public String getFormattedHtmlMessage() {
+ return "Incorrectly merged cells";
+ }
+
+ });
+ }
+
+ super.beforeClientResponse(initial);
+ }
+
+ /**
+ * Sets the grid data source.
+ * <p>
+ *
+ * <strong>Note</strong> Grid columns are based on properties and try to
+ * detect a correct converter for the data type. The columns are not
+ * reinitialized automatically if the container is changed, and if the same
+ * properties are present after container change, the columns are reused.
+ * Properties with same names, but different data types will lead to
+ * unpredictable behaviour.
+ *
+ * @param container
+ * The container data source. Cannot be null.
+ * @throws IllegalArgumentException
+ * if the data source is null
+ */
+ public void setContainerDataSource(Container.Indexed container) {
+ defaultContainer = false;
+ internalSetContainerDataSource(container);
+ }
+
+ private void internalSetContainerDataSource(Container.Indexed container) {
+ if (container == null) {
+ throw new IllegalArgumentException(
+ "Cannot set the datasource to null");
+ }
+ if (datasource == container) {
+ return;
+ }
+
+ // Remove old listeners
+ if (datasource instanceof PropertySetChangeNotifier) {
+ ((PropertySetChangeNotifier) datasource)
+ .removePropertySetChangeListener(propertyListener);
+ }
+
+ if (datasourceExtension != null) {
+ removeExtension(datasourceExtension);
+ }
+
+ // Remove old DetailComponentManager
+ if (detailComponentManager != null) {
+ detailComponentManager.remove();
+ }
+
+ resetEditor();
+
+ datasource = container;
+
+ //
+ // Adjust sort order
+ //
+
+ if (container instanceof Container.Sortable) {
+
+ // If the container is sortable, go through the current sort order
+ // and match each item to the sortable properties of the new
+ // container. If the new container does not support an item in the
+ // current sort order, that item is removed from the current sort
+ // order list.
+ Collection<?> sortableProps = ((Container.Sortable) getContainerDataSource())
+ .getSortableContainerPropertyIds();
+
+ Iterator<SortOrder> i = sortOrder.iterator();
+ while (i.hasNext()) {
+ if (!sortableProps.contains(i.next().getPropertyId())) {
+ i.remove();
+ }
+ }
+
+ sort(false);
+ } else {
+ // Clear sorting order. Don't sort.
+ sortOrder.clear();
+ }
+
+ datasourceExtension = new RpcDataProviderExtension(container);
+ datasourceExtension.extend(this);
+ datasourceExtension.addDataGenerator(new RowDataGenerator());
+ for (Extension e : getExtensions()) {
+ if (e instanceof DataGenerator) {
+ datasourceExtension.addDataGenerator((DataGenerator) e);
+ }
+ }
+
+ if (detailComponentManager != null) {
+ detailComponentManager = new DetailComponentManager(this,
+ detailComponentManager.getDetailsGenerator());
+ } else {
+ detailComponentManager = new DetailComponentManager(this);
+ }
+
+ /*
+ * selectionModel == null when the invocation comes from the
+ * constructor.
+ */
+ if (selectionModel != null) {
+ selectionModel.reset();
+ }
+
+ // Listen to changes in properties and remove columns if needed
+ if (datasource instanceof PropertySetChangeNotifier) {
+ ((PropertySetChangeNotifier) datasource)
+ .addPropertySetChangeListener(propertyListener);
+ }
+
+ /*
+ * activeRowHandler will be updated by the client-side request that
+ * occurs on container change - no need to actively re-insert any
+ * ValueChangeListeners at this point.
+ */
+
+ setFrozenColumnCount(0);
+
+ if (columns.isEmpty()) {
+ // Add columns
+ for (Object propertyId : datasource.getContainerPropertyIds()) {
+ Column column = appendColumn(propertyId);
+
+ // Initial sorting is defined by container
+ if (datasource instanceof Sortable) {
+ column.setSortable(((Sortable) datasource)
+ .getSortableContainerPropertyIds()
+ .contains(propertyId));
+ } else {
+ column.setSortable(false);
+ }
+ }
+ } else {
+ Collection<?> properties = datasource.getContainerPropertyIds();
+ for (Object property : columns.keySet()) {
+ if (!properties.contains(property)) {
+ throw new IllegalStateException(
+ "Found at least one column in Grid that does not exist in the given container: "
+ + property + " with the header \""
+ + getColumn(property).getHeaderCaption()
+ + "\". "
+ + "Call removeAllColumns() before setContainerDataSource() if you want to reconfigure the columns based on the new container.");
+ }
+
+ if (!(datasource instanceof Sortable)
+ || !((Sortable) datasource)
+ .getSortableContainerPropertyIds()
+ .contains(property)) {
+ columns.get(property).setSortable(false);
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns the grid data source.
+ *
+ * @return the container data source of the grid
+ */
+ public Container.Indexed getContainerDataSource() {
+ return datasource;
+ }
+
+ /**
+ * Returns a column based on the property id
+ *
+ * @param propertyId
+ * the property id of the column
+ * @return the column or <code>null</code> if not found
+ */
+ public Column getColumn(Object propertyId) {
+ return columns.get(propertyId);
+ }
+
+ /**
+ * Returns a copy of currently configures columns in their current visual
+ * order in this Grid.
+ *
+ * @return unmodifiable copy of current columns in visual order
+ */
+ public List<Column> getColumns() {
+ List<Column> columns = new ArrayList<>();
+ for (String columnId : getState(false).columnOrder) {
+ columns.add(getColumnByColumnId(columnId));
+ }
+ return Collections.unmodifiableList(columns);
+ }
+
+ /**
+ * Adds a new Column to Grid. Also adds the property to container with data
+ * type String, if property for column does not exist in it. Default value
+ * for the new property is an empty String.
+ * <p>
+ * Note that adding a new property is only done for the default container
+ * that Grid sets up with the default constructor.
+ *
+ * @param propertyId
+ * the property id of the new column
+ * @return the new column
+ *
+ * @throws IllegalStateException
+ * if column for given property already exists in this grid
+ */
+
+ public Column addColumn(Object propertyId) throws IllegalStateException {
+ if (datasource.getContainerPropertyIds().contains(propertyId)
+ && !columns.containsKey(propertyId)) {
+ appendColumn(propertyId);
+ } else if (defaultContainer) {
+ addColumnProperty(propertyId, String.class, "");
+ } else {
+ if (columns.containsKey(propertyId)) {
+ throw new IllegalStateException(
+ "A column for property id '" + propertyId.toString()
+ + "' already exists in this grid");
+ } else {
+ throw new IllegalStateException(
+ "Property id '" + propertyId.toString()
+ + "' does not exist in the container");
+ }
+ }
+
+ // Inform the data provider of this new column.
+ Column column = getColumn(propertyId);
+ List<Column> addedColumns = new ArrayList<>();
+ addedColumns.add(column);
+ datasourceExtension.columnsAdded(addedColumns);
+
+ return column;
+ }
+
+ /**
+ * Adds a new Column to Grid. This function makes sure that the property
+ * with the given id and data type exists in the container. If property does
+ * not exists, it will be created.
+ * <p>
+ * Default value for the new property is 0 if type is Integer, Double and
+ * Float. If type is String, default value is an empty string. For all other
+ * types the default value is null.
+ * <p>
+ * Note that adding a new property is only done for the default container
+ * that Grid sets up with the default constructor.
+ *
+ * @param propertyId
+ * the property id of the new column
+ * @param type
+ * the data type for the new property
+ * @return the new column
+ *
+ * @throws IllegalStateException
+ * if column for given property already exists in this grid or
+ * property already exists in the container with wrong type
+ */
+ public Column addColumn(Object propertyId, Class<?> type) {
+ addColumnProperty(propertyId, type, null);
+ return getColumn(propertyId);
+ }
+
+ protected void addColumnProperty(Object propertyId, Class<?> type,
+ Object defaultValue) throws IllegalStateException {
+ if (!defaultContainer) {
+ throw new IllegalStateException(
+ "Container for this Grid is not a default container from Grid() constructor");
+ }
+
+ if (!columns.containsKey(propertyId)) {
+ if (!datasource.getContainerPropertyIds().contains(propertyId)) {
+ datasource.addContainerProperty(propertyId, type, defaultValue);
+ } else {
+ Property<?> containerProperty = datasource.getContainerProperty(
+ datasource.firstItemId(), propertyId);
+ if (containerProperty.getType() == type) {
+ appendColumn(propertyId);
+ } else {
+ throw new IllegalStateException(
+ "DataSource already has the given property "
+ + propertyId + " with a different type");
+ }
+ }
+ } else {
+ throw new IllegalStateException(
+ "Grid already has a column for property " + propertyId);
+ }
+ }
+
+ /**
+ * Removes all columns from this Grid.
+ */
+ public void removeAllColumns() {
+ List<Column> removed = new ArrayList<>(columns.values());
+ Set<Object> properties = new HashSet<>(columns.keySet());
+ for (Object propertyId : properties) {
+ removeColumn(propertyId);
+ }
+ datasourceExtension.columnsRemoved(removed);
+ }
+
+ /**
+ * Used internally by the {@link Grid} to get a {@link Column} by
+ * referencing its generated state id. Also used by {@link Column} to verify
+ * if it has been detached from the {@link Grid}.
+ *
+ * @param columnId
+ * the client id generated for the column when the column is
+ * added to the grid
+ * @return the column with the id or <code>null</code> if not found
+ */
+ Column getColumnByColumnId(String columnId) {
+ Object propertyId = getPropertyIdByColumnId(columnId);
+ return getColumn(propertyId);
+ }
+
+ /**
+ * Used internally by the {@link Grid} to get a property id by referencing
+ * the columns generated state id.
+ *
+ * @param columnId
+ * The state id of the column
+ * @return The column instance or null if not found
+ */
+ Object getPropertyIdByColumnId(String columnId) {
+ return columnKeys.get(columnId);
+ }
+
+ /**
+ * Returns whether column reordering is allowed. Default value is
+ * <code>false</code>.
+ *
+ * @since 7.5.0
+ * @return true if reordering is allowed
+ */
+ public boolean isColumnReorderingAllowed() {
+ return getState(false).columnReorderingAllowed;
+ }
+
+ /**
+ * Sets whether or not column reordering is allowed. Default value is
+ * <code>false</code>.
+ *
+ * @since 7.5.0
+ * @param columnReorderingAllowed
+ * specifies whether column reordering is allowed
+ */
+ public void setColumnReorderingAllowed(boolean columnReorderingAllowed) {
+ if (isColumnReorderingAllowed() != columnReorderingAllowed) {
+ getState().columnReorderingAllowed = columnReorderingAllowed;
+ }
+ }
+
+ @Override
+ protected GridState getState() {
+ return (GridState) super.getState();
+ }
+
+ @Override
+ protected GridState getState(boolean markAsDirty) {
+ return (GridState) super.getState(markAsDirty);
+ }
+
+ /**
+ * Creates a new column based on a property id and appends it as the last
+ * column.
+ *
+ * @param datasourcePropertyId
+ * The property id of a property in the datasource
+ */
+ private Column appendColumn(Object datasourcePropertyId) {
+ if (datasourcePropertyId == null) {
+ throw new IllegalArgumentException("Property id cannot be null");
+ }
+ assert datasource.getContainerPropertyIds().contains(
+ datasourcePropertyId) : "Datasource should contain the property id";
+
+ GridColumnState columnState = new GridColumnState();
+ columnState.id = columnKeys.key(datasourcePropertyId);
+
+ Column column = new Column(this, columnState, datasourcePropertyId);
+ columns.put(datasourcePropertyId, column);
+
+ getState().columns.add(columnState);
+ getState().columnOrder.add(columnState.id);
+ header.addColumn(datasourcePropertyId);
+ footer.addColumn(datasourcePropertyId);
+
+ String humanFriendlyPropertyId = SharedUtil.propertyIdToHumanFriendly(
+ String.valueOf(datasourcePropertyId));
+ column.setHeaderCaption(humanFriendlyPropertyId);
+
+ if (datasource instanceof Sortable
+ && ((Sortable) datasource).getSortableContainerPropertyIds()
+ .contains(datasourcePropertyId)) {
+ column.setSortable(true);
+ }
+
+ return column;
+ }
+
+ /**
+ * Removes a column from Grid based on a property id.
+ *
+ * @param propertyId
+ * The property id of column to be removed
+ *
+ * @throws IllegalArgumentException
+ * if there is no column for given property id in this grid
+ */
+ public void removeColumn(Object propertyId)
+ throws IllegalArgumentException {
+ if (!columns.keySet().contains(propertyId)) {
+ throw new IllegalArgumentException(
+ "There is no column for given property id " + propertyId);
+ }
+
+ List<Column> removed = new ArrayList<>();
+ removed.add(getColumn(propertyId));
+ internalRemoveColumn(propertyId);
+ datasourceExtension.columnsRemoved(removed);
+ }
+
+ private void internalRemoveColumn(Object propertyId) {
+ setEditorField(propertyId, null);
+ header.removeColumn(propertyId);
+ footer.removeColumn(propertyId);
+ Column column = columns.remove(propertyId);
+ getState().columnOrder.remove(columnKeys.key(propertyId));
+ getState().columns.remove(column.getState());
+ removeExtension(column.getRenderer());
+ }
+
+ /**
+ * Sets the columns and their order for the grid. Current columns whose
+ * property id is not in propertyIds are removed. Similarly, a column is
+ * added for any property id in propertyIds that has no corresponding column
+ * in this Grid.
+ *
+ * @since 7.5.0
+ *
+ * @param propertyIds
+ * properties in the desired column order
+ */
+ public void setColumns(Object... propertyIds) {
+ Set<?> removePids = new HashSet<>(columns.keySet());
+ removePids.removeAll(Arrays.asList(propertyIds));
+ for (Object removePid : removePids) {
+ removeColumn(removePid);
+ }
+ Set<?> addPids = new HashSet<>(Arrays.asList(propertyIds));
+ addPids.removeAll(columns.keySet());
+ for (Object propertyId : addPids) {
+ addColumn(propertyId);
+ }
+ setColumnOrder(propertyIds);
+ }
+
+ /**
+ * Sets a new column order for the grid. All columns which are not ordered
+ * here will remain in the order they were before as the last columns of
+ * grid.
+ *
+ * @param propertyIds
+ * properties in the order columns should be
+ */
+ public void setColumnOrder(Object... propertyIds) {
+ List<String> columnOrder = new ArrayList<>();
+ for (Object propertyId : propertyIds) {
+ if (columns.containsKey(propertyId)) {
+ columnOrder.add(columnKeys.key(propertyId));
+ } else {
+ throw new IllegalArgumentException(
+ "Grid does not contain column for property "
+ + String.valueOf(propertyId));
+ }
+ }
+
+ List<String> stateColumnOrder = getState().columnOrder;
+ if (stateColumnOrder.size() != columnOrder.size()) {
+ stateColumnOrder.removeAll(columnOrder);
+ columnOrder.addAll(stateColumnOrder);
+ }
+ getState().columnOrder = columnOrder;
+ fireColumnReorderEvent(false);
+ }
+
+ /**
+ * Sets the number of frozen columns in this grid. Setting the count to 0
+ * means that no data columns will be frozen, but the built-in selection
+ * checkbox column will still be frozen if it's in use. Setting the count to
+ * -1 will also disable the selection column.
+ * <p>
+ * The default value is 0.
+ *
+ * @param numberOfColumns
+ * the number of columns that should be frozen
+ *
+ * @throws IllegalArgumentException
+ * if the column count is < 0 or > the number of visible columns
+ */
+ public void setFrozenColumnCount(int numberOfColumns) {
+ if (numberOfColumns < -1 || numberOfColumns > columns.size()) {
+ throw new IllegalArgumentException(
+ "count must be between -1 and the current number of columns ("
+ + columns.size() + "): " + numberOfColumns);
+ }
+
+ getState().frozenColumnCount = numberOfColumns;
+ }
+
+ /**
+ * Gets the number of frozen columns in this grid. 0 means that no data
+ * columns will be frozen, but the built-in selection checkbox column will
+ * still be frozen if it's in use. -1 means that not even the selection
+ * column is frozen.
+ * <p>
+ * <em>NOTE:</em> this count includes {@link Column#isHidden() hidden
+ * columns} in the count.
+ *
+ * @see #setFrozenColumnCount(int)
+ *
+ * @return the number of frozen columns
+ */
+ public int getFrozenColumnCount() {
+ return getState(false).frozenColumnCount;
+ }
+
+ /**
+ * Scrolls to a certain item, using {@link ScrollDestination#ANY}.
+ * <p>
+ * If the item has visible details, its size will also be taken into
+ * account.
+ *
+ * @param itemId
+ * id of item to scroll to.
+ * @throws IllegalArgumentException
+ * if the provided id is not recognized by the data source.
+ */
+ public void scrollTo(Object itemId) throws IllegalArgumentException {
+ scrollTo(itemId, ScrollDestination.ANY);
+ }
+
+ /**
+ * Scrolls to a certain item, using user-specified scroll destination.
+ * <p>
+ * If the item has visible details, its size will also be taken into
+ * account.
+ *
+ * @param itemId
+ * id of item to scroll to.
+ * @param destination
+ * value specifying desired position of scrolled-to row.
+ * @throws IllegalArgumentException
+ * if the provided id is not recognized by the data source.
+ */
+ public void scrollTo(Object itemId, ScrollDestination destination)
+ throws IllegalArgumentException {
+
+ int row = datasource.indexOfId(itemId);
+
+ if (row == -1) {
+ throw new IllegalArgumentException(
+ "Item with specified ID does not exist in data source");
+ }
+
+ GridClientRpc clientRPC = getRpcProxy(GridClientRpc.class);
+ clientRPC.scrollToRow(row, destination);
+ }
+
+ /**
+ * Scrolls to the beginning of the first data row.
+ */
+ public void scrollToStart() {
+ GridClientRpc clientRPC = getRpcProxy(GridClientRpc.class);
+ clientRPC.scrollToStart();
+ }
+
+ /**
+ * Scrolls to the end of the last data row.
+ */
+ public void scrollToEnd() {
+ GridClientRpc clientRPC = getRpcProxy(GridClientRpc.class);
+ clientRPC.scrollToEnd();
+ }
+
+ /**
+ * Sets the number of rows that should be visible in Grid's body, while
+ * {@link #getHeightMode()} is {@link HeightMode#ROW}.
+ * <p>
+ * If Grid is currently not in {@link HeightMode#ROW}, the given value is
+ * remembered, and applied once the mode is applied.
+ *
+ * @param rows
+ * The height in terms of number of rows displayed in Grid's
+ * body. If Grid doesn't contain enough rows, white space is
+ * displayed instead. If <code>null</code> is given, then Grid's
+ * height is undefined
+ * @throws IllegalArgumentException
+ * if {@code rows} is zero or less
+ * @throws IllegalArgumentException
+ * if {@code rows} is {@link Double#isInfinite(double) infinite}
+ * @throws IllegalArgumentException
+ * if {@code rows} is {@link Double#isNaN(double) NaN}
+ */
+ public void setHeightByRows(double rows) {
+ if (rows <= 0.0d) {
+ throw new IllegalArgumentException(
+ "More than zero rows must be shown.");
+ } else if (Double.isInfinite(rows)) {
+ throw new IllegalArgumentException(
+ "Grid doesn't support infinite heights");
+ } else if (Double.isNaN(rows)) {
+ throw new IllegalArgumentException("NaN is not a valid row count");
+ }
+
+ getState().heightByRows = rows;
+ }
+
+ /**
+ * Gets the amount of rows in Grid's body that are shown, while
+ * {@link #getHeightMode()} is {@link HeightMode#ROW}.
+ *
+ * @return the amount of rows that are being shown in Grid's body
+ * @see #setHeightByRows(double)
+ */
+ public double getHeightByRows() {
+ return getState(false).heightByRows;
+ }
+
+ /**
+ * {@inheritDoc}
+ * <p>
+ * <em>Note:</em> This method will change the widget's size in the browser
+ * only if {@link #getHeightMode()} returns {@link HeightMode#CSS}.
+ *
+ * @see #setHeightMode(HeightMode)
+ */
+ @Override
+ public void setHeight(float height, Unit unit) {
+ super.setHeight(height, unit);
+ }
+
+ /**
+ * Defines the mode in which the Grid widget's height is calculated.
+ * <p>
+ * If {@link HeightMode#CSS} is given, Grid will respect the values given
+ * via a {@code setHeight}-method, and behave as a traditional Component.
+ * <p>
+ * If {@link HeightMode#ROW} is given, Grid will make sure that the body
+ * will display as many rows as {@link #getHeightByRows()} defines.
+ * <em>Note:</em> If headers/footers are inserted or removed, the widget
+ * will resize itself to still display the required amount of rows in its
+ * body. It also takes the horizontal scrollbar into account.
+ *
+ * @param heightMode
+ * the mode in to which Grid should be set
+ */
+ public void setHeightMode(HeightMode heightMode) {
+ /*
+ * This method is a workaround for the fact that Vaadin re-applies
+ * widget dimensions (height/width) on each state change event. The
+ * original design was to have setHeight and setHeightByRow be equals,
+ * and whichever was called the latest was considered in effect.
+ *
+ * But, because of Vaadin always calling setHeight on the widget, this
+ * approach doesn't work.
+ */
+
+ getState().heightMode = heightMode;
+ }
+
+ /**
+ * Returns the current {@link HeightMode} the Grid is in.
+ * <p>
+ * Defaults to {@link HeightMode#CSS}.
+ *
+ * @return the current HeightMode
+ */
+ public HeightMode getHeightMode() {
+ return getState(false).heightMode;
+ }
+
+ /* Selection related methods: */
+
+ /**
+ * Takes a new {@link SelectionModel} into use.
+ * <p>
+ * The SelectionModel that is previously in use will have all its items
+ * deselected.
+ * <p>
+ * If the given SelectionModel is already in use, this method does nothing.
+ *
+ * @param selectionModel
+ * the new SelectionModel to use
+ * @throws IllegalArgumentException
+ * if {@code selectionModel} is <code>null</code>
+ */
+ public void setSelectionModel(SelectionModel selectionModel)
+ throws IllegalArgumentException {
+ if (selectionModel == null) {
+ throw new IllegalArgumentException(
+ "Selection model may not be null");
+ }
+
+ if (this.selectionModel != selectionModel) {
+ // this.selectionModel is null on init
+ if (this.selectionModel != null) {
+ this.selectionModel.remove();
+ }
+
+ this.selectionModel = selectionModel;
+ selectionModel.setGrid(this);
+ }
+ }
+
+ /**
+ * Returns the currently used {@link SelectionModel}.
+ *
+ * @return the currently used SelectionModel
+ */
+ public SelectionModel getSelectionModel() {
+ return selectionModel;
+ }
+
+ /**
+ * Sets the Grid's selection mode.
+ * <p>
+ * Grid supports three selection modes: multiselect, single select and no
+ * selection, and this is a convenience method for choosing between one of
+ * them.
+ * <p>
+ * Technically, this method is a shortcut that can be used instead of
+ * calling {@code setSelectionModel} with a specific SelectionModel
+ * instance. Grid comes with three built-in SelectionModel classes, and the
+ * {@link SelectionMode} enum represents each of them.
+ * <p>
+ * Essentially, the two following method calls are equivalent:
+ * <p>
+ * <code><pre>
+ * grid.setSelectionMode(SelectionMode.MULTI);
+ * grid.setSelectionModel(new MultiSelectionMode());
+ * </pre></code>
+ *
+ *
+ * @param selectionMode
+ * the selection mode to switch to
+ * @return The {@link SelectionModel} instance that was taken into use
+ * @throws IllegalArgumentException
+ * if {@code selectionMode} is <code>null</code>
+ * @see SelectionModel
+ */
+ public SelectionModel setSelectionMode(final SelectionMode selectionMode)
+ throws IllegalArgumentException {
+ if (selectionMode == null) {
+ throw new IllegalArgumentException(
+ "selection mode may not be null");
+ }
+ final SelectionModel newSelectionModel = selectionMode.createModel();
+ setSelectionModel(newSelectionModel);
+ return newSelectionModel;
+ }
+
+ /**
+ * Checks whether an item is selected or not.
+ *
+ * @param itemId
+ * the item id to check for
+ * @return <code>true</code> iff the item is selected
+ */
+ // keep this javadoc in sync with SelectionModel.isSelected
+ public boolean isSelected(Object itemId) {
+ return selectionModel.isSelected(itemId);
+ }
+
+ /**
+ * Returns a collection of all the currently selected itemIds.
+ * <p>
+ * This method is a shorthand that delegates to the
+ * {@link #getSelectionModel() selection model}.
+ *
+ * @return a collection of all the currently selected itemIds
+ */
+ // keep this javadoc in sync with SelectionModel.getSelectedRows
+ public Collection<Object> getSelectedRows() {
+ return getSelectionModel().getSelectedRows();
+ }
+
+ /**
+ * Gets the item id of the currently selected item.
+ * <p>
+ * This method is a shorthand that delegates to the
+ * {@link #getSelectionModel() selection model}. Only
+ * {@link SelectionModel.Single} is supported.
+ *
+ * @return the item id of the currently selected item, or <code>null</code>
+ * if nothing is selected
+ * @throws IllegalStateException
+ * if the selection model does not implement
+ * {@code SelectionModel.Single}
+ */
+ // keep this javadoc in sync with SelectionModel.Single.getSelectedRow
+ public Object getSelectedRow() throws IllegalStateException {
+ if (selectionModel instanceof SelectionModel.Single) {
+ return ((SelectionModel.Single) selectionModel).getSelectedRow();
+ } else if (selectionModel instanceof SelectionModel.Multi) {
+ throw new IllegalStateException("Cannot get unique selected row: "
+ + "Grid is in multiselect mode "
+ + "(the current selection model is "
+ + selectionModel.getClass().getName() + ").");
+ } else if (selectionModel instanceof SelectionModel.None) {
+ throw new IllegalStateException(
+ "Cannot get selected row: " + "Grid selection is disabled "
+ + "(the current selection model is "
+ + selectionModel.getClass().getName() + ").");
+ } else {
+ throw new IllegalStateException("Cannot get selected row: "
+ + "Grid selection model does not implement "
+ + SelectionModel.Single.class.getName() + " or "
+ + SelectionModel.Multi.class.getName()
+ + "(the current model is "
+ + selectionModel.getClass().getName() + ").");
+ }
+ }
+
+ /**
+ * Marks an item as selected.
+ * <p>
+ * This method is a shorthand that delegates to the
+ * {@link #getSelectionModel() selection model}. Only
+ * {@link SelectionModel.Single} and {@link SelectionModel.Multi} are
+ * supported.
+ *
+ * @param itemId
+ * the itemId to mark as selected
+ * @return <code>true</code> if the selection state changed,
+ * <code>false</code> if the itemId already was selected
+ * @throws IllegalArgumentException
+ * if the {@code itemId} doesn't exist in the currently active
+ * Container
+ * @throws IllegalStateException
+ * if the selection was illegal. One such reason might be that
+ * the implementation already had an item selected, and that
+ * needs to be explicitly deselected before re-selecting
+ * something.
+ * @throws IllegalStateException
+ * if the selection model does not implement
+ * {@code SelectionModel.Single} or {@code SelectionModel.Multi}
+ */
+ // keep this javadoc in sync with SelectionModel.Single.select
+ public boolean select(Object itemId)
+ throws IllegalArgumentException, IllegalStateException {
+ if (selectionModel instanceof SelectionModel.Single) {
+ return ((SelectionModel.Single) selectionModel).select(itemId);
+ } else if (selectionModel instanceof SelectionModel.Multi) {
+ return ((SelectionModel.Multi) selectionModel).select(itemId);
+ } else if (selectionModel instanceof SelectionModel.None) {
+ throw new IllegalStateException("Cannot select row '" + itemId
+ + "': Grid selection is disabled "
+ + "(the current selection model is "
+ + selectionModel.getClass().getName() + ").");
+ } else {
+ throw new IllegalStateException("Cannot select row '" + itemId
+ + "': Grid selection model does not implement "
+ + SelectionModel.Single.class.getName() + " or "
+ + SelectionModel.Multi.class.getName()
+ + "(the current model is "
+ + selectionModel.getClass().getName() + ").");
+ }
+ }
+
+ /**
+ * Marks an item as unselected.
+ * <p>
+ * This method is a shorthand that delegates to the
+ * {@link #getSelectionModel() selection model}. Only
+ * {@link SelectionModel.Single} and {@link SelectionModel.Multi} are
+ * supported.
+ *
+ * @param itemId
+ * the itemId to remove from being selected
+ * @return <code>true</code> if the selection state changed,
+ * <code>false</code> if the itemId was already selected
+ * @throws IllegalArgumentException
+ * if the {@code itemId} doesn't exist in the currently active
+ * Container
+ * @throws IllegalStateException
+ * if the deselection was illegal. One such reason might be that
+ * the implementation requires one or more items to be selected
+ * at all times.
+ * @throws IllegalStateException
+ * if the selection model does not implement
+ * {@code SelectionModel.Single} or {code SelectionModel.Multi}
+ */
+ // keep this javadoc in sync with SelectionModel.Single.deselect
+ public boolean deselect(Object itemId) throws IllegalStateException {
+ if (selectionModel instanceof SelectionModel.Single) {
+ if (isSelected(itemId)) {
+ return ((SelectionModel.Single) selectionModel).select(null);
+ }
+ return false;
+ } else if (selectionModel instanceof SelectionModel.Multi) {
+ return ((SelectionModel.Multi) selectionModel).deselect(itemId);
+ } else if (selectionModel instanceof SelectionModel.None) {
+ throw new IllegalStateException("Cannot deselect row '" + itemId
+ + "': Grid selection is disabled "
+ + "(the current selection model is "
+ + selectionModel.getClass().getName() + ").");
+ } else {
+ throw new IllegalStateException("Cannot deselect row '" + itemId
+ + "': Grid selection model does not implement "
+ + SelectionModel.Single.class.getName() + " or "
+ + SelectionModel.Multi.class.getName()
+ + "(the current model is "
+ + selectionModel.getClass().getName() + ").");
+ }
+ }
+
+ /**
+ * Marks all items as unselected.
+ * <p>
+ * This method is a shorthand that delegates to the
+ * {@link #getSelectionModel() selection model}. Only
+ * {@link SelectionModel.Single} and {@link SelectionModel.Multi} are
+ * supported.
+ *
+ * @return <code>true</code> if the selection state changed,
+ * <code>false</code> if the itemId was already selected
+ * @throws IllegalStateException
+ * if the deselection was illegal. One such reason might be that
+ * the implementation requires one or more items to be selected
+ * at all times.
+ * @throws IllegalStateException
+ * if the selection model does not implement
+ * {@code SelectionModel.Single} or {code SelectionModel.Multi}
+ */
+ public boolean deselectAll() throws IllegalStateException {
+ if (selectionModel instanceof SelectionModel.Single) {
+ if (getSelectedRow() != null) {
+ return deselect(getSelectedRow());
+ }
+ return false;
+ } else if (selectionModel instanceof SelectionModel.Multi) {
+ return ((SelectionModel.Multi) selectionModel).deselectAll();
+ } else if (selectionModel instanceof SelectionModel.None) {
+ throw new IllegalStateException(
+ "Cannot deselect all rows" + ": Grid selection is disabled "
+ + "(the current selection model is "
+ + selectionModel.getClass().getName() + ").");
+ } else {
+ throw new IllegalStateException("Cannot deselect all rows:"
+ + " Grid selection model does not implement "
+ + SelectionModel.Single.class.getName() + " or "
+ + SelectionModel.Multi.class.getName()
+ + "(the current model is "
+ + selectionModel.getClass().getName() + ").");
+ }
+ }
+
+ /**
+ * Fires a selection change event.
+ * <p>
+ * <strong>Note:</strong> This is not a method that should be called by
+ * application logic. This method is publicly accessible only so that
+ * {@link SelectionModel SelectionModels} would be able to inform Grid of
+ * these events.
+ *
+ * @param newSelection
+ * the selection that was added by this event
+ * @param oldSelection
+ * the selection that was removed by this event
+ */
+ public void fireSelectionEvent(Collection<Object> oldSelection,
+ Collection<Object> newSelection) {
+ fireEvent(new SelectionEvent(this, oldSelection, newSelection));
+ }
+
+ @Override
+ public void addSelectionListener(SelectionListener listener) {
+ addListener(SelectionEvent.class, listener, SELECTION_CHANGE_METHOD);
+ }
+
+ @Override
+ public void removeSelectionListener(SelectionListener listener) {
+ removeListener(SelectionEvent.class, listener, SELECTION_CHANGE_METHOD);
+ }
+
+ private void fireColumnReorderEvent(boolean userOriginated) {
+ fireEvent(new ColumnReorderEvent(this, userOriginated));
+ }
+
+ /**
+ * Registers a new column reorder listener.
+ *
+ * @since 7.5.0
+ * @param listener
+ * the listener to register
+ */
+ public void addColumnReorderListener(ColumnReorderListener listener) {
+ addListener(ColumnReorderEvent.class, listener, COLUMN_REORDER_METHOD);
+ }
+
+ /**
+ * Removes a previously registered column reorder listener.
+ *
+ * @since 7.5.0
+ * @param listener
+ * the listener to remove
+ */
+ public void removeColumnReorderListener(ColumnReorderListener listener) {
+ removeListener(ColumnReorderEvent.class, listener,
+ COLUMN_REORDER_METHOD);
+ }
+
+ private void fireColumnResizeEvent(Column column, boolean userOriginated) {
+ fireEvent(new ColumnResizeEvent(this, column, userOriginated));
+ }
+
+ /**
+ * Registers a new column resize listener.
+ *
+ * @param listener
+ * the listener to register
+ */
+ public void addColumnResizeListener(ColumnResizeListener listener) {
+ addListener(ColumnResizeEvent.class, listener, COLUMN_RESIZE_METHOD);
+ }
+
+ /**
+ * Removes a previously registered column resize listener.
+ *
+ * @param listener
+ * the listener to remove
+ */
+ public void removeColumnResizeListener(ColumnResizeListener listener) {
+ removeListener(ColumnResizeEvent.class, listener, COLUMN_RESIZE_METHOD);
+ }
+
+ /**
+ * Gets the {@link KeyMapper } being used by the data source.
+ *
+ * @return the key mapper being used by the data source
+ */
+ KeyMapper<Object> getKeyMapper() {
+ return datasourceExtension.getKeyMapper();
+ }
+
+ /**
+ * Adds a renderer to this grid's connector hierarchy.
+ *
+ * @param renderer
+ * the renderer to add
+ */
+ void addRenderer(Renderer<?> renderer) {
+ addExtension(renderer);
+ }
+
+ /**
+ * Sets the current sort order using the fluid Sort API. Read the
+ * documentation for {@link Sort} for more information.
+ * <p>
+ * <em>Note:</em> Sorting by a property that has no column in Grid will hide
+ * all possible sorting indicators.
+ *
+ * @param s
+ * a sort instance
+ *
+ * @throws IllegalStateException
+ * if container is not sortable (does not implement
+ * Container.Sortable)
+ * @throws IllegalArgumentException
+ * if trying to sort by non-existing property
+ */
+ public void sort(Sort s) {
+ setSortOrder(s.build());
+ }
+
+ /**
+ * Sort this Grid in ascending order by a specified property.
+ * <p>
+ * <em>Note:</em> Sorting by a property that has no column in Grid will hide
+ * all possible sorting indicators.
+ *
+ * @param propertyId
+ * a property ID
+ *
+ * @throws IllegalStateException
+ * if container is not sortable (does not implement
+ * Container.Sortable)
+ * @throws IllegalArgumentException
+ * if trying to sort by non-existing property
+ */
+ public void sort(Object propertyId) {
+ sort(propertyId, SortDirection.ASCENDING);
+ }
+
+ /**
+ * Sort this Grid in user-specified {@link SortOrder} by a property.
+ * <p>
+ * <em>Note:</em> Sorting by a property that has no column in Grid will hide
+ * all possible sorting indicators.
+ *
+ * @param propertyId
+ * a property ID
+ * @param direction
+ * a sort order value (ascending/descending)
+ *
+ * @throws IllegalStateException
+ * if container is not sortable (does not implement
+ * Container.Sortable)
+ * @throws IllegalArgumentException
+ * if trying to sort by non-existing property
+ */
+ public void sort(Object propertyId, SortDirection direction) {
+ sort(Sort.by(propertyId, direction));
+ }
+
+ /**
+ * Clear the current sort order, and re-sort the grid.
+ */
+ public void clearSortOrder() {
+ sortOrder.clear();
+ sort(false);
+ }
+
+ /**
+ * Sets the sort order to use.
+ * <p>
+ * <em>Note:</em> Sorting by a property that has no column in Grid will hide
+ * all possible sorting indicators.
+ *
+ * @param order
+ * a sort order list.
+ *
+ * @throws IllegalStateException
+ * if container is not sortable (does not implement
+ * Container.Sortable)
+ * @throws IllegalArgumentException
+ * if order is null or trying to sort by non-existing property
+ */
+ public void setSortOrder(List<SortOrder> order) {
+ setSortOrder(order, false);
+ }
+
+ private void setSortOrder(List<SortOrder> order, boolean userOriginated)
+ throws IllegalStateException, IllegalArgumentException {
+ if (!(getContainerDataSource() instanceof Container.Sortable)) {
+ throw new IllegalStateException(
+ "Attached container is not sortable (does not implement Container.Sortable)");
+ }
+
+ if (order == null) {
+ throw new IllegalArgumentException("Order list may not be null!");
+ }
+
+ sortOrder.clear();
+
+ Collection<?> sortableProps = ((Container.Sortable) getContainerDataSource())
+ .getSortableContainerPropertyIds();
+
+ for (SortOrder o : order) {
+ if (!sortableProps.contains(o.getPropertyId())) {
+ throw new IllegalArgumentException("Property "
+ + o.getPropertyId()
+ + " does not exist or is not sortable in the current container");
+ }
+ }
+
+ sortOrder.addAll(order);
+ sort(userOriginated);
+ }
+
+ /**
+ * Get the current sort order list.
+ *
+ * @return a sort order list
+ */
+ public List<SortOrder> getSortOrder() {
+ return Collections.unmodifiableList(sortOrder);
+ }
+
+ /**
+ * Apply sorting to data source.
+ */
+ private void sort(boolean userOriginated) {
+
+ Container c = getContainerDataSource();
+ if (c instanceof Container.Sortable) {
+ Container.Sortable cs = (Container.Sortable) c;
+
+ final int items = sortOrder.size();
+ Object[] propertyIds = new Object[items];
+ boolean[] directions = new boolean[items];
+
+ SortDirection[] stateDirs = new SortDirection[items];
+
+ for (int i = 0; i < items; ++i) {
+ SortOrder order = sortOrder.get(i);
+
+ stateDirs[i] = order.getDirection();
+ propertyIds[i] = order.getPropertyId();
+ switch (order.getDirection()) {
+ case ASCENDING:
+ directions[i] = true;
+ break;
+ case DESCENDING:
+ directions[i] = false;
+ break;
+ default:
+ throw new IllegalArgumentException("getDirection() of "
+ + order + " returned an unexpected value");
+ }
+ }
+
+ cs.sort(propertyIds, directions);
+
+ if (columns.keySet().containsAll(Arrays.asList(propertyIds))) {
+ String[] columnKeys = new String[items];
+ for (int i = 0; i < items; ++i) {
+ columnKeys[i] = this.columnKeys.key(propertyIds[i]);
+ }
+ getState().sortColumns = columnKeys;
+ getState(false).sortDirs = stateDirs;
+ } else {
+ // Not all sorted properties are in Grid. Remove any indicators.
+ getState().sortColumns = new String[] {};
+ getState(false).sortDirs = new SortDirection[] {};
+ }
+ fireEvent(new SortEvent(this, new ArrayList<>(sortOrder),
+ userOriginated));
+ } else {
+ throw new IllegalStateException(
+ "Container is not sortable (does not implement Container.Sortable)");
+ }
+ }
+
+ /**
+ * Adds a sort order change listener that gets notified when the sort order
+ * changes.
+ *
+ * @param listener
+ * the sort order change listener to add
+ */
+ @Override
+ public void addSortListener(SortListener listener) {
+ addListener(SortEvent.class, listener, SORT_ORDER_CHANGE_METHOD);
+ }
+
+ /**
+ * Removes a sort order change listener previously added using
+ * {@link #addSortListener(SortListener)}.
+ *
+ * @param listener
+ * the sort order change listener to remove
+ */
+ @Override
+ public void removeSortListener(SortListener listener) {
+ removeListener(SortEvent.class, listener, SORT_ORDER_CHANGE_METHOD);
+ }
+
+ /* Grid Headers */
+
+ /**
+ * Returns the header section of this grid. The default header contains a
+ * single row displaying the column captions.
+ *
+ * @return the header
+ */
+ protected Header getHeader() {
+ return header;
+ }
+
+ /**
+ * Gets the header row at given index.
+ *
+ * @param rowIndex
+ * 0 based index for row. Counted from top to bottom
+ * @return header row at given index
+ * @throws IllegalArgumentException
+ * if no row exists at given index
+ */
+ public HeaderRow getHeaderRow(int rowIndex) {
+ return header.getRow(rowIndex);
+ }
+
+ /**
+ * Inserts a new row at the given position to the header section. Shifts the
+ * row currently at that position and any subsequent rows down (adds one to
+ * their indices).
+ *
+ * @param index
+ * the position at which to insert the row
+ * @return the new row
+ *
+ * @throws IllegalArgumentException
+ * if the index is less than 0 or greater than row count
+ * @see #appendHeaderRow()
+ * @see #prependHeaderRow()
+ * @see #removeHeaderRow(HeaderRow)
+ * @see #removeHeaderRow(int)
+ */
+ public HeaderRow addHeaderRowAt(int index) {
+ return header.addRowAt(index);
+ }
+
+ /**
+ * Adds a new row at the bottom of the header section.
+ *
+ * @return the new row
+ * @see #prependHeaderRow()
+ * @see #addHeaderRowAt(int)
+ * @see #removeHeaderRow(HeaderRow)
+ * @see #removeHeaderRow(int)
+ */
+ public HeaderRow appendHeaderRow() {
+ return header.appendRow();
+ }
+
+ /**
+ * Returns the current default row of the header section. The default row is
+ * a special header row providing a user interface for sorting columns.
+ * Setting a header text for column updates cells in the default header.
+ *
+ * @return the default row or null if no default row set
+ */
+ public HeaderRow getDefaultHeaderRow() {
+ return header.getDefaultRow();
+ }
+
+ /**
+ * Gets the row count for the header section.
+ *
+ * @return row count
+ */
+ public int getHeaderRowCount() {
+ return header.getRowCount();
+ }
+
+ /**
+ * Adds a new row at the top of the header section.
+ *
+ * @return the new row
+ * @see #appendHeaderRow()
+ * @see #addHeaderRowAt(int)
+ * @see #removeHeaderRow(HeaderRow)
+ * @see #removeHeaderRow(int)
+ */
+ public HeaderRow prependHeaderRow() {
+ return header.prependRow();
+ }
+
+ /**
+ * Removes the given row from the header section.
+ *
+ * @param row
+ * the row to be removed
+ *
+ * @throws IllegalArgumentException
+ * if the row does not exist in this section
+ * @see #removeHeaderRow(int)
+ * @see #addHeaderRowAt(int)
+ * @see #appendHeaderRow()
+ * @see #prependHeaderRow()
+ */
+ public void removeHeaderRow(HeaderRow row) {
+ header.removeRow(row);
+ }
+
+ /**
+ * Removes the row at the given position from the header section.
+ *
+ * @param rowIndex
+ * the position of the row
+ *
+ * @throws IllegalArgumentException
+ * if no row exists at given index
+ * @see #removeHeaderRow(HeaderRow)
+ * @see #addHeaderRowAt(int)
+ * @see #appendHeaderRow()
+ * @see #prependHeaderRow()
+ */
+ public void removeHeaderRow(int rowIndex) {
+ header.removeRow(rowIndex);
+ }
+
+ /**
+ * Sets the default row of the header. The default row is a special header
+ * row providing a user interface for sorting columns.
+ *
+ * @param row
+ * the new default row, or null for no default row
+ *
+ * @throws IllegalArgumentException
+ * header does not contain the row
+ */
+ public void setDefaultHeaderRow(HeaderRow row) {
+ header.setDefaultRow(row);
+ }
+
+ /**
+ * Sets the visibility of the header section.
+ *
+ * @param visible
+ * true to show header section, false to hide
+ */
+ public void setHeaderVisible(boolean visible) {
+ header.setVisible(visible);
+ }
+
+ /**
+ * Returns the visibility of the header section.
+ *
+ * @return true if visible, false otherwise.
+ */
+ public boolean isHeaderVisible() {
+ return header.isVisible();
+ }
+
+ /* Grid Footers */
+
+ /**
+ * Returns the footer section of this grid. The default header contains a
+ * single row displaying the column captions.
+ *
+ * @return the footer
+ */
+ protected Footer getFooter() {
+ return footer;
+ }
+
+ /**
+ * Gets the footer row at given index.
+ *
+ * @param rowIndex
+ * 0 based index for row. Counted from top to bottom
+ * @return footer row at given index
+ * @throws IllegalArgumentException
+ * if no row exists at given index
+ */
+ public FooterRow getFooterRow(int rowIndex) {
+ return footer.getRow(rowIndex);
+ }
+
+ /**
+ * Inserts a new row at the given position to the footer section. Shifts the
+ * row currently at that position and any subsequent rows down (adds one to
+ * their indices).
+ *
+ * @param index
+ * the position at which to insert the row
+ * @return the new row
+ *
+ * @throws IllegalArgumentException
+ * if the index is less than 0 or greater than row count
+ * @see #appendFooterRow()
+ * @see #prependFooterRow()
+ * @see #removeFooterRow(FooterRow)
+ * @see #removeFooterRow(int)
+ */
+ public FooterRow addFooterRowAt(int index) {
+ return footer.addRowAt(index);
+ }
+
+ /**
+ * Adds a new row at the bottom of the footer section.
+ *
+ * @return the new row
+ * @see #prependFooterRow()
+ * @see #addFooterRowAt(int)
+ * @see #removeFooterRow(FooterRow)
+ * @see #removeFooterRow(int)
+ */
+ public FooterRow appendFooterRow() {
+ return footer.appendRow();
+ }
+
+ /**
+ * Gets the row count for the footer.
+ *
+ * @return row count
+ */
+ public int getFooterRowCount() {
+ return footer.getRowCount();
+ }
+
+ /**
+ * Adds a new row at the top of the footer section.
+ *
+ * @return the new row
+ * @see #appendFooterRow()
+ * @see #addFooterRowAt(int)
+ * @see #removeFooterRow(FooterRow)
+ * @see #removeFooterRow(int)
+ */
+ public FooterRow prependFooterRow() {
+ return footer.prependRow();
+ }
+
+ /**
+ * Removes the given row from the footer section.
+ *
+ * @param row
+ * the row to be removed
+ *
+ * @throws IllegalArgumentException
+ * if the row does not exist in this section
+ * @see #removeFooterRow(int)
+ * @see #addFooterRowAt(int)
+ * @see #appendFooterRow()
+ * @see #prependFooterRow()
+ */
+ public void removeFooterRow(FooterRow row) {
+ footer.removeRow(row);
+ }
+
+ /**
+ * Removes the row at the given position from the footer section.
+ *
+ * @param rowIndex
+ * the position of the row
+ *
+ * @throws IllegalArgumentException
+ * if no row exists at given index
+ * @see #removeFooterRow(FooterRow)
+ * @see #addFooterRowAt(int)
+ * @see #appendFooterRow()
+ * @see #prependFooterRow()
+ */
+ public void removeFooterRow(int rowIndex) {
+ footer.removeRow(rowIndex);
+ }
+
+ /**
+ * Sets the visibility of the footer section.
+ *
+ * @param visible
+ * true to show footer section, false to hide
+ */
+ public void setFooterVisible(boolean visible) {
+ footer.setVisible(visible);
+ }
+
+ /**
+ * Returns the visibility of the footer section.
+ *
+ * @return true if visible, false otherwise.
+ */
+ public boolean isFooterVisible() {
+ return footer.isVisible();
+ }
+
+ private void addComponent(Component c) {
+ extensionComponents.add(c);
+ c.setParent(this);
+ markAsDirty();
+ }
+
+ private void removeComponent(Component c) {
+ extensionComponents.remove(c);
+ c.setParent(null);
+ markAsDirty();
+ }
+
+ @Override
+ public Iterator<Component> iterator() {
+ // This is a hash set to avoid adding header/footer components inside
+ // merged cells multiple times
+ LinkedHashSet<Component> componentList = new LinkedHashSet<>();
+
+ Header header = getHeader();
+ for (int i = 0; i < header.getRowCount(); ++i) {
+ HeaderRow row = header.getRow(i);
+ for (Object propId : columns.keySet()) {
+ HeaderCell cell = row.getCell(propId);
+ if (cell.getCellState().type == GridStaticCellType.WIDGET) {
+ componentList.add(cell.getComponent());
+ }
+ }
+ }
+
+ Footer footer = getFooter();
+ for (int i = 0; i < footer.getRowCount(); ++i) {
+ FooterRow row = footer.getRow(i);
+ for (Object propId : columns.keySet()) {
+ FooterCell cell = row.getCell(propId);
+ if (cell.getCellState().type == GridStaticCellType.WIDGET) {
+ componentList.add(cell.getComponent());
+ }
+ }
+ }
+
+ componentList.addAll(getEditorFields());
+
+ componentList.addAll(extensionComponents);
+
+ return componentList.iterator();
+ }
+
+ @Override
+ public boolean isRendered(Component childComponent) {
+ if (getEditorFields().contains(childComponent)) {
+ // Only render editor fields if the editor is open
+ return isEditorActive();
+ } else {
+ // TODO Header and footer components should also only be rendered if
+ // the header/footer is visible
+ return true;
+ }
+ }
+
+ EditorClientRpc getEditorRpc() {
+ return getRpcProxy(EditorClientRpc.class);
+ }
+
+ /**
+ * Sets the {@code CellDescriptionGenerator} instance for generating
+ * optional descriptions (tooltips) for individual Grid cells. If a
+ * {@link RowDescriptionGenerator} is also set, the row description it
+ * generates is displayed for cells for which {@code generator} returns
+ * null.
+ *
+ * @param generator
+ * the description generator to use or {@code null} to remove a
+ * previously set generator if any
+ *
+ * @see #setRowDescriptionGenerator(RowDescriptionGenerator)
+ *
+ * @since 7.6
+ */
+ public void setCellDescriptionGenerator(
+ CellDescriptionGenerator generator) {
+ cellDescriptionGenerator = generator;
+ getState().hasDescriptions = (generator != null
+ || rowDescriptionGenerator != null);
+ datasourceExtension.refreshCache();
+ }
+
+ /**
+ * Returns the {@code CellDescriptionGenerator} instance used to generate
+ * descriptions (tooltips) for Grid cells.
+ *
+ * @return the description generator or {@code null} if no generator is set
+ *
+ * @since 7.6
+ */
+ public CellDescriptionGenerator getCellDescriptionGenerator() {
+ return cellDescriptionGenerator;
+ }
+
+ /**
+ * Sets the {@code RowDescriptionGenerator} instance for generating optional
+ * descriptions (tooltips) for Grid rows. If a
+ * {@link CellDescriptionGenerator} is also set, the row description
+ * generated by {@code generator} is used for cells for which the cell
+ * description generator returns null.
+ *
+ *
+ * @param generator
+ * the description generator to use or {@code null} to remove a
+ * previously set generator if any
+ *
+ * @see #setCellDescriptionGenerator(CellDescriptionGenerator)
+ *
+ * @since 7.6
+ */
+ public void setRowDescriptionGenerator(RowDescriptionGenerator generator) {
+ rowDescriptionGenerator = generator;
+ getState().hasDescriptions = (generator != null
+ || cellDescriptionGenerator != null);
+ datasourceExtension.refreshCache();
+ }
+
+ /**
+ * Returns the {@code RowDescriptionGenerator} instance used to generate
+ * descriptions (tooltips) for Grid rows
+ *
+ * @return the description generator or {@code} null if no generator is set
+ *
+ * @since 7.6
+ */
+ public RowDescriptionGenerator getRowDescriptionGenerator() {
+ return rowDescriptionGenerator;
+ }
+
+ /**
+ * Sets the style generator that is used for generating styles for cells
+ *
+ * @param cellStyleGenerator
+ * the cell style generator to set, or <code>null</code> to
+ * remove a previously set generator
+ */
+ public void setCellStyleGenerator(CellStyleGenerator cellStyleGenerator) {
+ this.cellStyleGenerator = cellStyleGenerator;
+ datasourceExtension.refreshCache();
+ }
+
+ /**
+ * Gets the style generator that is used for generating styles for cells
+ *
+ * @return the cell style generator, or <code>null</code> if no generator is
+ * set
+ */
+ public CellStyleGenerator getCellStyleGenerator() {
+ return cellStyleGenerator;
+ }
+
+ /**
+ * Sets the style generator that is used for generating styles for rows
+ *
+ * @param rowStyleGenerator
+ * the row style generator to set, or <code>null</code> to remove
+ * a previously set generator
+ */
+ public void setRowStyleGenerator(RowStyleGenerator rowStyleGenerator) {
+ this.rowStyleGenerator = rowStyleGenerator;
+ datasourceExtension.refreshCache();
+ }
+
+ /**
+ * Gets the style generator that is used for generating styles for rows
+ *
+ * @return the row style generator, or <code>null</code> if no generator is
+ * set
+ */
+ public RowStyleGenerator getRowStyleGenerator() {
+ return rowStyleGenerator;
+ }
+
+ /**
+ * Adds a row to the underlying container. The order of the parameters
+ * should match the current visible column order.
+ * <p>
+ * Please note that it's generally only safe to use this method during
+ * initialization. After Grid has been initialized and the visible column
+ * order might have been changed, it's better to instead add items directly
+ * to the underlying container and use {@link Item#getItemProperty(Object)}
+ * to make sure each value is assigned to the intended property.
+ *
+ * @param values
+ * the cell values of the new row, in the same order as the
+ * visible column order, not <code>null</code>.
+ * @return the item id of the new row
+ * @throws IllegalArgumentException
+ * if values is null
+ * @throws IllegalArgumentException
+ * if its length does not match the number of visible columns
+ * @throws IllegalArgumentException
+ * if a parameter value is not an instance of the corresponding
+ * property type
+ * @throws UnsupportedOperationException
+ * if the container does not support adding new items
+ */
+ public Object addRow(Object... values) {
+ if (values == null) {
+ throw new IllegalArgumentException("Values cannot be null");
+ }
+
+ Indexed dataSource = getContainerDataSource();
+ List<String> columnOrder = getState(false).columnOrder;
+
+ if (values.length != columnOrder.size()) {
+ throw new IllegalArgumentException(
+ "There are " + columnOrder.size() + " visible columns, but "
+ + values.length + " cell values were provided.");
+ }
+
+ // First verify all parameter types
+ for (int i = 0; i < columnOrder.size(); i++) {
+ Object propertyId = getPropertyIdByColumnId(columnOrder.get(i));
+
+ Class<?> propertyType = dataSource.getType(propertyId);
+ if (values[i] != null && !propertyType.isInstance(values[i])) {
+ throw new IllegalArgumentException("Parameter " + i + "("
+ + values[i] + ") is not an instance of "
+ + propertyType.getCanonicalName());
+ }
+ }
+
+ Object itemId = dataSource.addItem();
+ try {
+ Item item = dataSource.getItem(itemId);
+ for (int i = 0; i < columnOrder.size(); i++) {
+ Object propertyId = getPropertyIdByColumnId(columnOrder.get(i));
+ Property<Object> property = item.getItemProperty(propertyId);
+ property.setValue(values[i]);
+ }
+ } catch (RuntimeException e) {
+ try {
+ dataSource.removeItem(itemId);
+ } catch (Exception e2) {
+ getLogger().log(Level.SEVERE,
+ "Error recovering from exception in addRow", e);
+ }
+ throw e;
+ }
+
+ return itemId;
+ }
+
+ private static Logger getLogger() {
+ return Logger.getLogger(Grid.class.getName());
+ }
+
+ /**
+ * Sets whether or not the item editor UI is enabled for this grid. When the
+ * editor is enabled, the user can open it by double-clicking a row or
+ * hitting enter when a row is focused. The editor can also be opened
+ * programmatically using the {@link #editItem(Object)} method.
+ *
+ * @param isEnabled
+ * <code>true</code> to enable the feature, <code>false</code>
+ * otherwise
+ * @throws IllegalStateException
+ * if an item is currently being edited
+ *
+ * @see #getEditedItemId()
+ */
+ public void setEditorEnabled(boolean isEnabled)
+ throws IllegalStateException {
+ if (isEditorActive()) {
+ throw new IllegalStateException(
+ "Cannot disable the editor while an item ("
+ + getEditedItemId() + ") is being edited");
+ }
+ if (isEditorEnabled() != isEnabled) {
+ getState().editorEnabled = isEnabled;
+ }
+ }
+
+ /**
+ * Checks whether the item editor UI is enabled for this grid.
+ *
+ * @return <code>true</code> iff the editor is enabled for this grid
+ *
+ * @see #setEditorEnabled(boolean)
+ * @see #getEditedItemId()
+ */
+ public boolean isEditorEnabled() {
+ return getState(false).editorEnabled;
+ }
+
+ /**
+ * Gets the id of the item that is currently being edited.
+ *
+ * @return the id of the item that is currently being edited, or
+ * <code>null</code> if no item is being edited at the moment
+ */
+ public Object getEditedItemId() {
+ return editedItemId;
+ }
+
+ /**
+ * Gets the field group that is backing the item editor of this grid.
+ *
+ * @return the backing field group
+ */
+ public FieldGroup getEditorFieldGroup() {
+ return editorFieldGroup;
+ }
+
+ /**
+ * Sets the field group that is backing the item editor of this grid.
+ *
+ * @param fieldGroup
+ * the backing field group
+ *
+ * @throws IllegalStateException
+ * if the editor is currently active
+ */
+ public void setEditorFieldGroup(FieldGroup fieldGroup) {
+ if (isEditorActive()) {
+ throw new IllegalStateException(
+ "Cannot change field group while an item ("
+ + getEditedItemId() + ") is being edited");
+ }
+ editorFieldGroup = fieldGroup;
+ }
+
+ /**
+ * Returns whether an item is currently being edited in the editor.
+ *
+ * @return true iff the editor is open
+ */
+ public boolean isEditorActive() {
+ return editorActive;
+ }
+
+ private void checkColumnExists(Object propertyId) {
+ if (getColumn(propertyId) == null) {
+ throw new IllegalArgumentException(
+ "There is no column with the property id " + propertyId);
+ }
+ }
+
+ private Field<?> getEditorField(Object propertyId) {
+ checkColumnExists(propertyId);
+
+ if (!getColumn(propertyId).isEditable()) {
+ return null;
+ }
+
+ Field<?> editor = editorFieldGroup.getField(propertyId);
+
+ try {
+ if (editor == null) {
+ editor = editorFieldGroup.buildAndBind(propertyId);
+ }
+ } finally {
+ if (editor == null) {
+ editor = editorFieldGroup.getField(propertyId);
+ }
+
+ if (editor != null && editor.getParent() != Grid.this) {
+ assert editor.getParent() == null;
+ editor.setParent(this);
+ }
+ }
+ return editor;
+ }
+
+ /**
+ * Opens the editor interface for the provided item. Scrolls the Grid to
+ * bring the item to view if it is not already visible.
+ *
+ * Note that any cell content rendered by a WidgetRenderer will not be
+ * visible in the editor row.
+ *
+ * @param itemId
+ * the id of the item to edit
+ * @throws IllegalStateException
+ * if the editor is not enabled or already editing an item in
+ * buffered mode
+ * @throws IllegalArgumentException
+ * if the {@code itemId} is not in the backing container
+ * @see #setEditorEnabled(boolean)
+ */
+ public void editItem(Object itemId)
+ throws IllegalStateException, IllegalArgumentException {
+ if (!isEditorEnabled()) {
+ throw new IllegalStateException("Item editor is not enabled");
+ } else if (isEditorBuffered() && editedItemId != null) {
+ throw new IllegalStateException("Editing item " + itemId
+ + " failed. Item editor is already editing item "
+ + editedItemId);
+ } else if (!getContainerDataSource().containsId(itemId)) {
+ throw new IllegalArgumentException("Item with id " + itemId
+ + " not found in current container");
+ }
+ editedItemId = itemId;
+ getEditorRpc().bind(getContainerDataSource().indexOfId(itemId));
+ }
+
+ protected void doEditItem() {
+ Item item = getContainerDataSource().getItem(editedItemId);
+
+ editorFieldGroup.setItemDataSource(item);
+
+ for (Column column : getColumns()) {
+ column.getState().editorConnector = getEditorField(
+ column.getPropertyId());
+ }
+
+ editorActive = true;
+ // Must ensure that all fields, recursively, are sent to the client
+ // This is needed because the fields are hidden using isRendered
+ for (Field<?> f : getEditorFields()) {
+ f.markAsDirtyRecursive();
+ }
+
+ if (datasource instanceof ItemSetChangeNotifier) {
+ ((ItemSetChangeNotifier) datasource)
+ .addItemSetChangeListener(editorClosingItemSetListener);
+ }
+ }
+
+ private void setEditorField(Object propertyId, Field<?> field) {
+ checkColumnExists(propertyId);
+
+ Field<?> oldField = editorFieldGroup.getField(propertyId);
+ if (oldField != null) {
+ editorFieldGroup.unbind(oldField);
+ oldField.setParent(null);
+ }
+
+ if (field != null) {
+ field.setParent(this);
+ editorFieldGroup.bind(field, propertyId);
+ }
+ }
+
+ /**
+ * Saves all changes done to the bound fields.
+ * <p>
+ * <em>Note:</em> This is a pass-through call to the backing field group.
+ *
+ * @throws CommitException
+ * If the commit was aborted
+ *
+ * @see FieldGroup#commit()
+ */
+ public void saveEditor() throws CommitException {
+ editorFieldGroup.commit();
+ }
+
+ /**
+ * Cancels the currently active edit if any. Hides the editor and discards
+ * possible unsaved changes in the editor fields.
+ */
+ public void cancelEditor() {
+ if (isEditorActive()) {
+ getEditorRpc()
+ .cancel(getContainerDataSource().indexOfId(editedItemId));
+ doCancelEditor();
+ }
+ }
+
+ protected void doCancelEditor() {
+ editedItemId = null;
+ editorActive = false;
+ editorFieldGroup.discard();
+ editorFieldGroup.setItemDataSource(null);
+
+ if (datasource instanceof ItemSetChangeNotifier) {
+ ((ItemSetChangeNotifier) datasource)
+ .removeItemSetChangeListener(editorClosingItemSetListener);
+ }
+
+ // Mark Grid as dirty so the client side gets to know that the editors
+ // are no longer attached
+ markAsDirty();
+ }
+
+ void resetEditor() {
+ if (isEditorActive()) {
+ /*
+ * Simply force cancel the editing; throwing here would just make
+ * Grid.setContainerDataSource semantics more complicated.
+ */
+ cancelEditor();
+ }
+ for (Field<?> editor : getEditorFields()) {
+ editor.setParent(null);
+ }
+
+ editedItemId = null;
+ editorActive = false;
+ editorFieldGroup = new CustomFieldGroup();
+ }
+
+ /**
+ * Gets a collection of all fields bound to the item editor of this grid.
+ * <p>
+ * When {@link #editItem(Object) editItem} is called, fields are
+ * automatically created and bound to any unbound properties.
+ *
+ * @return a collection of all the fields bound to the item editor
+ */
+ Collection<Field<?>> getEditorFields() {
+ Collection<Field<?>> fields = editorFieldGroup.getFields();
+ assert allAttached(fields);
+ return fields;
+ }
+
+ private boolean allAttached(Collection<? extends Component> components) {
+ for (Component component : components) {
+ if (component.getParent() != this) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Sets the field factory for the {@link FieldGroup}. The field factory is
+ * only used when {@link FieldGroup} creates a new field.
+ * <p>
+ * <em>Note:</em> This is a pass-through call to the backing field group.
+ *
+ * @param fieldFactory
+ * The field factory to use
+ */
+ public void setEditorFieldFactory(FieldGroupFieldFactory fieldFactory) {
+ editorFieldGroup.setFieldFactory(fieldFactory);
+ }
+
+ /**
+ * Sets the error handler for the editor.
+ *
+ * The error handler is called whenever there is an exception in the editor.
+ *
+ * @param editorErrorHandler
+ * The editor error handler to use
+ * @throws IllegalArgumentException
+ * if the error handler is null
+ */
+ public void setEditorErrorHandler(EditorErrorHandler editorErrorHandler)
+ throws IllegalArgumentException {
+ if (editorErrorHandler == null) {
+ throw new IllegalArgumentException(
+ "The error handler cannot be null");
+ }
+ this.editorErrorHandler = editorErrorHandler;
+ }
+
+ /**
+ * Gets the error handler used for the editor
+ *
+ * @see #setErrorHandler(com.vaadin.server.ErrorHandler)
+ * @return the editor error handler, never null
+ */
+ public EditorErrorHandler getEditorErrorHandler() {
+ return editorErrorHandler;
+ }
+
+ /**
+ * Gets the field factory for the {@link FieldGroup}. The field factory is
+ * only used when {@link FieldGroup} creates a new field.
+ * <p>
+ * <em>Note:</em> This is a pass-through call to the backing field group.
+ *
+ * @return The field factory in use
+ */
+ public FieldGroupFieldFactory getEditorFieldFactory() {
+ return editorFieldGroup.getFieldFactory();
+ }
+
+ /**
+ * Sets the caption on the save button in the Grid editor.
+ *
+ * @param saveCaption
+ * the caption to set
+ * @throws IllegalArgumentException
+ * if {@code saveCaption} is {@code null}
+ */
+ public void setEditorSaveCaption(String saveCaption)
+ throws IllegalArgumentException {
+ if (saveCaption == null) {
+ throw new IllegalArgumentException("Save caption cannot be null");
+ }
+ getState().editorSaveCaption = saveCaption;
+ }
+
+ /**
+ * Gets the current caption of the save button in the Grid editor.
+ *
+ * @return the current caption of the save button
+ */
+ public String getEditorSaveCaption() {
+ return getState(false).editorSaveCaption;
+ }
+
+ /**
+ * Sets the caption on the cancel button in the Grid editor.
+ *
+ * @param cancelCaption
+ * the caption to set
+ * @throws IllegalArgumentException
+ * if {@code cancelCaption} is {@code null}
+ */
+ public void setEditorCancelCaption(String cancelCaption)
+ throws IllegalArgumentException {
+ if (cancelCaption == null) {
+ throw new IllegalArgumentException("Cancel caption cannot be null");
+ }
+ getState().editorCancelCaption = cancelCaption;
+ }
+
+ /**
+ * Gets the current caption of the cancel button in the Grid editor.
+ *
+ * @return the current caption of the cancel button
+ */
+ public String getEditorCancelCaption() {
+ return getState(false).editorCancelCaption;
+ }
+
+ /**
+ * Sets the buffered editor mode. The default mode is buffered (
+ * <code>true</code>).
+ *
+ * @since 7.6
+ * @param editorBuffered
+ * <code>true</code> to enable buffered editor,
+ * <code>false</code> to disable it
+ * @throws IllegalStateException
+ * If editor is active while attempting to change the buffered
+ * mode.
+ */
+ public void setEditorBuffered(boolean editorBuffered)
+ throws IllegalStateException {
+ if (isEditorActive()) {
+ throw new IllegalStateException(
+ "Can't change editor unbuffered mode while editor is active.");
+ }
+ getState().editorBuffered = editorBuffered;
+ editorFieldGroup.setBuffered(editorBuffered);
+ }
+
+ /**
+ * Gets the buffered editor mode.
+ *
+ * @since 7.6
+ * @return <code>true</code> if buffered editor is enabled,
+ * <code>false</code> otherwise
+ */
+ public boolean isEditorBuffered() {
+ return getState(false).editorBuffered;
+ }
+
+ @Override
+ public void addItemClickListener(ItemClickListener listener) {
+ addListener(GridConstants.ITEM_CLICK_EVENT_ID, ItemClickEvent.class,
+ listener, ItemClickEvent.ITEM_CLICK_METHOD);
+ }
+
+ @Override
+ @Deprecated
+ public void addListener(ItemClickListener listener) {
+ addItemClickListener(listener);
+ }
+
+ @Override
+ public void removeItemClickListener(ItemClickListener listener) {
+ removeListener(GridConstants.ITEM_CLICK_EVENT_ID, ItemClickEvent.class,
+ listener);
+ }
+
+ @Override
+ @Deprecated
+ public void removeListener(ItemClickListener listener) {
+ removeItemClickListener(listener);
+ }
+
+ /**
+ * Requests that the column widths should be recalculated.
+ * <p>
+ * In most cases Grid will know when column widths need to be recalculated
+ * but this method can be used to force recalculation in situations when
+ * grid does not recalculate automatically.
+ *
+ * @since 7.4.1
+ */
+ public void recalculateColumnWidths() {
+ getRpcProxy(GridClientRpc.class).recalculateColumnWidths();
+ }
+
+ /**
+ * Registers a new column visibility change listener
+ *
+ * @since 7.5.0
+ * @param listener
+ * the listener to register
+ */
+ public void addColumnVisibilityChangeListener(
+ ColumnVisibilityChangeListener listener) {
+ addListener(ColumnVisibilityChangeEvent.class, listener,
+ COLUMN_VISIBILITY_METHOD);
+ }
+
+ /**
+ * Removes a previously registered column visibility change listener
+ *
+ * @since 7.5.0
+ * @param listener
+ * the listener to remove
+ */
+ public void removeColumnVisibilityChangeListener(
+ ColumnVisibilityChangeListener listener) {
+ removeListener(ColumnVisibilityChangeEvent.class, listener,
+ COLUMN_VISIBILITY_METHOD);
+ }
+
+ private void fireColumnVisibilityChangeEvent(Column column, boolean hidden,
+ boolean isUserOriginated) {
+ fireEvent(new ColumnVisibilityChangeEvent(this, column, hidden,
+ isUserOriginated));
+ }
+
+ /**
+ * Sets a new details generator for row details.
+ * <p>
+ * The currently opened row details will be re-rendered.
+ *
+ * @since 7.5.0
+ * @param detailsGenerator
+ * the details generator to set
+ * @throws IllegalArgumentException
+ * if detailsGenerator is <code>null</code>;
+ */
+ public void setDetailsGenerator(DetailsGenerator detailsGenerator)
+ throws IllegalArgumentException {
+ detailComponentManager.setDetailsGenerator(detailsGenerator);
+ }
+
+ /**
+ * Gets the current details generator for row details.
+ *
+ * @since 7.5.0
+ * @return the detailsGenerator the current details generator
+ */
+ public DetailsGenerator getDetailsGenerator() {
+ return detailComponentManager.getDetailsGenerator();
+ }
+
+ /**
+ * Shows or hides the details for a specific item.
+ *
+ * @since 7.5.0
+ * @param itemId
+ * the id of the item for which to set details visibility
+ * @param visible
+ * <code>true</code> to show the details, or <code>false</code>
+ * to hide them
+ */
+ public void setDetailsVisible(Object itemId, boolean visible) {
+ detailComponentManager.setDetailsVisible(itemId, visible);
+ }
+
+ /**
+ * Checks whether details are visible for the given item.
+ *
+ * @since 7.5.0
+ * @param itemId
+ * the id of the item for which to check details visibility
+ * @return <code>true</code> iff the details are visible
+ */
+ public boolean isDetailsVisible(Object itemId) {
+ return detailComponentManager.isDetailsVisible(itemId);
+ }
+
+ private static SelectionMode getDefaultSelectionMode() {
+ return SelectionMode.SINGLE;
+ }
+
+ @Override
+ public void readDesign(Element design, DesignContext context) {
+ super.readDesign(design, context);
+
+ Attributes attrs = design.attributes();
+ if (attrs.hasKey("editable")) {
+ setEditorEnabled(DesignAttributeHandler.readAttribute("editable",
+ attrs, boolean.class));
+ }
+ if (attrs.hasKey("rows")) {
+ setHeightByRows(DesignAttributeHandler.readAttribute("rows", attrs,
+ double.class));
+ setHeightMode(HeightMode.ROW);
+ }
+ if (attrs.hasKey("selection-mode")) {
+ setSelectionMode(DesignAttributeHandler.readAttribute(
+ "selection-mode", attrs, SelectionMode.class));
+ }
+
+ if (design.children().size() > 0) {
+ if (design.children().size() > 1
+ || !design.child(0).tagName().equals("table")) {
+ throw new DesignException(
+ "Grid needs to have a table element as its only child");
+ }
+ Element table = design.child(0);
+
+ Elements colgroups = table.getElementsByTag("colgroup");
+ if (colgroups.size() != 1) {
+ throw new DesignException(
+ "Table element in declarative Grid needs to have a"
+ + " colgroup defining the columns used in Grid");
+ }
+
+ int i = 0;
+ for (Element col : colgroups.get(0).getElementsByTag("col")) {
+ String propertyId = DesignAttributeHandler.readAttribute(
+ "property-id", col.attributes(), "property-" + i,
+ String.class);
+ addColumn(propertyId, String.class).readDesign(col, context);
+ ++i;
+ }
+
+ for (Element child : table.children()) {
+ if (child.tagName().equals("thead")) {
+ header.readDesign(child, context);
+ } else if (child.tagName().equals("tbody")) {
+ for (Element row : child.children()) {
+ Elements cells = row.children();
+ Object[] data = new String[cells.size()];
+ for (int c = 0; c < cells.size(); ++c) {
+ data[c] = cells.get(c).html();
+ }
+ addRow(data);
+ }
+
+ // Since inline data is used, set HTML renderer for columns
+ for (Column c : getColumns()) {
+ c.setRenderer(new HtmlRenderer());
+ }
+ } else if (child.tagName().equals("tfoot")) {
+ footer.readDesign(child, context);
+ }
+ }
+ }
+
+ // Read frozen columns after columns are read.
+ if (attrs.hasKey("frozen-columns")) {
+ setFrozenColumnCount(DesignAttributeHandler
+ .readAttribute("frozen-columns", attrs, int.class));
+ }
+ }
+
+ @Override
+ public void writeDesign(Element design, DesignContext context) {
+ super.writeDesign(design, context);
+
+ Attributes attrs = design.attributes();
+ Grid def = context.getDefaultInstance(this);
+
+ DesignAttributeHandler.writeAttribute("editable", attrs,
+ isEditorEnabled(), def.isEditorEnabled(), boolean.class);
+
+ DesignAttributeHandler.writeAttribute("frozen-columns", attrs,
+ getFrozenColumnCount(), def.getFrozenColumnCount(), int.class);
+
+ if (getHeightMode() == HeightMode.ROW) {
+ DesignAttributeHandler.writeAttribute("rows", attrs,
+ getHeightByRows(), def.getHeightByRows(), double.class);
+ }
+
+ SelectionMode selectionMode = null;
+
+ if (selectionModel.getClass().equals(SingleSelectionModel.class)) {
+ selectionMode = SelectionMode.SINGLE;
+ } else if (selectionModel.getClass()
+ .equals(MultiSelectionModel.class)) {
+ selectionMode = SelectionMode.MULTI;
+ } else if (selectionModel.getClass().equals(NoSelectionModel.class)) {
+ selectionMode = SelectionMode.NONE;
+ }
+
+ assert selectionMode != null : "Unexpected selection model "
+ + selectionModel.getClass().getName();
+
+ DesignAttributeHandler.writeAttribute("selection-mode", attrs,
+ selectionMode, getDefaultSelectionMode(), SelectionMode.class);
+
+ if (columns.isEmpty()) {
+ // Empty grid. Structure not needed.
+ return;
+ }
+
+ // Do structure.
+ Element tableElement = design.appendElement("table");
+ Element colGroup = tableElement.appendElement("colgroup");
+
+ List<Column> columnOrder = getColumns();
+ for (int i = 0; i < columnOrder.size(); ++i) {
+ Column column = columnOrder.get(i);
+ Element colElement = colGroup.appendElement("col");
+ column.writeDesign(colElement, context);
+ }
+
+ // Always write thead. Reads correctly when there no header rows
+ header.writeDesign(tableElement.appendElement("thead"), context);
+
+ if (context.shouldWriteData(this)) {
+ Element bodyElement = tableElement.appendElement("tbody");
+ for (Object itemId : datasource.getItemIds()) {
+ Element tableRow = bodyElement.appendElement("tr");
+ for (Column c : getColumns()) {
+ Object value = datasource.getItem(itemId)
+ .getItemProperty(c.getPropertyId()).getValue();
+ tableRow.appendElement("td")
+ .append((value != null ? DesignFormatter
+ .encodeForTextNode(value.toString()) : ""));
+ }
+ }
+ }
+
+ if (footer.getRowCount() > 0) {
+ footer.writeDesign(tableElement.appendElement("tfoot"), context);
+ }
+ }
+
+ @Override
+ protected Collection<String> getCustomAttributes() {
+ Collection<String> result = super.getCustomAttributes();
+ result.add("editor-enabled");
+ result.add("editable");
+ result.add("frozen-column-count");
+ result.add("frozen-columns");
+ result.add("height-by-rows");
+ result.add("rows");
+ result.add("selection-mode");
+ result.add("header-visible");
+ result.add("footer-visible");
+ result.add("editor-error-handler");
+ result.add("height-mode");
+
+ return result;
+ }
+}
diff --git a/compatibility-server/src/main/java/com/vaadin/v7/ui/LegacyInlineDateField.java b/compatibility-server/src/main/java/com/vaadin/v7/ui/InlineDateField.java
index 4e1ad7e997..1bec2bc61e 100644
--- a/compatibility-server/src/main/java/com/vaadin/v7/ui/LegacyInlineDateField.java
+++ b/compatibility-server/src/main/java/com/vaadin/v7/ui/InlineDateField.java
@@ -18,7 +18,7 @@ package com.vaadin.v7.ui;
import java.util.Date;
-import com.vaadin.data.Property;
+import com.vaadin.v7.data.Property;
/**
* <p>
@@ -26,31 +26,31 @@ import com.vaadin.data.Property;
*
* </p>
*
- * @see LegacyDateField
- * @see LegacyPopupDateField
+ * @see DateField
+ * @see PopupDateField
* @author Vaadin Ltd.
* @since 5.0
*/
-public class LegacyInlineDateField extends LegacyDateField {
+public class InlineDateField extends DateField {
- public LegacyInlineDateField() {
+ public InlineDateField() {
super();
}
- public LegacyInlineDateField(Property dataSource)
+ public InlineDateField(Property dataSource)
throws IllegalArgumentException {
super(dataSource);
}
- public LegacyInlineDateField(String caption, Date value) {
+ public InlineDateField(String caption, Date value) {
super(caption, value);
}
- public LegacyInlineDateField(String caption, Property dataSource) {
+ public InlineDateField(String caption, Property dataSource) {
super(caption, dataSource);
}
- public LegacyInlineDateField(String caption) {
+ public InlineDateField(String caption) {
super(caption);
}
diff --git a/compatibility-server/src/main/java/com/vaadin/v7/ui/ListSelect.java b/compatibility-server/src/main/java/com/vaadin/v7/ui/ListSelect.java
new file mode 100644
index 0000000000..3b5683f55d
--- /dev/null
+++ b/compatibility-server/src/main/java/com/vaadin/v7/ui/ListSelect.java
@@ -0,0 +1,80 @@
+/*
+ * 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.v7.ui;
+
+import java.util.Collection;
+
+import com.vaadin.server.PaintException;
+import com.vaadin.server.PaintTarget;
+import com.vaadin.v7.data.Container;
+
+/**
+ * This is a simple list select without, for instance, support for new items,
+ * lazyloading, and other advanced features.
+ */
+@SuppressWarnings("serial")
+public class ListSelect extends AbstractSelect {
+
+ private int rows = 0;
+
+ public ListSelect() {
+ super();
+ }
+
+ public ListSelect(String caption, Collection<?> options) {
+ super(caption, options);
+ }
+
+ public ListSelect(String caption, Container dataSource) {
+ super(caption, dataSource);
+ }
+
+ public ListSelect(String caption) {
+ super(caption);
+ }
+
+ public int getRows() {
+ return rows;
+ }
+
+ /**
+ * Sets the number of rows in the editor. If the number of rows is set 0,
+ * the actual number of displayed rows is determined implicitly by the
+ * adapter.
+ *
+ * @param rows
+ * the number of rows to set.
+ */
+ public void setRows(int rows) {
+ if (rows < 0) {
+ rows = 0;
+ }
+ if (this.rows != rows) {
+ this.rows = rows;
+ markAsDirty();
+ }
+ }
+
+ @Override
+ public void paintContent(PaintTarget target) throws PaintException {
+ // Adds the number of rows
+ if (rows != 0) {
+ target.addAttribute("rows", rows);
+ }
+ super.paintContent(target);
+ }
+}
diff --git a/compatibility-server/src/main/java/com/vaadin/v7/ui/NativeSelect.java b/compatibility-server/src/main/java/com/vaadin/v7/ui/NativeSelect.java
new file mode 100644
index 0000000000..d68f184d43
--- /dev/null
+++ b/compatibility-server/src/main/java/com/vaadin/v7/ui/NativeSelect.java
@@ -0,0 +1,108 @@
+/*
+ * 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.v7.ui;
+
+import java.util.Collection;
+
+import com.vaadin.event.FieldEvents;
+import com.vaadin.event.FieldEvents.BlurEvent;
+import com.vaadin.event.FieldEvents.BlurListener;
+import com.vaadin.event.FieldEvents.FocusAndBlurServerRpcImpl;
+import com.vaadin.event.FieldEvents.FocusEvent;
+import com.vaadin.event.FieldEvents.FocusListener;
+import com.vaadin.v7.data.Container;
+
+/**
+ * This is a simple drop-down select without, for instance, support for
+ * multiselect, new items, lazyloading, and other advanced features. Sometimes
+ * "native" select without all the bells-and-whistles of the ComboBox is a
+ * better choice.
+ */
+@SuppressWarnings("serial")
+public class NativeSelect extends AbstractSelect
+ implements FieldEvents.BlurNotifier, FieldEvents.FocusNotifier {
+
+ FocusAndBlurServerRpcImpl focusBlurRpc = new FocusAndBlurServerRpcImpl(
+ this) {
+
+ @Override
+ protected void fireEvent(Event event) {
+ NativeSelect.this.fireEvent(event);
+ }
+ };
+
+ public NativeSelect() {
+ super();
+ registerRpc(focusBlurRpc);
+ }
+
+ public NativeSelect(String caption, Collection<?> options) {
+ super(caption, options);
+ registerRpc(focusBlurRpc);
+ }
+
+ public NativeSelect(String caption, Container dataSource) {
+ super(caption, dataSource);
+ registerRpc(focusBlurRpc);
+ }
+
+ public NativeSelect(String caption) {
+ super(caption);
+ registerRpc(focusBlurRpc);
+ }
+
+ @Override
+ public void setMultiSelect(boolean multiSelect)
+ throws UnsupportedOperationException {
+ if (multiSelect == true) {
+ throw new UnsupportedOperationException(
+ "Multiselect not supported");
+ }
+ }
+
+ @Override
+ public void setNewItemsAllowed(boolean allowNewOptions)
+ throws UnsupportedOperationException {
+ if (allowNewOptions == true) {
+ throw new UnsupportedOperationException(
+ "newItemsAllowed not supported");
+ }
+ }
+
+ @Override
+ public void addFocusListener(FocusListener listener) {
+ addListener(FocusEvent.EVENT_ID, FocusEvent.class, listener,
+ FocusListener.focusMethod);
+ }
+
+ @Override
+ public void removeFocusListener(FocusListener listener) {
+ removeListener(FocusEvent.EVENT_ID, FocusEvent.class, listener);
+ }
+
+ @Override
+ public void addBlurListener(BlurListener listener) {
+ addListener(BlurEvent.EVENT_ID, BlurEvent.class, listener,
+ BlurListener.blurMethod);
+ }
+
+ @Override
+ public void removeBlurListener(BlurListener listener) {
+ removeListener(BlurEvent.EVENT_ID, BlurEvent.class, listener);
+ }
+
+}
diff --git a/compatibility-server/src/main/java/com/vaadin/v7/ui/OptionGroup.java b/compatibility-server/src/main/java/com/vaadin/v7/ui/OptionGroup.java
new file mode 100644
index 0000000000..f4e3415cf5
--- /dev/null
+++ b/compatibility-server/src/main/java/com/vaadin/v7/ui/OptionGroup.java
@@ -0,0 +1,253 @@
+/*
+ * 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.v7.ui;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import org.jsoup.nodes.Element;
+
+import com.vaadin.event.FieldEvents;
+import com.vaadin.event.FieldEvents.BlurEvent;
+import com.vaadin.event.FieldEvents.BlurListener;
+import com.vaadin.event.FieldEvents.FocusEvent;
+import com.vaadin.event.FieldEvents.FocusListener;
+import com.vaadin.server.PaintException;
+import com.vaadin.server.PaintTarget;
+import com.vaadin.shared.ui.optiongroup.OptionGroupConstants;
+import com.vaadin.shared.ui.optiongroup.OptionGroupState;
+import com.vaadin.ui.declarative.DesignContext;
+import com.vaadin.ui.declarative.DesignFormatter;
+import com.vaadin.v7.data.Container;
+
+/**
+ * Configures select to be used as an option group.
+ */
+@SuppressWarnings("serial")
+public class OptionGroup extends AbstractSelect
+ implements FieldEvents.BlurNotifier, FieldEvents.FocusNotifier {
+
+ private Set<Object> disabledItemIds = new HashSet<Object>();
+
+ public OptionGroup() {
+ super();
+ }
+
+ public OptionGroup(String caption, Collection<?> options) {
+ super(caption, options);
+ }
+
+ public OptionGroup(String caption, Container dataSource) {
+ super(caption, dataSource);
+ }
+
+ public OptionGroup(String caption) {
+ super(caption);
+ }
+
+ @Override
+ protected void paintItem(PaintTarget target, Object itemId)
+ throws PaintException {
+ super.paintItem(target, itemId);
+ if (!isItemEnabled(itemId)) {
+ target.addAttribute(OptionGroupConstants.ATTRIBUTE_OPTION_DISABLED,
+ true);
+ }
+ }
+
+ @Override
+ public void changeVariables(Object source, Map<String, Object> variables) {
+ super.changeVariables(source, variables);
+
+ if (variables.containsKey(FocusEvent.EVENT_ID)) {
+ fireEvent(new FocusEvent(this));
+ }
+ if (variables.containsKey(BlurEvent.EVENT_ID)) {
+ fireEvent(new BlurEvent(this));
+ }
+ }
+
+ @Override
+ public void addBlurListener(BlurListener listener) {
+ addListener(BlurEvent.EVENT_ID, BlurEvent.class, listener,
+ BlurListener.blurMethod);
+ }
+
+ @Override
+ public void removeBlurListener(BlurListener listener) {
+ removeListener(BlurEvent.EVENT_ID, BlurEvent.class, listener);
+ }
+
+ @Override
+ public void addFocusListener(FocusListener listener) {
+ addListener(FocusEvent.EVENT_ID, FocusEvent.class, listener,
+ FocusListener.focusMethod);
+ }
+
+ @Override
+ public void removeFocusListener(FocusListener listener) {
+ removeListener(FocusEvent.EVENT_ID, FocusEvent.class, listener);
+
+ }
+
+ @Override
+ protected void setValue(Object newValue, boolean repaintIsNotNeeded) {
+ if (repaintIsNotNeeded) {
+ /*
+ * Check that value from changeVariables() doesn't contain unallowed
+ * selections: In the multi select mode, the user has selected or
+ * deselected a disabled item. In the single select mode, the user
+ * has selected a disabled item.
+ */
+ if (isMultiSelect()) {
+ Set<?> currentValueSet = (Set<?>) getValue();
+ Set<?> newValueSet = (Set<?>) newValue;
+ for (Object itemId : currentValueSet) {
+ if (!isItemEnabled(itemId)
+ && !newValueSet.contains(itemId)) {
+ markAsDirty();
+ return;
+ }
+ }
+ for (Object itemId : newValueSet) {
+ if (!isItemEnabled(itemId)
+ && !currentValueSet.contains(itemId)) {
+ markAsDirty();
+ return;
+ }
+ }
+ } else {
+ if (newValue == null) {
+ newValue = getNullSelectionItemId();
+ }
+ if (!isItemEnabled(newValue)) {
+ markAsDirty();
+ return;
+ }
+ }
+ }
+ super.setValue(newValue, repaintIsNotNeeded);
+ }
+
+ /**
+ * Sets an item disabled or enabled. In the multiselect mode, a disabled
+ * item cannot be selected or deselected by the user. In the single
+ * selection mode, a disable item cannot be selected.
+ *
+ * However, programmatical selection or deselection of an disable item is
+ * possible. By default, items are enabled.
+ *
+ * @param itemId
+ * the id of the item to be disabled or enabled
+ * @param enabled
+ * if true the item is enabled, otherwise the item is disabled
+ */
+ public void setItemEnabled(Object itemId, boolean enabled) {
+ if (itemId != null) {
+ if (enabled) {
+ disabledItemIds.remove(itemId);
+ } else {
+ disabledItemIds.add(itemId);
+ }
+ markAsDirty();
+ }
+ }
+
+ /**
+ * Returns true if the item is enabled.
+ *
+ * @param itemId
+ * the id of the item to be checked
+ * @return true if the item is enabled, false otherwise
+ * @see #setItemEnabled(Object, boolean)
+ */
+ public boolean isItemEnabled(Object itemId) {
+ if (itemId != null) {
+ return !disabledItemIds.contains(itemId);
+ }
+ return true;
+ }
+
+ /**
+ * Sets whether html is allowed in the item captions. If set to true, the
+ * captions are passed to the browser as html and the developer is
+ * responsible for ensuring no harmful html is used. If set to false, the
+ * content is passed to the browser as plain text.
+ *
+ * @param htmlContentAllowed
+ * true if the captions are used as html, false if used as plain
+ * text
+ */
+ public void setHtmlContentAllowed(boolean htmlContentAllowed) {
+ getState().htmlContentAllowed = htmlContentAllowed;
+ }
+
+ /**
+ * Checks whether captions are interpreted as html or plain text.
+ *
+ * @return true if the captions are used as html, false if used as plain
+ * text
+ * @see #setHtmlContentAllowed(boolean)
+ */
+ public boolean isHtmlContentAllowed() {
+ return getState(false).htmlContentAllowed;
+ }
+
+ @Override
+ protected Object readItem(Element child, Set<String> selected,
+ DesignContext context) {
+ Object itemId = super.readItem(child, selected, context);
+
+ if (child.hasAttr("disabled")) {
+ setItemEnabled(itemId, false);
+ }
+
+ return itemId;
+ }
+
+ @Override
+ protected Element writeItem(Element design, Object itemId,
+ DesignContext context) {
+ Element elem = super.writeItem(design, itemId, context);
+
+ if (!isItemEnabled(itemId)) {
+ elem.attr("disabled", "");
+ }
+ if (isHtmlContentAllowed()) {
+ // need to unencode HTML entities. AbstractSelect.writeDesign can't
+ // check if HTML content is allowed, so it always encodes entities
+ // like '>', '<' and '&'; in case HTML content is allowed this is
+ // undesirable so we need to unencode entities. Entities other than
+ // '<' and '>' will be taken care by Jsoup.
+ elem.html(DesignFormatter.decodeFromTextNode(elem.html()));
+ }
+
+ return elem;
+ }
+
+ @Override
+ protected OptionGroupState getState() {
+ return (OptionGroupState) super.getState();
+ }
+
+ @Override
+ protected OptionGroupState getState(boolean markAsDirty) {
+ return (OptionGroupState) super.getState(markAsDirty);
+ }
+}
diff --git a/compatibility-server/src/main/java/com/vaadin/v7/ui/LegacyPopupDateField.java b/compatibility-server/src/main/java/com/vaadin/v7/ui/PopupDateField.java
index c2470daf26..1ae0f79b30 100644
--- a/compatibility-server/src/main/java/com/vaadin/v7/ui/LegacyPopupDateField.java
+++ b/compatibility-server/src/main/java/com/vaadin/v7/ui/PopupDateField.java
@@ -18,10 +18,10 @@ package com.vaadin.v7.ui;
import java.util.Date;
-import com.vaadin.data.Property;
import com.vaadin.server.PaintException;
import com.vaadin.server.PaintTarget;
import com.vaadin.shared.ui.datefield.PopupDateFieldState;
+import com.vaadin.v7.data.Property;
/**
* <p>
@@ -29,33 +29,33 @@ import com.vaadin.shared.ui.datefield.PopupDateFieldState;
*
* </p>
*
- * @see LegacyDateField
- * @see LegacyInlineDateField
+ * @see DateField
+ * @see InlineDateField
* @author Vaadin Ltd.
* @since 5.0
*/
-public class LegacyPopupDateField extends LegacyDateField {
+public class PopupDateField extends DateField {
private String inputPrompt = null;
- public LegacyPopupDateField() {
+ public PopupDateField() {
super();
}
- public LegacyPopupDateField(Property dataSource)
+ public PopupDateField(Property dataSource)
throws IllegalArgumentException {
super(dataSource);
}
- public LegacyPopupDateField(String caption, Date value) {
+ public PopupDateField(String caption, Date value) {
super(caption, value);
}
- public LegacyPopupDateField(String caption, Property dataSource) {
+ public PopupDateField(String caption, Property dataSource) {
super(caption, dataSource);
}
- public LegacyPopupDateField(String caption) {
+ public PopupDateField(String caption) {
super(caption);
}
diff --git a/compatibility-server/src/main/java/com/vaadin/v7/ui/RichTextArea.java b/compatibility-server/src/main/java/com/vaadin/v7/ui/RichTextArea.java
new file mode 100644
index 0000000000..e7a790a2cc
--- /dev/null
+++ b/compatibility-server/src/main/java/com/vaadin/v7/ui/RichTextArea.java
@@ -0,0 +1,317 @@
+/*
+ * 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.v7.ui;
+
+import java.util.Map;
+
+import org.jsoup.nodes.Element;
+
+import com.vaadin.server.PaintException;
+import com.vaadin.server.PaintTarget;
+import com.vaadin.shared.ui.textarea.RichTextAreaState;
+import com.vaadin.ui.LegacyComponent;
+import com.vaadin.ui.declarative.DesignContext;
+import com.vaadin.v7.data.Property;
+
+/**
+ * A simple RichTextArea to edit HTML format text.
+ *
+ * Note, that using {@link TextField#setMaxLength(int)} method in
+ * {@link RichTextArea} may produce unexpected results as formatting is counted
+ * into length of field.
+ */
+public class RichTextArea extends AbstractField<String>
+ implements LegacyComponent {
+
+ /**
+ * Null representation.
+ */
+ private String nullRepresentation = "null";
+
+ /**
+ * Is setting to null from non-null value allowed by setting with null
+ * representation .
+ */
+ private boolean nullSettingAllowed = false;
+
+ /**
+ * Temporary flag that indicates all content will be selected after the next
+ * paint. Reset to false after painted.
+ */
+ private boolean selectAll = false;
+
+ /**
+ * Constructs an empty <code>RichTextArea</code> with no caption.
+ */
+ public RichTextArea() {
+ setValue("");
+ }
+
+ /**
+ *
+ * Constructs an empty <code>RichTextArea</code> with the given caption.
+ *
+ * @param caption
+ * the caption for the editor.
+ */
+ public RichTextArea(String caption) {
+ this();
+ setCaption(caption);
+ }
+
+ /**
+ * Constructs a new <code>RichTextArea</code> that's bound to the specified
+ * <code>Property</code> and has no caption.
+ *
+ * @param dataSource
+ * the data source for the editor value
+ */
+ public RichTextArea(Property dataSource) {
+ setPropertyDataSource(dataSource);
+ }
+
+ /**
+ * Constructs a new <code>RichTextArea</code> that's bound to the specified
+ * <code>Property</code> and has the given caption.
+ *
+ * @param caption
+ * the caption for the editor.
+ * @param dataSource
+ * the data source for the editor value
+ */
+ public RichTextArea(String caption, Property dataSource) {
+ this(dataSource);
+ setCaption(caption);
+ }
+
+ /**
+ * Constructs a new <code>RichTextArea</code> with the given caption and
+ * initial text contents.
+ *
+ * @param caption
+ * the caption for the editor.
+ * @param value
+ * the initial text content of the editor.
+ */
+ public RichTextArea(String caption, String value) {
+ setValue(value);
+ setCaption(caption);
+ }
+
+ @Override
+ public void paintContent(PaintTarget target) throws PaintException {
+ if (selectAll) {
+ target.addAttribute("selectAll", true);
+ selectAll = false;
+ }
+
+ // Adds the content as variable
+ String value = getValue();
+ if (value == null) {
+ value = getNullRepresentation();
+ }
+ if (value == null) {
+ throw new IllegalStateException(
+ "Null values are not allowed if the null-representation is null");
+ }
+ target.addVariable(this, "text", value);
+
+ }
+
+ @Override
+ public void setReadOnly(boolean readOnly) {
+ super.setReadOnly(readOnly);
+ // IE6 cannot support multi-classname selectors properly
+ // TODO Can be optimized now that support for I6 is dropped
+ if (readOnly) {
+ addStyleName("v-richtextarea-readonly");
+ } else {
+ removeStyleName("v-richtextarea-readonly");
+ }
+ }
+
+ /**
+ * Selects all text in the rich text area. As a side effect, focuses the
+ * rich text area.
+ *
+ * @since 6.5
+ */
+ public void selectAll() {
+ /*
+ * Set selection range functionality is currently being
+ * planned/developed for GWT RTA. Only selecting all is currently
+ * supported. Consider moving selectAll and other selection related
+ * functions to AbstractTextField at that point to share the
+ * implementation. Some third party components extending
+ * AbstractTextField might however not want to support them.
+ */
+ selectAll = true;
+ focus();
+ markAsDirty();
+ }
+
+ @Override
+ public void changeVariables(Object source, Map<String, Object> variables) {
+ // Sets the text
+ if (variables.containsKey("text") && !isReadOnly()) {
+
+ // Only do the setting if the string representation of the value
+ // has been updated
+ String newValue = (String) variables.get("text");
+
+ final String oldValue = getValue();
+ if (newValue != null && (oldValue == null || isNullSettingAllowed())
+ && newValue.equals(getNullRepresentation())) {
+ newValue = null;
+ }
+ if (newValue != oldValue
+ && (newValue == null || !newValue.equals(oldValue))) {
+ boolean wasModified = isModified();
+ setValue(newValue, true);
+
+ // If the modified status changes,
+ // repaint is needed after all.
+ if (wasModified != isModified()) {
+ markAsDirty();
+ }
+ }
+ }
+
+ }
+
+ @Override
+ public Class<String> getType() {
+ return String.class;
+ }
+
+ /**
+ * Gets the null-string representation.
+ *
+ * <p>
+ * The null-valued strings are represented on the user interface by
+ * replacing the null value with this string. If the null representation is
+ * set null (not 'null' string), painting null value throws exception.
+ * </p>
+ *
+ * <p>
+ * The default value is string 'null'.
+ * </p>
+ *
+ * @return the String Textual representation for null strings.
+ * @see TextField#isNullSettingAllowed()
+ */
+ public String getNullRepresentation() {
+ return nullRepresentation;
+ }
+
+ /**
+ * Is setting nulls with null-string representation allowed.
+ *
+ * <p>
+ * If this property is true, writing null-representation string to text
+ * field always sets the field value to real null. If this property is
+ * false, null setting is not made, but the null values are maintained.
+ * Maintenance of null-values is made by only converting the textfield
+ * contents to real null, if the text field matches the null-string
+ * representation and the current value of the field is null.
+ * </p>
+ *
+ * <p>
+ * By default this setting is false
+ * </p>
+ *
+ * @return boolean Should the null-string represenation be always converted
+ * to null-values.
+ * @see TextField#getNullRepresentation()
+ */
+ public boolean isNullSettingAllowed() {
+ return nullSettingAllowed;
+ }
+
+ /**
+ * Sets the null-string representation.
+ *
+ * <p>
+ * The null-valued strings are represented on the user interface by
+ * replacing the null value with this string. If the null representation is
+ * set null (not 'null' string), painting null value throws exception.
+ * </p>
+ *
+ * <p>
+ * The default value is string 'null'
+ * </p>
+ *
+ * @param nullRepresentation
+ * Textual representation for null strings.
+ * @see TextField#setNullSettingAllowed(boolean)
+ */
+ public void setNullRepresentation(String nullRepresentation) {
+ this.nullRepresentation = nullRepresentation;
+ }
+
+ /**
+ * Sets the null conversion mode.
+ *
+ * <p>
+ * If this property is true, writing null-representation string to text
+ * field always sets the field value to real null. If this property is
+ * false, null setting is not made, but the null values are maintained.
+ * Maintenance of null-values is made by only converting the textfield
+ * contents to real null, if the text field matches the null-string
+ * representation and the current value of the field is null.
+ * </p>
+ *
+ * <p>
+ * By default this setting is false.
+ * </p>
+ *
+ * @param nullSettingAllowed
+ * Should the null-string represenation be always converted to
+ * null-values.
+ * @see TextField#getNullRepresentation()
+ */
+ public void setNullSettingAllowed(boolean nullSettingAllowed) {
+ this.nullSettingAllowed = nullSettingAllowed;
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return super.isEmpty() || getValue().length() == 0;
+ }
+
+ @Override
+ public void clear() {
+ setValue("");
+ }
+
+ @Override
+ public void readDesign(Element design, DesignContext designContext) {
+ super.readDesign(design, designContext);
+ setValue(design.html(), false, true);
+ }
+
+ @Override
+ public void writeDesign(Element design, DesignContext designContext) {
+ super.writeDesign(design, designContext);
+ design.html(getValue());
+ }
+
+ @Override
+ protected RichTextAreaState getState() {
+ return (RichTextAreaState) super.getState();
+ }
+}
diff --git a/compatibility-server/src/main/java/com/vaadin/v7/ui/Select.java b/compatibility-server/src/main/java/com/vaadin/v7/ui/Select.java
new file mode 100644
index 0000000000..2710287445
--- /dev/null
+++ b/compatibility-server/src/main/java/com/vaadin/v7/ui/Select.java
@@ -0,0 +1,60 @@
+/*
+ * 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.v7.ui;
+
+import java.util.Collection;
+
+import com.vaadin.v7.data.Container;
+
+/**
+ * <p>
+ * A class representing a selection of items the user has selected in a UI. The
+ * set of choices is presented as a set of {@link com.vaadin.v7.data.Item}s in a
+ * {@link com.vaadin.v7.data.Container}.
+ * </p>
+ *
+ * <p>
+ * A <code>Select</code> component may be in single- or multiselect mode.
+ * Multiselect mode means that more than one item can be selected
+ * simultaneously.
+ * </p>
+ *
+ * @author Vaadin Ltd.
+ * @since 3.0
+ * @deprecated As of 7.0. Use {@link ComboBox} instead.
+ */
+@Deprecated
+public class Select extends ComboBox {
+ /* Component methods */
+
+ public Select() {
+ super();
+ }
+
+ public Select(String caption, Collection<?> options) {
+ super(caption, options);
+ }
+
+ public Select(String caption, Container dataSource) {
+ super(caption, dataSource);
+ }
+
+ public Select(String caption) {
+ super(caption);
+ }
+
+}
diff --git a/compatibility-server/src/main/java/com/vaadin/v7/ui/Table.java b/compatibility-server/src/main/java/com/vaadin/v7/ui/Table.java
new file mode 100644
index 0000000000..760940a482
--- /dev/null
+++ b/compatibility-server/src/main/java/com/vaadin/v7/ui/Table.java
@@ -0,0 +1,6536 @@
+/*
+ * 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.v7.ui;
+
+import java.io.Serializable;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.StringTokenizer;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import org.jsoup.nodes.Element;
+import org.jsoup.select.Elements;
+
+import com.vaadin.event.Action;
+import com.vaadin.event.Action.Handler;
+import com.vaadin.event.ContextClickEvent;
+import com.vaadin.event.DataBoundTransferable;
+import com.vaadin.event.ItemClickEvent;
+import com.vaadin.event.ItemClickEvent.ItemClickListener;
+import com.vaadin.event.ItemClickEvent.ItemClickNotifier;
+import com.vaadin.event.MouseEvents.ClickEvent;
+import com.vaadin.event.dd.DragAndDropEvent;
+import com.vaadin.event.dd.DragSource;
+import com.vaadin.event.dd.DropHandler;
+import com.vaadin.event.dd.DropTarget;
+import com.vaadin.event.dd.acceptcriteria.ServerSideCriterion;
+import com.vaadin.server.KeyMapper;
+import com.vaadin.server.LegacyCommunicationManager;
+import com.vaadin.server.LegacyPaint;
+import com.vaadin.server.PaintException;
+import com.vaadin.server.PaintTarget;
+import com.vaadin.server.Resource;
+import com.vaadin.shared.MouseEventDetails;
+import com.vaadin.shared.ui.MultiSelectMode;
+import com.vaadin.shared.ui.table.CollapseMenuContent;
+import com.vaadin.shared.ui.table.TableConstants;
+import com.vaadin.shared.ui.table.TableConstants.Section;
+import com.vaadin.shared.ui.table.TableServerRpc;
+import com.vaadin.shared.ui.table.TableState;
+import com.vaadin.shared.util.SharedUtil;
+import com.vaadin.ui.Component;
+import com.vaadin.ui.HasChildMeasurementHint;
+import com.vaadin.ui.HasComponents;
+import com.vaadin.ui.UniqueSerializable;
+import com.vaadin.ui.declarative.DesignAttributeHandler;
+import com.vaadin.ui.declarative.DesignContext;
+import com.vaadin.ui.declarative.DesignException;
+import com.vaadin.ui.declarative.DesignFormatter;
+import com.vaadin.util.ReflectTools;
+import com.vaadin.v7.data.Container;
+import com.vaadin.v7.data.Item;
+import com.vaadin.v7.data.Property;
+import com.vaadin.v7.data.util.ContainerOrderedWrapper;
+import com.vaadin.v7.data.util.IndexedContainer;
+import com.vaadin.v7.data.util.converter.Converter;
+import com.vaadin.v7.data.util.converter.ConverterUtil;
+
+/**
+ * <p>
+ * <code>Table</code> is used for representing data or components in a pageable
+ * and selectable table.
+ * </p>
+ *
+ * <p>
+ * Scalability of the Table is largely dictated by the container. A table does
+ * not have a limit for the number of items and is just as fast with hundreds of
+ * thousands of items as with just a few. The current GWT implementation with
+ * scrolling however limits the number of rows to around 500000, depending on
+ * the browser and the pixel height of rows.
+ * </p>
+ *
+ * <p>
+ * Components in a Table will not have their caption nor icon rendered.
+ * </p>
+ *
+ * @author Vaadin Ltd.
+ * @since 3.0
+ */
+@SuppressWarnings({ "deprecation" })
+public class Table extends AbstractSelect implements Action.Container,
+ Container.Ordered, Container.Sortable, ItemClickNotifier, DragSource,
+ DropTarget, HasComponents, HasChildMeasurementHint {
+
+ private transient Logger logger = null;
+
+ /**
+ * Modes that Table support as drag sourse.
+ */
+ public enum TableDragMode {
+ /**
+ * Table does not start drag and drop events. HTM5 style events started
+ * by browser may still happen.
+ */
+ NONE,
+ /**
+ * Table starts drag with a one row only.
+ */
+ ROW,
+ /**
+ * Table drags selected rows, if drag starts on a selected rows. Else it
+ * starts like in ROW mode. Note, that in Transferable there will still
+ * be only the row on which the drag started, other dragged rows need to
+ * be checked from the source Table.
+ */
+ MULTIROW
+ }
+
+ protected static final int CELL_KEY = 0;
+
+ protected static final int CELL_HEADER = 1;
+
+ protected static final int CELL_ICON = 2;
+
+ protected static final int CELL_ITEMID = 3;
+
+ protected static final int CELL_GENERATED_ROW = 4;
+
+ protected static final int CELL_FIRSTCOL = 5;
+
+ public enum Align {
+ /**
+ * Left column alignment. <b>This is the default behaviour. </b>
+ */
+ LEFT("b"),
+
+ /**
+ * Center column alignment.
+ */
+ CENTER("c"),
+
+ /**
+ * Right column alignment.
+ */
+ RIGHT("e");
+
+ private String alignment;
+
+ private Align(String alignment) {
+ this.alignment = alignment;
+ }
+
+ @Override
+ public String toString() {
+ return alignment;
+ }
+
+ public Align convertStringToAlign(String string) {
+ if (string == null) {
+ return null;
+ }
+ if (string.equals("b")) {
+ return Align.LEFT;
+ } else if (string.equals("c")) {
+ return Align.CENTER;
+ } else if (string.equals("e")) {
+ return Align.RIGHT;
+ } else {
+ return null;
+ }
+ }
+ }
+
+ /**
+ * @deprecated As of 7.0, use {@link Align#LEFT} instead
+ */
+ @Deprecated
+ public static final Align ALIGN_LEFT = Align.LEFT;
+
+ /**
+ * @deprecated As of 7.0, use {@link Align#CENTER} instead
+ */
+ @Deprecated
+ public static final Align ALIGN_CENTER = Align.CENTER;
+
+ /**
+ * @deprecated As of 7.0, use {@link Align#RIGHT} instead
+ */
+ @Deprecated
+ public static final Align ALIGN_RIGHT = Align.RIGHT;
+
+ public enum ColumnHeaderMode {
+ /**
+ * Column headers are hidden.
+ */
+ HIDDEN,
+ /**
+ * Property ID:s are used as column headers.
+ */
+ ID,
+ /**
+ * Column headers are explicitly specified with
+ * {@link #setColumnHeaders(String[])}.
+ */
+ EXPLICIT,
+ /**
+ * Column headers are explicitly specified with
+ * {@link #setColumnHeaders(String[])}. If a header is not specified for
+ * a given property, its property id is used instead.
+ * <p>
+ * <b>This is the default behavior. </b>
+ */
+ EXPLICIT_DEFAULTS_ID
+ }
+
+ /**
+ * @deprecated As of 7.0, use {@link ColumnHeaderMode#HIDDEN} instead
+ */
+ @Deprecated
+ public static final ColumnHeaderMode COLUMN_HEADER_MODE_HIDDEN = ColumnHeaderMode.HIDDEN;
+
+ /**
+ * @deprecated As of 7.0, use {@link ColumnHeaderMode#ID} instead
+ */
+ @Deprecated
+ public static final ColumnHeaderMode COLUMN_HEADER_MODE_ID = ColumnHeaderMode.ID;
+
+ /**
+ * @deprecated As of 7.0, use {@link ColumnHeaderMode#EXPLICIT} instead
+ */
+ @Deprecated
+ public static final ColumnHeaderMode COLUMN_HEADER_MODE_EXPLICIT = ColumnHeaderMode.EXPLICIT;
+
+ /**
+ * @deprecated As of 7.0, use {@link ColumnHeaderMode#EXPLICIT_DEFAULTS_ID}
+ * instead
+ */
+ @Deprecated
+ public static final ColumnHeaderMode COLUMN_HEADER_MODE_EXPLICIT_DEFAULTS_ID = ColumnHeaderMode.EXPLICIT_DEFAULTS_ID;
+
+ public enum RowHeaderMode {
+ /**
+ * Row caption mode: The row headers are hidden. <b>This is the default
+ * mode. </b>
+ */
+ HIDDEN(null),
+ /**
+ * Row caption mode: Items Id-objects toString is used as row caption.
+ */
+ ID(ItemCaptionMode.ID),
+ /**
+ * Row caption mode: Item-objects toString is used as row caption.
+ */
+ ITEM(ItemCaptionMode.ITEM),
+ /**
+ * Row caption mode: Index of the item is used as item caption. The
+ * index mode can only be used with the containers implementing the
+ * {@link com.vaadin.data.Container.Indexed} interface.
+ */
+ INDEX(ItemCaptionMode.INDEX),
+ /**
+ * Row caption mode: Item captions are explicitly specified, but if the
+ * caption is missing, the item id objects <code>toString()</code> is
+ * used instead.
+ */
+ EXPLICIT_DEFAULTS_ID(ItemCaptionMode.EXPLICIT_DEFAULTS_ID),
+ /**
+ * Row caption mode: Item captions are explicitly specified.
+ */
+ EXPLICIT(ItemCaptionMode.EXPLICIT),
+ /**
+ * Row caption mode: Only icons are shown, the captions are hidden.
+ */
+ ICON_ONLY(ItemCaptionMode.ICON_ONLY),
+ /**
+ * Row caption mode: Item captions are read from property specified with
+ * {@link #setItemCaptionPropertyId(Object)} .
+ */
+ PROPERTY(ItemCaptionMode.PROPERTY);
+
+ ItemCaptionMode mode;
+
+ private RowHeaderMode(ItemCaptionMode mode) {
+ this.mode = mode;
+ }
+
+ public ItemCaptionMode getItemCaptionMode() {
+ return mode;
+ }
+ }
+
+ /**
+ * @deprecated As of 7.0, use {@link RowHeaderMode#HIDDEN} instead
+ */
+ @Deprecated
+ public static final RowHeaderMode ROW_HEADER_MODE_HIDDEN = RowHeaderMode.HIDDEN;
+
+ /**
+ * @deprecated As of 7.0, use {@link RowHeaderMode#ID} instead
+ */
+ @Deprecated
+ public static final RowHeaderMode ROW_HEADER_MODE_ID = RowHeaderMode.ID;
+
+ /**
+ * @deprecated As of 7.0, use {@link RowHeaderMode#ITEM} instead
+ */
+ @Deprecated
+ public static final RowHeaderMode ROW_HEADER_MODE_ITEM = RowHeaderMode.ITEM;
+
+ /**
+ * @deprecated As of 7.0, use {@link RowHeaderMode#INDEX} instead
+ */
+ @Deprecated
+ public static final RowHeaderMode ROW_HEADER_MODE_INDEX = RowHeaderMode.INDEX;
+
+ /**
+ * @deprecated As of 7.0, use {@link RowHeaderMode#EXPLICIT_DEFAULTS_ID}
+ * instead
+ */
+ @Deprecated
+ public static final RowHeaderMode ROW_HEADER_MODE_EXPLICIT_DEFAULTS_ID = RowHeaderMode.EXPLICIT_DEFAULTS_ID;
+
+ /**
+ * @deprecated As of 7.0, use {@link RowHeaderMode#EXPLICIT} instead
+ */
+ @Deprecated
+ public static final RowHeaderMode ROW_HEADER_MODE_EXPLICIT = RowHeaderMode.EXPLICIT;
+
+ /**
+ * @deprecated As of 7.0, use {@link RowHeaderMode#ICON_ONLY} instead
+ */
+ @Deprecated
+ public static final RowHeaderMode ROW_HEADER_MODE_ICON_ONLY = RowHeaderMode.ICON_ONLY;
+
+ /**
+ * @deprecated As of 7.0, use {@link RowHeaderMode#PROPERTY} instead
+ */
+ @Deprecated
+ public static final RowHeaderMode ROW_HEADER_MODE_PROPERTY = RowHeaderMode.PROPERTY;
+
+ /**
+ * The default rate that table caches rows for smooth scrolling.
+ */
+ private static final double CACHE_RATE_DEFAULT = 2;
+
+ private static final String ROW_HEADER_COLUMN_KEY = "0";
+ private static final Object ROW_HEADER_FAKE_PROPERTY_ID = new UniqueSerializable() {
+ };
+
+ /**
+ * How layout manager should behave when measuring Table's child components
+ */
+ private ChildMeasurementHint childMeasurementHint = ChildMeasurementHint.MEASURE_ALWAYS;
+
+ /* Private table extensions to Select */
+
+ /**
+ * True if column collapsing is allowed.
+ */
+ private boolean columnCollapsingAllowed = false;
+
+ /**
+ * True if reordering of columns is allowed on the client side.
+ */
+ private boolean columnReorderingAllowed = false;
+
+ /**
+ * Keymapper for column ids.
+ */
+ private final KeyMapper<Object> columnIdMap = new KeyMapper<Object>();
+
+ /**
+ * Holds visible column propertyIds - in order.
+ */
+ private LinkedList<Object> visibleColumns = new LinkedList<Object>();
+
+ /**
+ * Holds noncollapsible columns.
+ */
+ private HashSet<Object> noncollapsibleColumns = new HashSet<Object>();
+
+ /**
+ * Holds propertyIds of currently collapsed columns.
+ */
+ private final HashSet<Object> collapsedColumns = new HashSet<Object>();
+
+ /**
+ * Holds headers for visible columns (by propertyId).
+ */
+ private final HashMap<Object, String> columnHeaders = new HashMap<Object, String>();
+
+ /**
+ * Holds footers for visible columns (by propertyId).
+ */
+ private final HashMap<Object, String> columnFooters = new HashMap<Object, String>();
+
+ /**
+ * Holds icons for visible columns (by propertyId).
+ */
+ private final HashMap<Object, Resource> columnIcons = new HashMap<Object, Resource>();
+
+ /**
+ * Holds alignments for visible columns (by propertyId).
+ */
+ private HashMap<Object, Align> columnAlignments = new HashMap<Object, Align>();
+
+ /**
+ * Holds column widths in pixels for visible columns (by propertyId).
+ */
+ private final HashMap<Object, Integer> columnWidths = new HashMap<Object, Integer>();
+
+ /**
+ * Holds column expand rations for visible columns (by propertyId).
+ */
+ private final HashMap<Object, Float> columnExpandRatios = new HashMap<Object, Float>();
+
+ /**
+ * Holds column generators
+ */
+ private final HashMap<Object, ColumnGenerator> columnGenerators = new LinkedHashMap<Object, ColumnGenerator>();
+
+ /**
+ * Holds value of property pageLength. 0 disables paging.
+ */
+ private int pageLength = 15;
+
+ /**
+ * Id the first item on the current page.
+ */
+ private Object currentPageFirstItemId = null;
+
+ /*
+ * If all rows get removed then scroll position of the previous container
+ * can be restored after re-adding/replacing rows via addAll(). This
+ * resolves #14581.
+ */
+ private int repairOnReAddAllRowsDataScrollPositionItemIndex = -1;
+
+ /**
+ * Index of the first item on the current page.
+ */
+ private int currentPageFirstItemIndex = 0;
+
+ /**
+ * Index of the "first" item on the last page if a user has used
+ * setCurrentPageFirstItemIndex to scroll down. -1 if not set.
+ */
+ private int currentPageFirstItemIndexOnLastPage = -1;
+
+ /**
+ * Holds value of property selectable.
+ */
+ private Boolean selectable;
+
+ /**
+ * Holds value of property columnHeaderMode.
+ */
+ private ColumnHeaderMode columnHeaderMode = ColumnHeaderMode.EXPLICIT_DEFAULTS_ID;
+
+ /**
+ * Holds value of property rowHeaderMode.
+ */
+ private RowHeaderMode rowHeaderMode = RowHeaderMode.EXPLICIT_DEFAULTS_ID;
+
+ /**
+ * Should the Table footer be visible?
+ */
+ private boolean columnFootersVisible = false;
+
+ /**
+ * Page contents buffer used in buffered mode.
+ */
+ private Object[][] pageBuffer = null;
+
+ /**
+ * Set of properties listened - the list is kept to release the listeners
+ * later.
+ */
+ private HashSet<Property<?>> listenedProperties = null;
+
+ /**
+ * Set of visible components - the is used for needsRepaint calculation.
+ */
+ private HashSet<Component> visibleComponents = null;
+
+ /**
+ * List of action handlers.
+ */
+ private LinkedList<Handler> actionHandlers = null;
+
+ /**
+ * Action mapper.
+ */
+ private KeyMapper<Action> actionMapper = null;
+
+ /**
+ * Table cell editor factory.
+ */
+ private TableFieldFactory fieldFactory = DefaultFieldFactory.get();
+
+ /**
+ * Is table editable.
+ */
+ private boolean editable = false;
+
+ /**
+ * Current sorting direction.
+ */
+ private boolean sortAscending = true;
+
+ /**
+ * Currently table is sorted on this propertyId.
+ */
+ private Object sortContainerPropertyId = null;
+
+ /**
+ * Is table sorting by the user enabled.
+ */
+ private boolean sortEnabled = true;
+
+ /**
+ * Number of rows explicitly requested by the client to be painted on next
+ * paint. This is -1 if no request by the client is made. Painting the
+ * component will automatically reset this to -1.
+ */
+ private int reqRowsToPaint = -1;
+
+ /**
+ * Index of the first rows explicitly requested by the client to be painted.
+ * This is -1 if no request by the client is made. Painting the component
+ * will automatically reset this to -1.
+ */
+ private int reqFirstRowToPaint = -1;
+
+ private int firstToBeRenderedInClient = -1;
+
+ private int lastToBeRenderedInClient = -1;
+
+ private boolean isContentRefreshesEnabled = true;
+
+ private int pageBufferFirstIndex;
+
+ private boolean containerChangeToBeRendered = false;
+
+ /**
+ * Table cell specific style generator
+ */
+ private CellStyleGenerator cellStyleGenerator = null;
+
+ /**
+ * Table cell specific tooltip generator
+ */
+ private ItemDescriptionGenerator itemDescriptionGenerator;
+
+ /*
+ * EXPERIMENTAL feature: will tell the client to re-calculate column widths
+ * if set to true. Currently no setter: extend to enable.
+ */
+ protected boolean alwaysRecalculateColumnWidths = false;
+
+ private double cacheRate = CACHE_RATE_DEFAULT;
+
+ private TableDragMode dragMode = TableDragMode.NONE;
+
+ private DropHandler dropHandler;
+
+ private MultiSelectMode multiSelectMode = MultiSelectMode.DEFAULT;
+
+ private boolean rowCacheInvalidated;
+
+ private RowGenerator rowGenerator = null;
+
+ private final Map<Field<?>, Property<?>> associatedProperties = new HashMap<Field<?>, Property<?>>();
+
+ private boolean painted = false;
+
+ private HashMap<Object, Converter<String, Object>> propertyValueConverters = new HashMap<Object, Converter<String, Object>>();
+
+ /**
+ * Set to true if the client-side should be informed that the key mapper has
+ * been reset so it can avoid sending back references to keys that are no
+ * longer present.
+ */
+ private boolean keyMapperReset;
+
+ private List<Throwable> exceptionsDuringCachePopulation = new ArrayList<Throwable>();
+
+ private boolean isBeingPainted;
+
+ /* Table constructors */
+
+ /**
+ * Creates a new empty table.
+ */
+ public Table() {
+ setRowHeaderMode(ROW_HEADER_MODE_HIDDEN);
+
+ registerRpc(new TableServerRpc() {
+
+ @Override
+ public void contextClick(String rowKey, String colKey,
+ Section section, MouseEventDetails details) {
+ Object itemId = itemIdMapper.get(rowKey);
+ Object propertyId = columnIdMap.get(colKey);
+ fireEvent(new TableContextClickEvent(Table.this, details,
+ itemId, propertyId, section));
+ }
+ });
+ }
+
+ /**
+ * Creates a new empty table with caption.
+ *
+ * @param caption
+ */
+ public Table(String caption) {
+ this();
+ setCaption(caption);
+ }
+
+ /**
+ * Creates a new table with caption and connect it to a Container.
+ *
+ * @param caption
+ * @param dataSource
+ */
+ public Table(String caption, Container dataSource) {
+ this();
+ setCaption(caption);
+ setContainerDataSource(dataSource);
+ }
+
+ /* Table functionality */
+
+ /**
+ * Gets the array of visible column id:s, including generated columns.
+ *
+ * <p>
+ * The columns are show in the order of their appearance in this array.
+ * </p>
+ *
+ * @return an array of currently visible propertyIds and generated column
+ * ids.
+ */
+ public Object[] getVisibleColumns() {
+ if (visibleColumns == null) {
+ return null;
+ }
+ return visibleColumns.toArray();
+ }
+
+ /**
+ * Sets the array of visible column property id:s.
+ *
+ * <p>
+ * The columns are show in the order of their appearance in this array.
+ * </p>
+ *
+ * @param visibleColumns
+ * the Array of shown property id:s.
+ */
+ public void setVisibleColumns(Object... visibleColumns) {
+
+ // Visible columns must exist
+ if (visibleColumns == null) {
+ throw new NullPointerException(
+ "Can not set visible columns to null value");
+ }
+
+ final LinkedList<Object> newVC = new LinkedList<Object>();
+
+ // Checks that the new visible columns contains no nulls, properties
+ // exist and that there are no duplicates before adding them to newVC.
+ final Collection<?> properties = getContainerPropertyIds();
+ for (int i = 0; i < visibleColumns.length; i++) {
+ if (visibleColumns[i] == null) {
+ throw new NullPointerException("Ids must be non-nulls");
+ } else if (!properties.contains(visibleColumns[i])
+ && !columnGenerators.containsKey(visibleColumns[i])) {
+ throw new IllegalArgumentException(
+ "Ids must exist in the Container or as a generated column, missing id: "
+ + visibleColumns[i]);
+ } else if (newVC.contains(visibleColumns[i])) {
+ throw new IllegalArgumentException(
+ "Ids must be unique, duplicate id: "
+ + visibleColumns[i]);
+ } else {
+ newVC.add(visibleColumns[i]);
+ }
+ }
+
+ this.visibleColumns = newVC;
+
+ // Assures visual refresh
+ refreshRowCache();
+ }
+
+ /**
+ * Gets the headers of the columns.
+ *
+ * <p>
+ * The headers match the property id:s given by the set visible column
+ * headers. The table must be set in either
+ * {@link #COLUMN_HEADER_MODE_EXPLICIT} or
+ * {@link #COLUMN_HEADER_MODE_EXPLICIT_DEFAULTS_ID} mode to show the
+ * headers. In the defaults mode any nulls in the headers array are replaced
+ * with id.toString().
+ * </p>
+ *
+ * @return the Array of column headers.
+ */
+ public String[] getColumnHeaders() {
+ if (columnHeaders == null) {
+ return null;
+ }
+ final String[] headers = new String[visibleColumns.size()];
+ int i = 0;
+ for (final Iterator<Object> it = visibleColumns.iterator(); it
+ .hasNext(); i++) {
+ headers[i] = getColumnHeader(it.next());
+ }
+ return headers;
+ }
+
+ /**
+ * Sets the headers of the columns.
+ *
+ * <p>
+ * The headers match the property id:s given by the set visible column
+ * headers. The table must be set in either
+ * {@link #COLUMN_HEADER_MODE_EXPLICIT} or
+ * {@link #COLUMN_HEADER_MODE_EXPLICIT_DEFAULTS_ID} mode to show the
+ * headers. In the defaults mode any nulls in the headers array are replaced
+ * with id.toString() outputs when rendering.
+ * </p>
+ *
+ * @param columnHeaders
+ * the Array of column headers that match the
+ * {@link #getVisibleColumns()} method.
+ */
+ public void setColumnHeaders(String... columnHeaders) {
+
+ if (columnHeaders.length != visibleColumns.size()) {
+ throw new IllegalArgumentException(
+ "The length of the headers array must match the number of visible columns");
+ }
+
+ this.columnHeaders.clear();
+ int i = 0;
+ for (final Iterator<Object> it = visibleColumns.iterator(); it.hasNext()
+ && i < columnHeaders.length; i++) {
+ this.columnHeaders.put(it.next(), columnHeaders[i]);
+ }
+
+ markAsDirty();
+ }
+
+ /**
+ * Gets the icons of the columns.
+ *
+ * <p>
+ * The icons in headers match the property id:s given by the set visible
+ * column headers. The table must be set in either
+ * {@link #COLUMN_HEADER_MODE_EXPLICIT} or
+ * {@link #COLUMN_HEADER_MODE_EXPLICIT_DEFAULTS_ID} mode to show the headers
+ * with icons.
+ * </p>
+ *
+ * @return the Array of icons that match the {@link #getVisibleColumns()}.
+ */
+ public Resource[] getColumnIcons() {
+ if (columnIcons == null) {
+ return null;
+ }
+ final Resource[] icons = new Resource[visibleColumns.size()];
+ int i = 0;
+ for (final Iterator<Object> it = visibleColumns.iterator(); it
+ .hasNext(); i++) {
+ icons[i] = columnIcons.get(it.next());
+ }
+
+ return icons;
+ }
+
+ /**
+ * Sets the icons of the columns.
+ *
+ * <p>
+ * The icons in headers match the property id:s given by the set visible
+ * column headers. The table must be set in either
+ * {@link #COLUMN_HEADER_MODE_EXPLICIT} or
+ * {@link #COLUMN_HEADER_MODE_EXPLICIT_DEFAULTS_ID} mode to show the headers
+ * with icons.
+ * </p>
+ *
+ * @param columnIcons
+ * the Array of icons that match the {@link #getVisibleColumns()}
+ * .
+ */
+ public void setColumnIcons(Resource... columnIcons) {
+
+ if (columnIcons.length != visibleColumns.size()) {
+ throw new IllegalArgumentException(
+ "The length of the icons array must match the number of visible columns");
+ }
+
+ this.columnIcons.clear();
+ int i = 0;
+ for (final Iterator<Object> it = visibleColumns.iterator(); it.hasNext()
+ && i < columnIcons.length; i++) {
+ this.columnIcons.put(it.next(), columnIcons[i]);
+ }
+
+ markAsDirty();
+ }
+
+ /**
+ * Gets the array of column alignments.
+ *
+ * <p>
+ * The items in the array must match the properties identified by
+ * {@link #getVisibleColumns()}. The possible values for the alignments
+ * include:
+ * <ul>
+ * <li>{@link Align#LEFT}: Left alignment</li>
+ * <li>{@link Align#CENTER}: Centered</li>
+ * <li>{@link Align#RIGHT}: Right alignment</li>
+ * </ul>
+ * The alignments default to {@link Align#LEFT}: any null values are
+ * rendered as align lefts.
+ * </p>
+ *
+ * @return the Column alignments array.
+ */
+ public Align[] getColumnAlignments() {
+ if (columnAlignments == null) {
+ return null;
+ }
+ final Align[] alignments = new Align[visibleColumns.size()];
+ int i = 0;
+ for (final Iterator<Object> it = visibleColumns.iterator(); it
+ .hasNext(); i++) {
+ alignments[i] = getColumnAlignment(it.next());
+ }
+
+ return alignments;
+ }
+
+ /**
+ * Sets the column alignments.
+ *
+ * <p>
+ * The amount of items in the array must match the amount of properties
+ * identified by {@link #getVisibleColumns()}. The possible values for the
+ * alignments include:
+ * <ul>
+ * <li>{@link Align#LEFT}: Left alignment</li>
+ * <li>{@link Align#CENTER}: Centered</li>
+ * <li>{@link Align#RIGHT}: Right alignment</li>
+ * </ul>
+ * The alignments default to {@link Align#LEFT}
+ * </p>
+ *
+ * @param columnAlignments
+ * the Column alignments array.
+ */
+ public void setColumnAlignments(Align... columnAlignments) {
+
+ if (columnAlignments.length != visibleColumns.size()) {
+ throw new IllegalArgumentException(
+ "The length of the alignments array must match the number of visible columns");
+ }
+
+ // Resets the alignments
+ final HashMap<Object, Align> newCA = new HashMap<Object, Align>();
+ int i = 0;
+ for (final Iterator<Object> it = visibleColumns.iterator(); it.hasNext()
+ && i < columnAlignments.length; i++) {
+ newCA.put(it.next(), columnAlignments[i]);
+ }
+ this.columnAlignments = newCA;
+
+ // Assures the visual refresh. No need to reset the page buffer before
+ // as the content has not changed, only the alignments.
+ refreshRenderedCells();
+ }
+
+ /**
+ * Sets columns width (in pixels). Theme may not necessarily respect very
+ * small or very big values. Setting width to -1 (default) means that theme
+ * will make decision of width.
+ *
+ * <p>
+ * Column can either have a fixed width or expand ratio. The latter one set
+ * is used. See @link {@link #setColumnExpandRatio(Object, float)}.
+ *
+ * @param propertyId
+ * columns property id
+ * @param width
+ * width to be reserved for columns content
+ * @since 4.0.3
+ */
+ public void setColumnWidth(Object propertyId, int width) {
+ if (propertyId == null) {
+ // Since propertyId is null, this is the row header. Use the magic
+ // id to store the width of the row header.
+ propertyId = ROW_HEADER_FAKE_PROPERTY_ID;
+ }
+
+ // Setting column width should remove any expand ratios as well
+ columnExpandRatios.remove(propertyId);
+
+ if (width < 0) {
+ columnWidths.remove(propertyId);
+ } else {
+ columnWidths.put(propertyId, width);
+ }
+ markAsDirty();
+ }
+
+ /**
+ * Sets the column expand ratio for given column.
+ * <p>
+ * Expand ratios can be defined to customize the way how excess space is
+ * divided among columns. Table can have excess space if it has its width
+ * defined and there is horizontally more space than columns consume
+ * naturally. Excess space is the space that is not used by columns with
+ * explicit width (see {@link #setColumnWidth(Object, int)}) or with natural
+ * width (no width nor expand ratio).
+ *
+ * <p>
+ * By default (without expand ratios) the excess space is divided
+ * proportionally to columns natural widths.
+ *
+ * <p>
+ * Only expand ratios of visible columns are used in final calculations.
+ *
+ * <p>
+ * Column can either have a fixed width or expand ratio. The latter one set
+ * is used.
+ *
+ * <p>
+ * A column with expand ratio is considered to be minimum width by default
+ * (if no excess space exists). The minimum width is defined by terminal
+ * implementation.
+ *
+ * <p>
+ * If terminal implementation supports re-sizable columns the column becomes
+ * fixed width column if users resizes the column.
+ *
+ * @param propertyId
+ * columns property id
+ * @param expandRatio
+ * the expandRatio used to divide excess space for this column
+ */
+ public void setColumnExpandRatio(Object propertyId, float expandRatio) {
+ if (propertyId == null) {
+ // Since propertyId is null, this is the row header. Use the magic
+ // id to store the width of the row header.
+ propertyId = ROW_HEADER_FAKE_PROPERTY_ID;
+ }
+
+ // Setting the column expand ratio should remove and defined column
+ // width
+ columnWidths.remove(propertyId);
+
+ if (expandRatio < 0) {
+ columnExpandRatios.remove(propertyId);
+ } else {
+ columnExpandRatios.put(propertyId, expandRatio);
+ }
+
+ requestRepaint();
+ }
+
+ /**
+ * Gets the column expand ratio for a column. See
+ * {@link #setColumnExpandRatio(Object, float)}
+ *
+ * @param propertyId
+ * columns property id
+ * @return the expandRatio used to divide excess space for this column
+ */
+ public float getColumnExpandRatio(Object propertyId) {
+ final Float width = columnExpandRatios.get(propertyId);
+ if (width == null) {
+ return -1;
+ }
+ return width.floatValue();
+ }
+
+ /**
+ * Gets the pixel width of column
+ *
+ * @param propertyId
+ * @return width of column or -1 when value not set
+ */
+ public int getColumnWidth(Object propertyId) {
+ if (propertyId == null) {
+ // Since propertyId is null, this is the row header. Use the magic
+ // id to retrieve the width of the row header.
+ propertyId = ROW_HEADER_FAKE_PROPERTY_ID;
+ }
+ final Integer width = columnWidths.get(propertyId);
+ if (width == null) {
+ return -1;
+ }
+ return width.intValue();
+ }
+
+ /**
+ * Gets the page length.
+ *
+ * <p>
+ * Setting page length 0 disables paging.
+ * </p>
+ *
+ * @return the Length of one page.
+ */
+ public int getPageLength() {
+ return pageLength;
+ }
+
+ /**
+ * Sets the page length.
+ *
+ * <p>
+ * Setting page length 0 disables paging. The page length defaults to 15.
+ * </p>
+ *
+ * <p>
+ * If Table has height set ({@link #setHeight(float, Unit)} ) the client
+ * side may update the page length automatically the correct value.
+ * </p>
+ *
+ * @param pageLength
+ * the length of one page.
+ */
+ public void setPageLength(int pageLength) {
+ if (pageLength >= 0 && this.pageLength != pageLength) {
+ this.pageLength = pageLength;
+ // Assures the visual refresh
+ refreshRowCache();
+ }
+ }
+
+ /**
+ * This method adjusts a possible caching mechanism of table implementation.
+ *
+ * <p>
+ * Table component may fetch and render some rows outside visible area. With
+ * complex tables (for example containing layouts and components), the
+ * client side may become unresponsive. Setting the value lower, UI will
+ * become more responsive. With higher values scrolling in client will hit
+ * server less frequently.
+ *
+ * <p>
+ * The amount of cached rows will be cacheRate multiplied with pageLength (
+ * {@link #setPageLength(int)} both below and above visible area..
+ *
+ * @param cacheRate
+ * a value over 0 (fastest rendering time). Higher value will
+ * cache more rows on server (smoother scrolling). Default value
+ * is 2.
+ */
+ public void setCacheRate(double cacheRate) {
+ if (cacheRate < 0) {
+ throw new IllegalArgumentException(
+ "cacheRate cannot be less than zero");
+ }
+ if (this.cacheRate != cacheRate) {
+ this.cacheRate = cacheRate;
+ markAsDirty();
+ }
+ }
+
+ /**
+ * @see #setCacheRate(double)
+ *
+ * @return the current cache rate value
+ */
+ public double getCacheRate() {
+ return cacheRate;
+ }
+
+ /**
+ * Getter for property currentPageFirstItem.
+ *
+ * @return the Value of property currentPageFirstItem.
+ */
+ public Object getCurrentPageFirstItemId() {
+
+ // Prioritise index over id if indexes are supported
+ if (items instanceof Container.Indexed) {
+ final int index = getCurrentPageFirstItemIndex();
+ Object id = null;
+ if (index >= 0 && index < size()) {
+ id = getIdByIndex(index);
+ }
+ if (id != null && !id.equals(currentPageFirstItemId)) {
+ currentPageFirstItemId = id;
+ }
+ }
+
+ // If there is no item id at all, use the first one
+ if (currentPageFirstItemId == null) {
+ currentPageFirstItemId = firstItemId();
+ }
+
+ return currentPageFirstItemId;
+ }
+
+ /**
+ * Returns the item ID for the item represented by the index given. Assumes
+ * that the current container implements {@link Container.Indexed}.
+ *
+ * See {@link Container.Indexed#getIdByIndex(int)} for more information
+ * about the exceptions that can be thrown.
+ *
+ * @param index
+ * the index for which the item ID should be fetched
+ * @return the item ID for the given index
+ *
+ * @throws ClassCastException
+ * if container does not implement {@link Container.Indexed}
+ * @throws IndexOutOfBoundsException
+ * thrown by {@link Container.Indexed#getIdByIndex(int)} if the
+ * index is invalid
+ */
+ protected Object getIdByIndex(int index) {
+ return ((Container.Indexed) items).getIdByIndex(index);
+ }
+
+ /**
+ * Setter for property currentPageFirstItemId.
+ *
+ * @param currentPageFirstItemId
+ * the New value of property currentPageFirstItemId.
+ */
+ public void setCurrentPageFirstItemId(Object currentPageFirstItemId) {
+
+ // Gets the corresponding index
+ int index = -1;
+ if (items instanceof Container.Indexed) {
+ index = indexOfId(currentPageFirstItemId);
+ } else {
+ // If the table item container does not have index, we have to
+ // calculates the index by hand
+ Object id = firstItemId();
+ while (id != null && !id.equals(currentPageFirstItemId)) {
+ index++;
+ id = nextItemId(id);
+ }
+ if (id == null) {
+ index = -1;
+ }
+ }
+
+ // If the search for item index was successful
+ if (index >= 0) {
+ /*
+ * The table is not capable of displaying an item in the container
+ * as the first if there are not enough items following the selected
+ * item so the whole table (pagelength) is filled.
+ */
+ int maxIndex = size() - pageLength;
+ if (maxIndex < 0) {
+ maxIndex = 0;
+ }
+
+ if (index > maxIndex) {
+ // Note that we pass index, not maxIndex, letting
+ // setCurrentPageFirstItemIndex handle the situation.
+ setCurrentPageFirstItemIndex(index);
+ return;
+ }
+
+ this.currentPageFirstItemId = currentPageFirstItemId;
+ currentPageFirstItemIndex = index;
+ }
+
+ // Assures the visual refresh
+ refreshRowCache();
+
+ }
+
+ protected int indexOfId(Object itemId) {
+ return ((Container.Indexed) items).indexOfId(itemId);
+ }
+
+ /**
+ * Gets the icon Resource for the specified column.
+ *
+ * @param propertyId
+ * the propertyId identifying the column.
+ * @return the icon for the specified column; null if the column has no icon
+ * set, or if the column is not visible.
+ */
+ public Resource getColumnIcon(Object propertyId) {
+ return columnIcons.get(propertyId);
+ }
+
+ /**
+ * Sets the icon Resource for the specified column.
+ * <p>
+ * Throws IllegalArgumentException if the specified column is not visible.
+ * </p>
+ *
+ * @param propertyId
+ * the propertyId identifying the column.
+ * @param icon
+ * the icon Resource to set.
+ */
+ public void setColumnIcon(Object propertyId, Resource icon) {
+
+ if (icon == null) {
+ columnIcons.remove(propertyId);
+ } else {
+ columnIcons.put(propertyId, icon);
+ }
+
+ markAsDirty();
+ }
+
+ /**
+ * Gets the header for the specified column.
+ *
+ * @param propertyId
+ * the propertyId identifying the column.
+ * @return the header for the specified column if it has one.
+ */
+ public String getColumnHeader(Object propertyId) {
+ if (getColumnHeaderMode() == ColumnHeaderMode.HIDDEN) {
+ return null;
+ }
+
+ String header = columnHeaders.get(propertyId);
+ if ((header == null
+ && getColumnHeaderMode() == ColumnHeaderMode.EXPLICIT_DEFAULTS_ID)
+ || getColumnHeaderMode() == ColumnHeaderMode.ID) {
+ header = propertyId.toString();
+ }
+
+ return header;
+ }
+
+ /**
+ * Sets the column header for the specified column;
+ *
+ * @param propertyId
+ * the propertyId identifying the column.
+ * @param header
+ * the header to set.
+ */
+ public void setColumnHeader(Object propertyId, String header) {
+
+ if (header == null) {
+ columnHeaders.remove(propertyId);
+ } else {
+ columnHeaders.put(propertyId, header);
+ }
+
+ markAsDirty();
+ }
+
+ /**
+ * Gets the specified column's alignment.
+ *
+ * @param propertyId
+ * the propertyID identifying the column.
+ * @return the specified column's alignment if it as one; {@link Align#LEFT}
+ * otherwise.
+ */
+ public Align getColumnAlignment(Object propertyId) {
+ final Align a = columnAlignments.get(propertyId);
+ return a == null ? Align.LEFT : a;
+ }
+
+ /**
+ * Sets the specified column's alignment.
+ *
+ * <p>
+ * Throws IllegalArgumentException if the alignment is not one of the
+ * following: {@link Align#LEFT}, {@link Align#CENTER} or
+ * {@link Align#RIGHT}
+ * </p>
+ *
+ * @param propertyId
+ * the propertyID identifying the column.
+ * @param alignment
+ * the desired alignment.
+ */
+ public void setColumnAlignment(Object propertyId, Align alignment) {
+ if (alignment == null || alignment == Align.LEFT) {
+ columnAlignments.remove(propertyId);
+ } else {
+ columnAlignments.put(propertyId, alignment);
+ }
+
+ // Assures the visual refresh. No need to reset the page buffer before
+ // as the content has not changed, only the alignments.
+ refreshRenderedCells();
+ }
+
+ /**
+ * Checks if the specified column is collapsed.
+ *
+ * @param propertyId
+ * the propertyID identifying the column.
+ * @return true if the column is collapsed; false otherwise;
+ */
+ public boolean isColumnCollapsed(Object propertyId) {
+ return collapsedColumns != null
+ && collapsedColumns.contains(propertyId);
+ }
+
+ /**
+ * Sets whether the specified column is collapsed or not.
+ *
+ *
+ * @param propertyId
+ * the propertyID identifying the column.
+ * @param collapsed
+ * the desired collapsedness.
+ * @throws IllegalStateException
+ * if column collapsing is not allowed
+ * @throws IllegalArgumentException
+ * if the property id does not exist
+ */
+ public void setColumnCollapsed(Object propertyId, boolean collapsed)
+ throws IllegalStateException {
+ if (!isColumnCollapsingAllowed()) {
+ throw new IllegalStateException("Column collapsing not allowed!");
+ }
+ if (collapsed && noncollapsibleColumns.contains(propertyId)) {
+ throw new IllegalStateException("The column is noncollapsible!");
+ }
+ if (!getContainerPropertyIds().contains(propertyId)
+ && !columnGenerators.containsKey(propertyId)) {
+ throw new IllegalArgumentException("Property '" + propertyId
+ + "' was not found in the container");
+ }
+
+ if (collapsed) {
+ if (collapsedColumns.add(propertyId)) {
+ fireColumnCollapseEvent(propertyId);
+ }
+ } else {
+ if (collapsedColumns.remove(propertyId)) {
+ fireColumnCollapseEvent(propertyId);
+ }
+ }
+
+ // Assures the visual refresh
+ refreshRowCache();
+ }
+
+ /**
+ * Checks if column collapsing is allowed.
+ *
+ * @return true if columns can be collapsed; false otherwise.
+ */
+ public boolean isColumnCollapsingAllowed() {
+ return columnCollapsingAllowed;
+ }
+
+ /**
+ * Sets whether column collapsing is allowed or not.
+ *
+ * @param collapsingAllowed
+ * specifies whether column collapsing is allowed.
+ */
+ public void setColumnCollapsingAllowed(boolean collapsingAllowed) {
+ columnCollapsingAllowed = collapsingAllowed;
+
+ if (!collapsingAllowed) {
+ collapsedColumns.clear();
+ }
+
+ // Assures the visual refresh. No need to reset the page buffer before
+ // as the content has not changed, only the alignments.
+ refreshRenderedCells();
+ }
+
+ /**
+ * Sets whether the given column is collapsible. Note that collapsible
+ * columns can only be actually collapsed (via UI or with
+ * {@link #setColumnCollapsed(Object, boolean) setColumnCollapsed()}) if
+ * {@link #isColumnCollapsingAllowed()} is true. By default all columns are
+ * collapsible.
+ *
+ * @param propertyId
+ * the propertyID identifying the column.
+ * @param collapsible
+ * true if the column should be collapsible, false otherwise.
+ */
+ public void setColumnCollapsible(Object propertyId, boolean collapsible) {
+ if (collapsible) {
+ noncollapsibleColumns.remove(propertyId);
+ } else {
+ noncollapsibleColumns.add(propertyId);
+ collapsedColumns.remove(propertyId);
+ }
+ refreshRowCache();
+ }
+
+ /**
+ * Checks if the given column is collapsible. Note that even if this method
+ * returns <code>true</code>, the column can only be actually collapsed (via
+ * UI or with {@link #setColumnCollapsed(Object, boolean)
+ * setColumnCollapsed()}) if {@link #isColumnCollapsingAllowed()} is also
+ * true.
+ *
+ * @return true if the column can be collapsed; false otherwise.
+ */
+ public boolean isColumnCollapsible(Object propertyId) {
+ return !noncollapsibleColumns.contains(propertyId);
+ }
+
+ /**
+ * Checks if column reordering is allowed.
+ *
+ * @return true if columns can be reordered; false otherwise.
+ */
+ public boolean isColumnReorderingAllowed() {
+ return columnReorderingAllowed;
+ }
+
+ /**
+ * Sets whether column reordering is allowed or not.
+ *
+ * @param columnReorderingAllowed
+ * specifies whether column reordering is allowed.
+ */
+ public void setColumnReorderingAllowed(boolean columnReorderingAllowed) {
+ if (columnReorderingAllowed != this.columnReorderingAllowed) {
+ this.columnReorderingAllowed = columnReorderingAllowed;
+ markAsDirty();
+ }
+ }
+
+ /*
+ * Arranges visible columns according to given columnOrder. Silently ignores
+ * colimnId:s that are not visible columns, and keeps the internal order of
+ * visible columns left out of the ordering (trailing). Silently does
+ * nothing if columnReordering is not allowed.
+ */
+ private void setColumnOrder(Object[] columnOrder) {
+ if (columnOrder == null || !isColumnReorderingAllowed()) {
+ return;
+ }
+ final LinkedList<Object> newOrder = new LinkedList<Object>();
+ for (int i = 0; i < columnOrder.length; i++) {
+ if (columnOrder[i] != null
+ && visibleColumns.contains(columnOrder[i])) {
+ visibleColumns.remove(columnOrder[i]);
+ newOrder.add(columnOrder[i]);
+ }
+ }
+ for (final Iterator<Object> it = visibleColumns.iterator(); it
+ .hasNext();) {
+ final Object columnId = it.next();
+ if (!newOrder.contains(columnId)) {
+ newOrder.add(columnId);
+ }
+ }
+ visibleColumns = newOrder;
+
+ // Assure visual refresh
+ refreshRowCache();
+ }
+
+ /**
+ * Getter for property currentPageFirstItem.
+ *
+ * @return the Value of property currentPageFirstItem.
+ */
+ public int getCurrentPageFirstItemIndex() {
+ return currentPageFirstItemIndex;
+ }
+
+ void setCurrentPageFirstItemIndex(int newIndex,
+ boolean needsPageBufferReset) {
+
+ if (newIndex < 0) {
+ newIndex = 0;
+ }
+
+ /*
+ * minimize Container.size() calls which may be expensive. For example
+ * it may cause sql query.
+ */
+ final int size = size();
+
+ /*
+ * The table is not capable of displaying an item in the container as
+ * the first if there are not enough items following the selected item
+ * so the whole table (pagelength) is filled.
+ */
+ int maxIndex = size - pageLength;
+ if (maxIndex < 0) {
+ maxIndex = 0;
+ }
+
+ /*
+ * If the new index is on the last page we set the index to be the first
+ * item on that last page and make a note of the real index for the
+ * client side to be able to move the scroll position to the correct
+ * position.
+ */
+ int indexOnLastPage = -1;
+ if (newIndex > maxIndex) {
+ indexOnLastPage = newIndex;
+ newIndex = maxIndex;
+ }
+
+ // Refresh first item id
+ if (items instanceof Container.Indexed) {
+ try {
+ currentPageFirstItemId = getIdByIndex(newIndex);
+ } catch (final IndexOutOfBoundsException e) {
+ currentPageFirstItemId = null;
+ }
+ currentPageFirstItemIndex = newIndex;
+
+ if (needsPageBufferReset) {
+ /*
+ * The flag currentPageFirstItemIndexOnLastPage denotes a user
+ * set scrolling position on the last page via
+ * setCurrentPageFirstItemIndex() and shouldn't be changed by
+ * the table component internally changing the firstvisible item
+ * on lazy row fetching. Doing so would make the scrolling
+ * position not be updated correctly when the lazy rows are
+ * finally rendered.
+ */
+
+ boolean isLastRowPossiblyPartiallyVisible = true;
+ if (indexOnLastPage != -1) {
+ /*
+ * If the requested row was greater than maxIndex, the last
+ * row should be fully visible (See
+ * TestCurrentPageFirstItem).
+ */
+ isLastRowPossiblyPartiallyVisible = false;
+ }
+
+ int extraRows = isLastRowPossiblyPartiallyVisible ? 0 : 1;
+ currentPageFirstItemIndexOnLastPage = currentPageFirstItemIndex
+ + extraRows;
+ } else {
+ currentPageFirstItemIndexOnLastPage = -1;
+ }
+
+ } else {
+
+ // For containers not supporting indexes, we must iterate the
+ // container forwards / backwards
+ // next available item forward or backward
+
+ currentPageFirstItemId = firstItemId();
+
+ // Go forwards in the middle of the list (respect borders)
+ while (currentPageFirstItemIndex < newIndex
+ && !isLastId(currentPageFirstItemId)) {
+ currentPageFirstItemIndex++;
+ currentPageFirstItemId = nextItemId(currentPageFirstItemId);
+ }
+
+ // If we did hit the border
+ if (isLastId(currentPageFirstItemId)) {
+ currentPageFirstItemIndex = size - 1;
+ }
+
+ // Go backwards in the middle of the list (respect borders)
+ while (currentPageFirstItemIndex > newIndex
+ && !isFirstId(currentPageFirstItemId)) {
+ currentPageFirstItemIndex--;
+ currentPageFirstItemId = prevItemId(currentPageFirstItemId);
+ }
+
+ // If we did hit the border
+ if (isFirstId(currentPageFirstItemId)) {
+ currentPageFirstItemIndex = 0;
+ }
+
+ // Go forwards once more
+ while (currentPageFirstItemIndex < newIndex
+ && !isLastId(currentPageFirstItemId)) {
+ currentPageFirstItemIndex++;
+ currentPageFirstItemId = nextItemId(currentPageFirstItemId);
+ }
+
+ // If for some reason we do hit border again, override
+ // the user index request
+ if (isLastId(currentPageFirstItemId)) {
+ newIndex = currentPageFirstItemIndex = size - 1;
+ }
+ }
+
+ if (needsPageBufferReset) {
+ // Assures the visual refresh
+ refreshRowCache();
+ }
+ }
+
+ /**
+ * Setter for property currentPageFirstItem.
+ *
+ * @param newIndex
+ * the New value of property currentPageFirstItem.
+ */
+ public void setCurrentPageFirstItemIndex(int newIndex) {
+ setCurrentPageFirstItemIndex(newIndex, true);
+ }
+
+ /**
+ * Returns whether table is selectable.
+ *
+ * <p>
+ * The table is not selectable until it's explicitly set as selectable or at
+ * least one {@link ValueChangeListener} is added.
+ * </p>
+ *
+ * @return whether table is selectable.
+ */
+ public boolean isSelectable() {
+ if (selectable == null) {
+ return hasListeners(ValueChangeEvent.class);
+ }
+ return selectable;
+ }
+
+ /**
+ * Setter for property selectable.
+ *
+ * <p>
+ * The table is not selectable until it's explicitly set as selectable via
+ * this method or alternatively at least one {@link ValueChangeListener} is
+ * added.
+ * </p>
+ *
+ * @param selectable
+ * the New value of property selectable.
+ */
+ public void setSelectable(boolean selectable) {
+ if (!SharedUtil.equals(this.selectable, selectable)) {
+ this.selectable = selectable;
+ markAsDirty();
+ }
+ }
+
+ /**
+ * Getter for property columnHeaderMode.
+ *
+ * @return the Value of property columnHeaderMode.
+ */
+ public ColumnHeaderMode getColumnHeaderMode() {
+ return columnHeaderMode;
+ }
+
+ /**
+ * Setter for property columnHeaderMode.
+ *
+ * @param columnHeaderMode
+ * the New value of property columnHeaderMode.
+ */
+ public void setColumnHeaderMode(ColumnHeaderMode columnHeaderMode) {
+ if (columnHeaderMode == null) {
+ throw new IllegalArgumentException(
+ "Column header mode can not be null");
+ }
+ if (columnHeaderMode != this.columnHeaderMode) {
+ this.columnHeaderMode = columnHeaderMode;
+ markAsDirty();
+ }
+
+ }
+
+ /**
+ * Refreshes the rows in the internal cache. Only if
+ * {@link #resetPageBuffer()} is called before this then all values are
+ * guaranteed to be recreated.
+ */
+ protected void refreshRenderedCells() {
+ if (!isAttached()) {
+ return;
+ }
+
+ if (!isContentRefreshesEnabled) {
+ return;
+ }
+
+ // Collects the basic facts about the table page
+ final int pagelen = getPageLength();
+ int rows, totalRows;
+ rows = totalRows = size();
+ int firstIndex = Math.min(getCurrentPageFirstItemIndex(),
+ totalRows - 1);
+ if (rows > 0 && firstIndex >= 0) {
+ rows -= firstIndex;
+ }
+ if (pagelen > 0 && pagelen < rows) {
+ rows = pagelen;
+ }
+
+ // If "to be painted next" variables are set, use them
+ if (lastToBeRenderedInClient - firstToBeRenderedInClient > 0) {
+ rows = lastToBeRenderedInClient - firstToBeRenderedInClient + 1;
+ }
+ if (firstToBeRenderedInClient >= 0) {
+ if (firstToBeRenderedInClient < totalRows) {
+ firstIndex = firstToBeRenderedInClient;
+ } else {
+ firstIndex = totalRows - 1;
+ }
+ } else {
+ // initial load
+
+ // #8805 send one extra row in the beginning in case a partial
+ // row is shown on the UI
+ if (firstIndex > 0) {
+ firstIndex = firstIndex - 1;
+ rows = rows + 1;
+ }
+ firstToBeRenderedInClient = firstIndex;
+ }
+ if (totalRows > 0) {
+ if (rows + firstIndex > totalRows) {
+ rows = totalRows - firstIndex;
+ }
+ } else {
+ rows = 0;
+ }
+
+ // Saves the results to internal buffer
+ pageBuffer = getVisibleCellsNoCache(firstIndex, rows, true);
+
+ if (rows > 0) {
+ pageBufferFirstIndex = firstIndex;
+ }
+ if (getPageLength() != 0) {
+ removeUnnecessaryRows();
+ }
+
+ setRowCacheInvalidated(true);
+ markAsDirty();
+ maybeThrowCacheUpdateExceptions();
+
+ }
+
+ private void maybeThrowCacheUpdateExceptions() {
+ if (!exceptionsDuringCachePopulation.isEmpty()) {
+ Throwable[] causes = new Throwable[exceptionsDuringCachePopulation
+ .size()];
+ exceptionsDuringCachePopulation.toArray(causes);
+
+ exceptionsDuringCachePopulation.clear();
+ throw new CacheUpdateException(this,
+ "Error during Table cache update.", causes);
+ }
+
+ }
+
+ /**
+ * Exception thrown when one or more exceptions occurred during updating of
+ * the Table cache.
+ * <p>
+ * Contains all exceptions which occurred during the cache update. The first
+ * occurred exception is set as the cause of this exception. All occurred
+ * exceptions can be accessed using {@link #getCauses()}.
+ * </p>
+ *
+ */
+ public static class CacheUpdateException extends RuntimeException {
+ private Throwable[] causes;
+ private Table table;
+
+ public CacheUpdateException(Table table, String message,
+ Throwable[] causes) {
+ super(maybeSupplementMessage(message, causes.length), causes[0]);
+ this.table = table;
+ this.causes = causes;
+ }
+
+ private static String maybeSupplementMessage(String message,
+ int causeCount) {
+ if (causeCount > 1) {
+ return message + " Additional causes not shown.";
+ } else {
+ return message;
+ }
+ }
+
+ /**
+ * Returns the cause(s) for this exception
+ *
+ * @return the exception(s) which caused this exception
+ */
+ public Throwable[] getCauses() {
+ return causes;
+ }
+
+ public Table getTable() {
+ return table;
+ }
+
+ }
+
+ /**
+ * Removes rows that fall outside the required cache.
+ */
+ private void removeUnnecessaryRows() {
+ int minPageBufferIndex = getMinPageBufferIndex();
+ int maxPageBufferIndex = getMaxPageBufferIndex();
+
+ int maxBufferSize = maxPageBufferIndex - minPageBufferIndex + 1;
+
+ /*
+ * Number of rows that were previously cached. This is not necessarily
+ * the same as pageLength if we do not have enough rows in the
+ * container.
+ */
+ int currentlyCachedRowCount = pageBuffer[CELL_ITEMID].length;
+
+ if (currentlyCachedRowCount <= maxBufferSize) {
+ // removal unnecessary
+ return;
+ }
+
+ /* Figure out which rows to get rid of. */
+ int firstCacheRowToRemoveInPageBuffer = -1;
+ if (minPageBufferIndex > pageBufferFirstIndex) {
+ firstCacheRowToRemoveInPageBuffer = pageBufferFirstIndex;
+ } else if (maxPageBufferIndex < pageBufferFirstIndex
+ + currentlyCachedRowCount) {
+ firstCacheRowToRemoveInPageBuffer = maxPageBufferIndex + 1;
+ }
+
+ if (firstCacheRowToRemoveInPageBuffer
+ - pageBufferFirstIndex < currentlyCachedRowCount) {
+ /*
+ * Unregister all components that fall beyond the cache limits after
+ * inserting the new rows.
+ */
+ unregisterComponentsAndPropertiesInRows(
+ firstCacheRowToRemoveInPageBuffer, currentlyCachedRowCount
+ - firstCacheRowToRemoveInPageBuffer);
+ }
+ }
+
+ /**
+ * Requests that the Table should be repainted as soon as possible.
+ *
+ * Note that a {@code Table} does not necessarily repaint its contents when
+ * this method has been called. See {@link #refreshRowCache()} for forcing
+ * an update of the contents.
+ *
+ * @deprecated As of 7.0, use {@link #markAsDirty()} instead
+ */
+
+ @Deprecated
+ @Override
+ public void requestRepaint() {
+ markAsDirty();
+ }
+
+ /**
+ * Requests that the Table should be repainted as soon as possible.
+ *
+ * Note that a {@code Table} does not necessarily repaint its contents when
+ * this method has been called. See {@link #refreshRowCache()} for forcing
+ * an update of the contents.
+ */
+
+ @Override
+ public void markAsDirty() {
+ // Overridden only for javadoc
+ super.markAsDirty();
+ }
+
+ @Override
+ public void markAsDirtyRecursive() {
+ super.markAsDirtyRecursive();
+
+ // Avoid sending a partial repaint (#8714)
+ refreshRowCache();
+ }
+
+ private void removeRowsFromCacheAndFillBottom(int firstIndex, int rows) {
+ int totalCachedRows = pageBuffer[CELL_ITEMID].length;
+ int totalRows = size();
+ int firstIndexInPageBuffer = firstIndex - pageBufferFirstIndex;
+
+ /*
+ * firstIndexInPageBuffer is the first row to be removed. "rows" rows
+ * after that should be removed. If the page buffer does not contain
+ * that many rows, we only remove the rows that actually are in the page
+ * buffer.
+ */
+ if (firstIndexInPageBuffer + rows > totalCachedRows) {
+ rows = totalCachedRows - firstIndexInPageBuffer;
+ }
+
+ /*
+ * Unregister components that will no longer be in the page buffer to
+ * make sure that no components leak.
+ */
+ unregisterComponentsAndPropertiesInRows(firstIndex, rows);
+
+ /*
+ * The number of rows that should be in the cache after this operation
+ * is done (pageBuffer currently contains the expanded items).
+ */
+ int newCachedRowCount = totalCachedRows;
+ if (newCachedRowCount + pageBufferFirstIndex > totalRows) {
+ newCachedRowCount = totalRows - pageBufferFirstIndex;
+ }
+
+ /*
+ * The index at which we should render the first row that does not come
+ * from the previous page buffer.
+ */
+ int firstAppendedRowInPageBuffer = totalCachedRows - rows;
+ int firstAppendedRow = firstAppendedRowInPageBuffer
+ + pageBufferFirstIndex;
+
+ /*
+ * Calculate the maximum number of new rows that we can add to the page
+ * buffer. Less than the rows we removed if the container does not
+ * contain that many items afterwards.
+ */
+ int maxRowsToRender = (totalRows - firstAppendedRow);
+ int rowsToAdd = rows;
+ if (rowsToAdd > maxRowsToRender) {
+ rowsToAdd = maxRowsToRender;
+ }
+
+ Object[][] cells = null;
+ if (rowsToAdd > 0) {
+ cells = getVisibleCellsNoCache(firstAppendedRow, rowsToAdd, false);
+ }
+ /*
+ * Create the new cache buffer by copying the first rows from the old
+ * buffer, moving the following rows upwards and appending more rows if
+ * applicable.
+ */
+ Object[][] newPageBuffer = new Object[pageBuffer.length][newCachedRowCount];
+
+ for (int i = 0; i < pageBuffer.length; i++) {
+ for (int row = 0; row < firstIndexInPageBuffer; row++) {
+ // Copy the first rows
+ newPageBuffer[i][row] = pageBuffer[i][row];
+ }
+ for (int row = firstIndexInPageBuffer; row < firstAppendedRowInPageBuffer; row++) {
+ // Move the rows that were after the expanded rows
+ newPageBuffer[i][row] = pageBuffer[i][row + rows];
+ }
+ for (int row = firstAppendedRowInPageBuffer; row < newCachedRowCount; row++) {
+ // Add the newly rendered rows. Only used if rowsToAdd > 0
+ // (cells != null)
+ newPageBuffer[i][row] = cells[i][row
+ - firstAppendedRowInPageBuffer];
+ }
+ }
+ pageBuffer = newPageBuffer;
+ }
+
+ private Object[][] getVisibleCellsUpdateCacheRows(int firstIndex,
+ int rows) {
+ Object[][] cells = getVisibleCellsNoCache(firstIndex, rows, false);
+ int cacheIx = firstIndex - pageBufferFirstIndex;
+ // update the new rows in the cache.
+ int totalCachedRows = pageBuffer[CELL_ITEMID].length;
+ int end = Math.min(cacheIx + rows, totalCachedRows);
+ for (int ix = cacheIx; ix < end; ix++) {
+ for (int i = 0; i < pageBuffer.length; i++) {
+ pageBuffer[i][ix] = cells[i][ix - cacheIx];
+ }
+ }
+ return cells;
+ }
+
+ /**
+ * @param firstIndex
+ * The position where new rows should be inserted
+ * @param rows
+ * The maximum number of rows that should be inserted at position
+ * firstIndex. Less rows will be inserted if the page buffer is
+ * too small.
+ * @return
+ */
+ private Object[][] getVisibleCellsInsertIntoCache(int firstIndex,
+ int rows) {
+ getLogger().log(Level.FINEST,
+ "Insert {0} rows at index {1} to existing page buffer requested",
+ new Object[] { rows, firstIndex });
+
+ int minPageBufferIndex = getMinPageBufferIndex();
+ int maxPageBufferIndex = getMaxPageBufferIndex();
+
+ int maxBufferSize = maxPageBufferIndex - minPageBufferIndex + 1;
+
+ if (getPageLength() == 0) {
+ // If pageLength == 0 then all rows should be rendered
+ maxBufferSize = pageBuffer[CELL_ITEMID].length + rows;
+ }
+ /*
+ * Number of rows that were previously cached. This is not necessarily
+ * the same as maxBufferSize.
+ */
+ int currentlyCachedRowCount = pageBuffer[CELL_ITEMID].length;
+
+ /* If rows > size available in page buffer */
+ if (firstIndex + rows - 1 > maxPageBufferIndex) {
+ rows = maxPageBufferIndex - firstIndex + 1;
+ }
+
+ /*
+ * "rows" rows will be inserted at firstIndex. Find out how many old
+ * rows fall outside the new buffer so we can unregister components in
+ * the cache.
+ */
+
+ /*
+ * if there are rows before the new pageBuffer limits they must be
+ * removed
+ */
+ int lastCacheRowToRemove = minPageBufferIndex - 1;
+ int rowsFromBeginning = lastCacheRowToRemove - pageBufferFirstIndex + 1;
+ if (lastCacheRowToRemove >= pageBufferFirstIndex) {
+ unregisterComponentsAndPropertiesInRows(pageBufferFirstIndex,
+ rowsFromBeginning);
+ } else {
+ rowsFromBeginning = 0;
+ }
+
+ /*
+ * the rows that fall outside of the new pageBuffer limits after the new
+ * rows are inserted must also be removed
+ */
+ int firstCacheRowToRemove = firstIndex;
+ /*
+ * IF there is space remaining in the buffer after the rows have been
+ * inserted, we can keep more rows.
+ */
+ int numberOfOldRowsAfterInsertedRows = Math.min(
+ pageBufferFirstIndex + currentlyCachedRowCount + rows,
+ maxPageBufferIndex + 1) - (firstIndex + rows - 1);
+ if (numberOfOldRowsAfterInsertedRows > 0) {
+ firstCacheRowToRemove += numberOfOldRowsAfterInsertedRows;
+ }
+ int rowsFromAfter = currentlyCachedRowCount
+ - (firstCacheRowToRemove - pageBufferFirstIndex);
+
+ if (rowsFromAfter > 0) {
+ /*
+ * Unregister all components that fall beyond the cache limits after
+ * inserting the new rows.
+ */
+ unregisterComponentsAndPropertiesInRows(firstCacheRowToRemove,
+ rowsFromAfter);
+ }
+
+ // Calculate the new cache size
+ int newCachedRowCount = maxBufferSize;
+ if (pageBufferFirstIndex + currentlyCachedRowCount + rows
+ - 1 < maxPageBufferIndex) {
+ // there aren't enough rows to fill the whole potential -> use what
+ // there is
+ newCachedRowCount -= maxPageBufferIndex - (pageBufferFirstIndex
+ + currentlyCachedRowCount + rows - 1);
+ } else if (minPageBufferIndex < pageBufferFirstIndex) {
+ newCachedRowCount -= pageBufferFirstIndex - minPageBufferIndex;
+ }
+ /*
+ * calculate the internal location of the new rows within the new cache
+ */
+ int firstIndexInNewPageBuffer = firstIndex - pageBufferFirstIndex
+ - rowsFromBeginning;
+
+ /* Paint the new rows into a separate buffer */
+ Object[][] cells = getVisibleCellsNoCache(firstIndex, rows, false);
+
+ /*
+ * Create the new cache buffer and fill it with the data from the old
+ * buffer as well as the inserted rows.
+ */
+ Object[][] newPageBuffer = new Object[pageBuffer.length][newCachedRowCount];
+
+ for (int i = 0; i < pageBuffer.length; i++) {
+ for (int row = 0; row < firstIndexInNewPageBuffer; row++) {
+ // Copy the first rows
+ newPageBuffer[i][row] = pageBuffer[i][rowsFromBeginning + row];
+ }
+ for (int row = firstIndexInNewPageBuffer; row < firstIndexInNewPageBuffer
+ + rows; row++) {
+ // Copy the newly created rows
+ newPageBuffer[i][row] = cells[i][row
+ - firstIndexInNewPageBuffer];
+ }
+ for (int row = firstIndexInNewPageBuffer
+ + rows; row < newCachedRowCount; row++) {
+ // Move the old rows down below the newly inserted rows
+ newPageBuffer[i][row] = pageBuffer[i][rowsFromBeginning + row
+ - rows];
+ }
+ }
+ pageBuffer = newPageBuffer;
+ pageBufferFirstIndex = Math.max(
+ pageBufferFirstIndex + rowsFromBeginning, minPageBufferIndex);
+ if (getLogger().isLoggable(Level.FINEST)) {
+ getLogger().log(Level.FINEST,
+ "Page Buffer now contains {0} rows ({1}-{2})",
+ new Object[] { pageBuffer[CELL_ITEMID].length,
+ pageBufferFirstIndex, (pageBufferFirstIndex
+ + pageBuffer[CELL_ITEMID].length - 1) });
+ }
+ return cells;
+ }
+
+ private int getMaxPageBufferIndex() {
+ int total = size();
+ if (getPageLength() == 0) {
+ // everything is shown at once, no caching
+ return total - 1;
+ }
+ // Page buffer must not become larger than pageLength*cacheRate after
+ // the current page
+ int maxPageBufferIndex = getCurrentPageFirstItemIndex()
+ + (int) (getPageLength() * (1 + getCacheRate()));
+ if (shouldHideNullSelectionItem()) {
+ --total;
+ }
+ if (maxPageBufferIndex >= total) {
+ maxPageBufferIndex = total - 1;
+ }
+ return maxPageBufferIndex;
+ }
+
+ private int getMinPageBufferIndex() {
+ if (getPageLength() == 0) {
+ // everything is shown at once, no caching
+ return 0;
+ }
+ // Page buffer must not become larger than pageLength*cacheRate before
+ // the current page
+ int minPageBufferIndex = getCurrentPageFirstItemIndex()
+ - (int) (getPageLength() * getCacheRate());
+ if (minPageBufferIndex < 0) {
+ minPageBufferIndex = 0;
+ }
+ return minPageBufferIndex;
+ }
+
+ /**
+ * Render rows with index "firstIndex" to "firstIndex+rows-1" to a new
+ * buffer.
+ *
+ * Reuses values from the current page buffer if the rows are found there.
+ *
+ * @param firstIndex
+ * @param rows
+ * @param replaceListeners
+ * @return
+ */
+ private Object[][] getVisibleCellsNoCache(int firstIndex, int rows,
+ boolean replaceListeners) {
+ if (getLogger().isLoggable(Level.FINEST)) {
+ getLogger().log(Level.FINEST,
+ "Render visible cells for rows {0}-{1}",
+ new Object[] { firstIndex, (firstIndex + rows - 1) });
+ }
+ final Object[] colids = getVisibleColumns();
+ final int cols = colids.length;
+
+ HashSet<Property<?>> oldListenedProperties = listenedProperties;
+ HashSet<Component> oldVisibleComponents = visibleComponents;
+
+ if (replaceListeners) {
+ // initialize the listener collections, this should only be done if
+ // the entire cache is refreshed (through refreshRenderedCells)
+ listenedProperties = new HashSet<Property<?>>();
+ visibleComponents = new HashSet<Component>();
+ }
+
+ Object[][] cells = new Object[cols + CELL_FIRSTCOL][rows];
+ if (rows == 0) {
+ unregisterPropertiesAndComponents(oldListenedProperties,
+ oldVisibleComponents);
+ return cells;
+ }
+
+ final RowHeaderMode headmode = getRowHeaderMode();
+ final boolean[] iscomponent = new boolean[cols];
+ for (int i = 0; i < cols; i++) {
+ iscomponent[i] = columnGenerators.containsKey(colids[i])
+ || Component.class.isAssignableFrom(getType(colids[i]));
+ }
+ int firstIndexNotInCache;
+ if (pageBuffer != null && pageBuffer[CELL_ITEMID].length > 0) {
+ firstIndexNotInCache = pageBufferFirstIndex
+ + pageBuffer[CELL_ITEMID].length;
+ } else {
+ firstIndexNotInCache = -1;
+ }
+
+ // Creates the page contents
+ int filledRows = 0;
+ if (items instanceof Container.Indexed) {
+ // more efficient implementation for containers supporting access by
+ // index
+
+ List<?> itemIds = getItemIds(firstIndex, rows);
+ for (int i = 0; i < rows && i < itemIds.size(); i++) {
+ Object id = itemIds.get(i);
+ if (id == null) {
+ throw new IllegalStateException(
+ "Null itemId returned from container");
+ }
+ // Start by parsing the values, id should already be set
+ parseItemIdToCells(cells, id, i, firstIndex, headmode, cols,
+ colids, firstIndexNotInCache, iscomponent,
+ oldListenedProperties);
+
+ filledRows++;
+ }
+ } else {
+ // slow back-up implementation for cases where the container does
+ // not support access by index
+
+ // Gets the first item id
+ Object id = firstItemId();
+ for (int i = 0; i < firstIndex; i++) {
+ id = nextItemId(id);
+ }
+ for (int i = 0; i < rows && id != null; i++) {
+ // Start by parsing the values, id should already be set
+ parseItemIdToCells(cells, id, i, firstIndex, headmode, cols,
+ colids, firstIndexNotInCache, iscomponent,
+ oldListenedProperties);
+
+ // Gets the next item id for non indexed container
+ id = nextItemId(id);
+
+ filledRows++;
+ }
+ }
+
+ // Assures that all the rows of the cell-buffer are valid
+ if (filledRows != cells[0].length) {
+ final Object[][] temp = new Object[cells.length][filledRows];
+ for (int i = 0; i < cells.length; i++) {
+ for (int j = 0; j < filledRows; j++) {
+ temp[i][j] = cells[i][j];
+ }
+ }
+ cells = temp;
+ }
+
+ unregisterPropertiesAndComponents(oldListenedProperties,
+ oldVisibleComponents);
+
+ return cells;
+ }
+
+ protected List<Object> getItemIds(int firstIndex, int rows) {
+ return (List<Object>) ((Container.Indexed) items).getItemIds(firstIndex,
+ rows);
+ }
+
+ /**
+ * Update a cache array for a row, register any relevant listeners etc.
+ *
+ * This is an internal method extracted from
+ * {@link #getVisibleCellsNoCache(int, int, boolean)} and should be removed
+ * when the Table is rewritten.
+ */
+ private void parseItemIdToCells(Object[][] cells, Object id, int i,
+ int firstIndex, RowHeaderMode headmode, int cols, Object[] colids,
+ int firstIndexNotInCache, boolean[] iscomponent,
+ HashSet<Property<?>> oldListenedProperties) {
+
+ cells[CELL_ITEMID][i] = id;
+ cells[CELL_KEY][i] = itemIdMapper.key(id);
+ if (headmode != ROW_HEADER_MODE_HIDDEN) {
+ switch (headmode) {
+ case INDEX:
+ cells[CELL_HEADER][i] = String.valueOf(i + firstIndex + 1);
+ break;
+ default:
+ try {
+ cells[CELL_HEADER][i] = getItemCaption(id);
+ } catch (Exception e) {
+ exceptionsDuringCachePopulation.add(e);
+ cells[CELL_HEADER][i] = "";
+ }
+ }
+ try {
+ cells[CELL_ICON][i] = getItemIcon(id);
+ } catch (Exception e) {
+ exceptionsDuringCachePopulation.add(e);
+ cells[CELL_ICON][i] = null;
+ }
+ }
+
+ GeneratedRow generatedRow = rowGenerator != null
+ ? rowGenerator.generateRow(this, id) : null;
+ cells[CELL_GENERATED_ROW][i] = generatedRow;
+
+ for (int j = 0; j < cols; j++) {
+ if (isColumnCollapsed(colids[j])) {
+ continue;
+ }
+ Property<?> p = null;
+ Object value = "";
+ boolean isGeneratedRow = generatedRow != null;
+ boolean isGeneratedColumn = columnGenerators.containsKey(colids[j]);
+ boolean isGenerated = isGeneratedRow || isGeneratedColumn;
+
+ if (!isGenerated) {
+ try {
+ p = getContainerProperty(id, colids[j]);
+ } catch (Exception e) {
+ exceptionsDuringCachePopulation.add(e);
+ value = null;
+ }
+ }
+
+ if (isGeneratedRow) {
+ if (generatedRow.isSpanColumns() && j > 0) {
+ value = null;
+ } else if (generatedRow.isSpanColumns() && j == 0
+ && generatedRow.getValue() instanceof Component) {
+ value = generatedRow.getValue();
+ } else if (generatedRow.getText().length > j) {
+ value = generatedRow.getText()[j];
+ }
+ } else {
+ // check if current pageBuffer already has row
+ int index = firstIndex + i;
+ if (p != null || isGenerated) {
+ int indexInOldBuffer = index - pageBufferFirstIndex;
+ if (index < firstIndexNotInCache
+ && index >= pageBufferFirstIndex
+ && pageBuffer[CELL_GENERATED_ROW][indexInOldBuffer] == null
+ && id.equals(
+ pageBuffer[CELL_ITEMID][indexInOldBuffer])) {
+ // we already have data in our cache,
+ // recycle it instead of fetching it via
+ // getValue/getPropertyValue
+ value = pageBuffer[CELL_FIRSTCOL + j][indexInOldBuffer];
+ if (!isGeneratedColumn && iscomponent[j]
+ || !(value instanceof Component)) {
+ listenProperty(p, oldListenedProperties);
+ }
+ } else {
+ if (isGeneratedColumn) {
+ ColumnGenerator cg = columnGenerators
+ .get(colids[j]);
+ try {
+ value = cg.generateCell(this, id, colids[j]);
+ } catch (Exception e) {
+ exceptionsDuringCachePopulation.add(e);
+ value = null;
+ }
+ if (value != null && !(value instanceof Component)
+ && !(value instanceof String)) {
+ // Avoid errors if a generator returns
+ // something
+ // other than a Component or a String
+ value = value.toString();
+ }
+ } else if (iscomponent[j]) {
+ try {
+ value = p.getValue();
+ } catch (Exception e) {
+ exceptionsDuringCachePopulation.add(e);
+ value = null;
+ }
+ listenProperty(p, oldListenedProperties);
+ } else if (p != null) {
+ try {
+ value = getPropertyValue(id, colids[j], p);
+ } catch (Exception e) {
+ exceptionsDuringCachePopulation.add(e);
+ value = null;
+ }
+ /*
+ * If returned value is Component (via fieldfactory
+ * or overridden getPropertyValue) we expect it to
+ * listen property value changes. Otherwise if
+ * property emits value change events, table will
+ * start to listen them and refresh content when
+ * needed.
+ */
+ if (!(value instanceof Component)) {
+ listenProperty(p, oldListenedProperties);
+ }
+ } else {
+ try {
+ value = getPropertyValue(id, colids[j], null);
+ } catch (Exception e) {
+ exceptionsDuringCachePopulation.add(e);
+ value = null;
+ }
+ }
+ }
+ }
+ }
+
+ if (value instanceof Component) {
+ registerComponent((Component) value);
+ }
+ cells[CELL_FIRSTCOL + j][i] = value;
+ }
+ }
+
+ protected void registerComponent(Component component) {
+ getLogger().log(Level.FINEST, "Registered {0}: {1}", new Object[] {
+ component.getClass().getSimpleName(), component.getCaption() });
+ if (!equals(component.getParent())) {
+ component.setParent(this);
+ }
+ visibleComponents.add(component);
+ }
+
+ private void listenProperty(Property<?> p,
+ HashSet<Property<?>> oldListenedProperties) {
+ if (p instanceof Property.ValueChangeNotifier) {
+ if (oldListenedProperties == null
+ || !oldListenedProperties.contains(p)) {
+ ((Property.ValueChangeNotifier) p).addListener(this);
+ }
+ /*
+ * register listened properties, so we can do proper cleanup to free
+ * memory. Essential if table has loads of data and it is used for a
+ * long time.
+ */
+ listenedProperties.add(p);
+
+ }
+ }
+
+ /**
+ * @param firstIx
+ * Index of the first row to process. Global index, not relative
+ * to page buffer.
+ * @param count
+ */
+ private void unregisterComponentsAndPropertiesInRows(int firstIx,
+ int count) {
+ if (getLogger().isLoggable(Level.FINEST)) {
+ getLogger().log(Level.FINEST,
+ "Unregistering components in rows {0}-{1}",
+ new Object[] { firstIx, (firstIx + count - 1) });
+ }
+ Object[] colids = getVisibleColumns();
+ if (pageBuffer != null && pageBuffer[CELL_ITEMID].length > 0) {
+ int bufSize = pageBuffer[CELL_ITEMID].length;
+ int ix = firstIx - pageBufferFirstIndex;
+ ix = ix < 0 ? 0 : ix;
+ if (ix < bufSize) {
+ count = count > bufSize - ix ? bufSize - ix : count;
+ for (int i = 0; i < count; i++) {
+ for (int c = 0; c < colids.length; c++) {
+ Object cellVal = pageBuffer[CELL_FIRSTCOL + c][i + ix];
+ if (cellVal instanceof Component
+ && visibleComponents.contains(cellVal)) {
+ visibleComponents.remove(cellVal);
+ unregisterComponent((Component) cellVal);
+ } else {
+ Property<?> p = getContainerProperty(
+ pageBuffer[CELL_ITEMID][i + ix], colids[c]);
+ if (p instanceof ValueChangeNotifier
+ && listenedProperties.contains(p)) {
+ listenedProperties.remove(p);
+ ((ValueChangeNotifier) p).removeListener(this);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Helper method to remove listeners and maintain correct component
+ * hierarchy. Detaches properties and components if those are no more
+ * rendered in client.
+ *
+ * @param oldListenedProperties
+ * set of properties that where listened in last render
+ * @param oldVisibleComponents
+ * set of components that where attached in last render
+ */
+ private void unregisterPropertiesAndComponents(
+ HashSet<Property<?>> oldListenedProperties,
+ HashSet<Component> oldVisibleComponents) {
+ if (oldVisibleComponents != null) {
+ for (final Iterator<Component> i = oldVisibleComponents
+ .iterator(); i.hasNext();) {
+ Component c = i.next();
+ if (!visibleComponents.contains(c)) {
+ unregisterComponent(c);
+ }
+ }
+ }
+
+ if (oldListenedProperties != null) {
+ for (final Iterator<Property<?>> i = oldListenedProperties
+ .iterator(); i.hasNext();) {
+ Property.ValueChangeNotifier o = (ValueChangeNotifier) i.next();
+ if (!listenedProperties.contains(o)) {
+ o.removeListener(this);
+ }
+ }
+ }
+ }
+
+ /**
+ * This method cleans up a Component that has been generated when Table is
+ * in editable mode. The component needs to be detached from its parent and
+ * if it is a field, it needs to be detached from its property data source
+ * in order to allow garbage collection to take care of removing the unused
+ * component from memory.
+ *
+ * Override this method and getPropertyValue(Object, Object, Property) with
+ * custom logic if you need to deal with buffered fields.
+ *
+ * @see #getPropertyValue(Object, Object, Property)
+ *
+ * @param component
+ * component that should be unregistered.
+ */
+ protected void unregisterComponent(Component component) {
+ getLogger().log(Level.FINEST, "Unregistered {0}: {1}", new Object[] {
+ component.getClass().getSimpleName(), component.getCaption() });
+ component.setParent(null);
+ /*
+ * Also remove property data sources to unregister listeners keeping the
+ * fields in memory.
+ */
+ if (component instanceof Field) {
+ Field<?> field = (Field<?>) component;
+ Property<?> associatedProperty = associatedProperties
+ .remove(component);
+ if (associatedProperty != null
+ && field.getPropertyDataSource() == associatedProperty) {
+ // Remove the property data source only if it's the one we
+ // added in getPropertyValue
+ field.setPropertyDataSource(null);
+ }
+ }
+ }
+
+ /**
+ * Sets the row header mode.
+ * <p>
+ * The mode can be one of the following ones:
+ * <ul>
+ * <li>{@link #ROW_HEADER_MODE_HIDDEN}: The row captions are hidden.</li>
+ * <li>{@link #ROW_HEADER_MODE_ID}: Items Id-objects <code>toString()</code>
+ * is used as row caption.
+ * <li>{@link #ROW_HEADER_MODE_ITEM}: Item-objects <code>toString()</code>
+ * is used as row caption.
+ * <li>{@link #ROW_HEADER_MODE_PROPERTY}: Property set with
+ * {@link #setItemCaptionPropertyId(Object)} is used as row header.
+ * <li>{@link #ROW_HEADER_MODE_EXPLICIT_DEFAULTS_ID}: Items Id-objects
+ * <code>toString()</code> is used as row header. If caption is explicitly
+ * specified, it overrides the id-caption.
+ * <li>{@link #ROW_HEADER_MODE_EXPLICIT}: The row headers must be explicitly
+ * specified.</li>
+ * <li>{@link #ROW_HEADER_MODE_INDEX}: The index of the item is used as row
+ * caption. The index mode can only be used with the containers implementing
+ * <code>Container.Indexed</code> interface.</li>
+ * </ul>
+ * The default value is {@link #ROW_HEADER_MODE_HIDDEN}
+ * </p>
+ *
+ * @param mode
+ * the One of the modes listed above.
+ */
+ public void setRowHeaderMode(RowHeaderMode mode) {
+ if (mode != null) {
+ rowHeaderMode = mode;
+ if (mode != RowHeaderMode.HIDDEN) {
+ setItemCaptionMode(mode.getItemCaptionMode());
+ }
+ // Assures the visual refresh. No need to reset the page buffer
+ // before
+ // as the content has not changed, only the alignments.
+ refreshRenderedCells();
+ }
+ }
+
+ /**
+ * Gets the row header mode.
+ *
+ * @return the Row header mode.
+ * @see #setRowHeaderMode
+ */
+ public RowHeaderMode getRowHeaderMode() {
+ return rowHeaderMode;
+ }
+
+ /**
+ * Adds the new row to table and fill the visible cells (except generated
+ * columns) with given values.
+ *
+ * @param cells
+ * the Object array that is used for filling the visible cells
+ * new row. The types must be settable to visible column property
+ * types.
+ * @param itemId
+ * the Id the new row. If null, a new id is automatically
+ * assigned. If given, the table cannot already have a item with
+ * given id.
+ * @return Returns item id for the new row. Returns null if operation fails.
+ */
+ public Object addItem(Object[] cells, Object itemId)
+ throws UnsupportedOperationException {
+
+ // remove generated columns from the list of columns being assigned
+ final LinkedList<Object> availableCols = new LinkedList<Object>();
+ for (Iterator<Object> it = visibleColumns.iterator(); it.hasNext();) {
+ Object id = it.next();
+ if (!columnGenerators.containsKey(id)) {
+ availableCols.add(id);
+ }
+ }
+ // Checks that a correct number of cells are given
+ if (cells.length != availableCols.size()) {
+ return null;
+ }
+
+ // Creates new item
+ Item item;
+ if (itemId == null) {
+ itemId = items.addItem();
+ if (itemId == null) {
+ return null;
+ }
+ item = items.getItem(itemId);
+ } else {
+ item = items.addItem(itemId);
+ }
+ if (item == null) {
+ return null;
+ }
+
+ // Fills the item properties
+ for (int i = 0; i < availableCols.size(); i++) {
+ item.getItemProperty(availableCols.get(i)).setValue(cells[i]);
+ }
+
+ if (!(items instanceof Container.ItemSetChangeNotifier)) {
+ refreshRowCache();
+ }
+
+ return itemId;
+ }
+
+ /**
+ * Discards and recreates the internal row cache. Call this if you make
+ * changes that affect the rows but the information about the changes are
+ * not automatically propagated to the Table.
+ * <p>
+ * Do not call this e.g. if you have updated the data model through a
+ * Property. These types of changes are automatically propagated to the
+ * Table.
+ * <p>
+ * A typical case when this is needed is if you update a generator (e.g.
+ * CellStyleGenerator) and want to ensure that the rows are redrawn with new
+ * styles.
+ * <p>
+ * <i>Note that calling this method is not cheap so avoid calling it
+ * unnecessarily.</i>
+ *
+ * @since 6.7.2
+ */
+ public void refreshRowCache() {
+ resetPageBuffer();
+ refreshRenderedCells();
+ }
+
+ /**
+ * Sets the Container that serves as the data source of the viewer. As a
+ * side-effect the table's selection value is set to null as the old
+ * selection might not exist in new Container.<br>
+ * <br>
+ * All rows and columns are generated as visible using this method. If the
+ * new container contains properties that are not meant to be shown you
+ * should use {@link Table#setContainerDataSource(Container, Collection)}
+ * instead, especially if the table is editable.
+ * <p>
+ * Keeps propertyValueConverters if the corresponding id exists in the new
+ * data source and is of a compatible type.
+ * </p>
+ *
+ * @param newDataSource
+ * the new data source.
+ */
+ @Override
+ public void setContainerDataSource(Container newDataSource) {
+ if (newDataSource == null) {
+ newDataSource = new IndexedContainer();
+ }
+
+ Collection<Object> generated;
+ if (columnGenerators != null) {
+ generated = columnGenerators.keySet();
+ } else {
+ generated = Collections.emptyList();
+ }
+ List<Object> visibleIds = new ArrayList<Object>();
+ if (generated.isEmpty()) {
+ visibleIds.addAll(newDataSource.getContainerPropertyIds());
+ } else {
+ for (Object id : newDataSource.getContainerPropertyIds()) {
+ // don't add duplicates
+ if (!generated.contains(id)) {
+ visibleIds.add(id);
+ }
+ }
+ // generated columns to the end
+ visibleIds.addAll(generated);
+ }
+ setContainerDataSource(newDataSource, visibleIds);
+ }
+
+ /**
+ * Sets the container data source and the columns that will be visible.
+ * Columns are shown in the collection's iteration order.
+ * <p>
+ * Keeps propertyValueConverters if the corresponding id exists in the new
+ * data source and is of a compatible type.
+ * </p>
+ *
+ * @see Table#setContainerDataSource(Container)
+ * @see Table#setVisibleColumns(Object[])
+ * @see Table#setConverter(Object, Converter<String, ?>)
+ *
+ * @param newDataSource
+ * the new data source.
+ * @param visibleIds
+ * IDs of the visible columns
+ */
+ public void setContainerDataSource(Container newDataSource,
+ Collection<?> visibleIds) {
+
+ disableContentRefreshing();
+
+ if (newDataSource == null) {
+ newDataSource = new IndexedContainer();
+ }
+ if (visibleIds == null) {
+ visibleIds = new ArrayList<Object>();
+ }
+
+ // Retain propertyValueConverters if their corresponding ids are
+ // properties of the new
+ // data source and are of a compatible type
+ if (propertyValueConverters != null) {
+ Collection<?> newPropertyIds = newDataSource
+ .getContainerPropertyIds();
+ LinkedList<Object> retainableValueConverters = new LinkedList<Object>();
+ for (Object propertyId : newPropertyIds) {
+ Converter<String, ?> converter = getConverter(propertyId);
+ if (converter != null) {
+ if (typeIsCompatible(converter.getModelType(),
+ newDataSource.getType(propertyId))) {
+ retainableValueConverters.add(propertyId);
+ }
+ }
+ }
+ propertyValueConverters.keySet()
+ .retainAll(retainableValueConverters);
+ }
+
+ // Assures that the data source is ordered by making unordered
+ // containers ordered by wrapping them
+ if (newDataSource instanceof Container.Ordered) {
+ super.setContainerDataSource(newDataSource);
+ } else {
+ super.setContainerDataSource(
+ new ContainerOrderedWrapper(newDataSource));
+ }
+
+ // Resets page position
+ currentPageFirstItemId = null;
+ currentPageFirstItemIndex = 0;
+
+ // Resets column properties
+ if (collapsedColumns != null) {
+ collapsedColumns.clear();
+ }
+
+ // don't add the same id twice
+ Collection<Object> col = new LinkedList<Object>();
+ for (Iterator<?> it = visibleIds.iterator(); it.hasNext();) {
+ Object id = it.next();
+ if (!col.contains(id)) {
+ col.add(id);
+ }
+ }
+
+ setVisibleColumns(col.toArray());
+
+ // Assure visual refresh
+ resetPageBuffer();
+
+ enableContentRefreshing(true);
+ }
+
+ /**
+ * Checks if class b can be safely assigned to class a.
+ *
+ * @param a
+ * @param b
+ * @return
+ */
+ private boolean typeIsCompatible(Class<?> a, Class<?> b) {
+ // TODO Implement this check properly
+ // Basically we need to do a a.isAssignableFrom(b)
+ // with special considerations for primitive types.
+ return true;
+ }
+
+ /**
+ * Gets items ids from a range of key values
+ *
+ * @param itemId
+ * The start key
+ * @param length
+ * amount of items to be retrieved
+ * @return
+ */
+ private LinkedHashSet<Object> getItemIdsInRange(Object itemId,
+ final int length) {
+ LinkedHashSet<Object> ids = new LinkedHashSet<Object>();
+ for (int i = 0; i < length; i++) {
+ assert itemId != null; // should not be null unless client-server
+ // are out of sync
+ ids.add(itemId);
+ itemId = nextItemId(itemId);
+ }
+ return ids;
+ }
+
+ /**
+ * Handles selection if selection is a multiselection
+ *
+ * @param variables
+ * The variables
+ */
+ private void handleSelectedItems(Map<String, Object> variables) {
+ final String[] ka = (String[]) variables.get("selected");
+ final String[] ranges = (String[]) variables.get("selectedRanges");
+
+ Set<Object> renderedButNotSelectedItemIds = getCurrentlyRenderedItemIds();
+
+ @SuppressWarnings("unchecked")
+ HashSet<Object> newValue = new LinkedHashSet<Object>(
+ (Collection<Object>) getValue());
+
+ if (variables.containsKey("clearSelections")) {
+ // the client side has instructed to swipe all previous selections
+ newValue.clear();
+ }
+
+ /*
+ * Then add (possibly some of them back) rows that are currently
+ * selected on the client side (the ones that the client side is aware
+ * of).
+ */
+ for (int i = 0; i < ka.length; i++) {
+ // key to id
+ final Object id = itemIdMapper.get(ka[i]);
+ if (!isNullSelectionAllowed()
+ && (id == null || id == getNullSelectionItemId())) {
+ // skip empty selection if nullselection is not allowed
+ markAsDirty();
+ } else if (id != null && containsId(id)) {
+ newValue.add(id);
+ renderedButNotSelectedItemIds.remove(id);
+ }
+ }
+
+ /* Add range items aka shift clicked multiselection areas */
+ if (ranges != null) {
+ for (String range : ranges) {
+ String[] split = range.split("-");
+ Object startItemId = itemIdMapper.get(split[0]);
+ int length = Integer.valueOf(split[1]);
+ LinkedHashSet<Object> itemIdsInRange = getItemIdsInRange(
+ startItemId, length);
+ newValue.addAll(itemIdsInRange);
+ renderedButNotSelectedItemIds.removeAll(itemIdsInRange);
+ }
+ }
+ /*
+ * finally clear all currently rendered rows (the ones that the client
+ * side counterpart is aware of) that the client didn't send as selected
+ */
+ newValue.removeAll(renderedButNotSelectedItemIds);
+
+ if (!isNullSelectionAllowed() && newValue.isEmpty()) {
+ // empty selection not allowed, keep old value
+ markAsDirty();
+ return;
+ }
+
+ setValue(newValue, true);
+
+ }
+
+ private Set<Object> getCurrentlyRenderedItemIds() {
+ HashSet<Object> ids = new HashSet<Object>();
+ if (pageBuffer != null) {
+ for (int i = 0; i < pageBuffer[CELL_ITEMID].length; i++) {
+ ids.add(pageBuffer[CELL_ITEMID][i]);
+ }
+ }
+ return ids;
+ }
+
+ /* Component basics */
+
+ /**
+ * Invoked when the value of a variable has changed.
+ *
+ * @see com.vaadin.v7.ui.Select#changeVariables(java.lang.Object,
+ * java.util.Map)
+ */
+
+ @Override
+ public void changeVariables(Object source, Map<String, Object> variables) {
+
+ boolean clientNeedsContentRefresh = false;
+
+ handleClickEvent(variables);
+
+ handleColumnResizeEvent(variables);
+
+ handleColumnWidthUpdates(variables);
+
+ disableContentRefreshing();
+
+ if (!isSelectable() && variables.containsKey("selected")) {
+ // Not-selectable is a special case, AbstractSelect does not support
+ // TODO could be optimized.
+ variables = new HashMap<String, Object>(variables);
+ variables.remove("selected");
+ }
+
+ /*
+ * The AbstractSelect cannot handle the multiselection properly, instead
+ * we handle it ourself
+ */
+ else if (isSelectable() && isMultiSelect()
+ && variables.containsKey("selected")
+ && multiSelectMode == MultiSelectMode.DEFAULT) {
+ handleSelectedItems(variables);
+ variables = new HashMap<String, Object>(variables);
+ variables.remove("selected");
+ }
+
+ super.changeVariables(source, variables);
+
+ // Client might update the pagelength if Table height is fixed
+ if (variables.containsKey("pagelength")) {
+ // Sets pageLength directly to avoid repaint that setter causes
+ pageLength = (Integer) variables.get("pagelength");
+ }
+
+ // Page start index
+ if (variables.containsKey("firstvisible")) {
+ final Integer value = (Integer) variables.get("firstvisible");
+ if (value != null) {
+ setCurrentPageFirstItemIndex(value.intValue(), false);
+ }
+ }
+
+ // Sets requested firstrow and rows for the next paint
+ if (variables.containsKey("reqfirstrow")
+ || variables.containsKey("reqrows")) {
+
+ try {
+ firstToBeRenderedInClient = ((Integer) variables
+ .get("firstToBeRendered")).intValue();
+ lastToBeRenderedInClient = ((Integer) variables
+ .get("lastToBeRendered")).intValue();
+ } catch (Exception e) {
+ // FIXME: Handle exception
+ getLogger().log(Level.FINER,
+ "Could not parse the first and/or last rows.", e);
+ }
+
+ // respect suggested rows only if table is not otherwise updated
+ // (row caches emptied by other event)
+ if (!containerChangeToBeRendered) {
+ Integer value = (Integer) variables.get("reqfirstrow");
+ if (value != null) {
+ reqFirstRowToPaint = value.intValue();
+ }
+
+ value = (Integer) variables.get("reqrows");
+ if (value != null) {
+ reqRowsToPaint = value.intValue();
+ int size = size();
+ // sanity check
+
+ if (reqFirstRowToPaint >= size) {
+ reqFirstRowToPaint = size;
+ }
+
+ if (reqFirstRowToPaint + reqRowsToPaint > size) {
+ reqRowsToPaint = size - reqFirstRowToPaint;
+ }
+ }
+ }
+ if (getLogger().isLoggable(Level.FINEST)) {
+ getLogger().log(Level.FINEST, "Client wants rows {0}-{1}",
+ new Object[] { reqFirstRowToPaint,
+ (reqFirstRowToPaint + reqRowsToPaint - 1) });
+ }
+ clientNeedsContentRefresh = true;
+ }
+
+ if (isSortEnabled()) {
+ // Sorting
+ boolean doSort = false;
+ if (variables.containsKey("sortcolumn")) {
+ final String colId = (String) variables.get("sortcolumn");
+ if (colId != null && !"".equals(colId)
+ && !"null".equals(colId)) {
+ final Object id = columnIdMap.get(colId);
+ setSortContainerPropertyId(id, false);
+ doSort = true;
+ }
+ }
+ if (variables.containsKey("sortascending")) {
+ final boolean state = ((Boolean) variables.get("sortascending"))
+ .booleanValue();
+ if (state != sortAscending) {
+ setSortAscending(state, false);
+ doSort = true;
+ }
+ }
+ if (doSort) {
+ this.sort();
+ resetPageBuffer();
+ }
+ }
+
+ // Dynamic column hide/show and order
+ // Update visible columns
+ if (isColumnCollapsingAllowed()) {
+ if (variables.containsKey("collapsedcolumns")) {
+ try {
+ final Object[] ids = (Object[]) variables
+ .get("collapsedcolumns");
+ Set<Object> idSet = new HashSet<Object>();
+ for (Object id : ids) {
+ idSet.add(columnIdMap.get(id.toString()));
+ }
+ for (final Iterator<Object> it = visibleColumns
+ .iterator(); it.hasNext();) {
+ Object propertyId = it.next();
+ if (isColumnCollapsed(propertyId)) {
+ if (!idSet.contains(propertyId)) {
+ setColumnCollapsed(propertyId, false);
+ }
+ } else if (idSet.contains(propertyId)) {
+ setColumnCollapsed(propertyId, true);
+ }
+ }
+ } catch (final Exception e) {
+ // FIXME: Handle exception
+ getLogger().log(Level.FINER,
+ "Could not determine column collapsing state", e);
+ }
+ clientNeedsContentRefresh = true;
+ }
+ }
+ if (isColumnReorderingAllowed()) {
+ if (variables.containsKey("columnorder")) {
+ try {
+ final Object[] ids = (Object[]) variables
+ .get("columnorder");
+ // need a real Object[], ids can be a String[]
+ final Object[] idsTemp = new Object[ids.length];
+ for (int i = 0; i < ids.length; i++) {
+ idsTemp[i] = columnIdMap.get(ids[i].toString());
+ }
+ setColumnOrder(idsTemp);
+ if (hasListeners(ColumnReorderEvent.class)) {
+ fireEvent(new ColumnReorderEvent(this));
+ }
+ } catch (final Exception e) {
+ // FIXME: Handle exception
+ getLogger().log(Level.FINER,
+ "Could not determine column reordering state", e);
+ }
+ clientNeedsContentRefresh = true;
+ }
+ }
+
+ enableContentRefreshing(clientNeedsContentRefresh);
+
+ // Actions
+ if (variables.containsKey("action")) {
+ final StringTokenizer st = new StringTokenizer(
+ (String) variables.get("action"), ",");
+ if (st.countTokens() == 2) {
+ final Object itemId = itemIdMapper.get(st.nextToken());
+ final Action action = actionMapper.get(st.nextToken());
+
+ if (action != null && (itemId == null || containsId(itemId))
+ && actionHandlers != null) {
+ for (Handler ah : actionHandlers) {
+ ah.handleAction(action, this, itemId);
+ }
+ }
+ }
+ }
+
+ }
+
+ /**
+ * Handles click event
+ *
+ * @param variables
+ */
+ private void handleClickEvent(Map<String, Object> variables) {
+
+ // Item click event
+ if (variables.containsKey("clickEvent")) {
+ String key = (String) variables.get("clickedKey");
+ Object itemId = itemIdMapper.get(key);
+ Object propertyId = null;
+ String colkey = (String) variables.get("clickedColKey");
+ // click is not necessary on a property
+ if (colkey != null) {
+ propertyId = columnIdMap.get(colkey);
+ }
+ MouseEventDetails evt = MouseEventDetails
+ .deSerialize((String) variables.get("clickEvent"));
+ Item item = getItem(itemId);
+ if (item != null) {
+ fireEvent(new ItemClickEvent(this, item, itemId, propertyId,
+ evt));
+ }
+ }
+
+ // Header click event
+ else if (variables.containsKey("headerClickEvent")) {
+
+ MouseEventDetails details = MouseEventDetails
+ .deSerialize((String) variables.get("headerClickEvent"));
+
+ Object cid = variables.get("headerClickCID");
+ Object propertyId = null;
+ if (cid != null) {
+ propertyId = columnIdMap.get(cid.toString());
+ }
+ fireEvent(new HeaderClickEvent(this, propertyId, details));
+ }
+
+ // Footer click event
+ else if (variables.containsKey("footerClickEvent")) {
+ MouseEventDetails details = MouseEventDetails
+ .deSerialize((String) variables.get("footerClickEvent"));
+
+ Object cid = variables.get("footerClickCID");
+ Object propertyId = null;
+ if (cid != null) {
+ propertyId = columnIdMap.get(cid.toString());
+ }
+ fireEvent(new FooterClickEvent(this, propertyId, details));
+ }
+ }
+
+ /**
+ * Handles the column resize event sent by the client.
+ *
+ * @param variables
+ */
+ private void handleColumnResizeEvent(Map<String, Object> variables) {
+ if (variables.containsKey("columnResizeEventColumn")) {
+ Object cid = variables.get("columnResizeEventColumn");
+ Object propertyId = null;
+ if (cid != null) {
+ propertyId = columnIdMap.get(cid.toString());
+
+ Object prev = variables.get("columnResizeEventPrev");
+ int previousWidth = -1;
+ if (prev != null) {
+ previousWidth = Integer.valueOf(prev.toString());
+ }
+
+ Object curr = variables.get("columnResizeEventCurr");
+ int currentWidth = -1;
+ if (curr != null) {
+ currentWidth = Integer.valueOf(curr.toString());
+ }
+
+ fireColumnResizeEvent(propertyId, previousWidth, currentWidth);
+ }
+ }
+ }
+
+ private void fireColumnCollapseEvent(Object propertyId) {
+ fireEvent(new ColumnCollapseEvent(this, propertyId));
+ }
+
+ private void fireColumnResizeEvent(Object propertyId, int previousWidth,
+ int currentWidth) {
+ /*
+ * Update the sizes on the server side. If a column previously had a
+ * expand ratio and the user resized the column then the expand ratio
+ * will be turned into a static pixel size.
+ */
+ setColumnWidth(propertyId, currentWidth);
+
+ fireEvent(new ColumnResizeEvent(this, propertyId, previousWidth,
+ currentWidth));
+ }
+
+ private void handleColumnWidthUpdates(Map<String, Object> variables) {
+ if (variables.containsKey("columnWidthUpdates")) {
+ String[] events = (String[]) variables.get("columnWidthUpdates");
+ for (String str : events) {
+ String[] eventDetails = str.split(":");
+ Object propertyId = columnIdMap.get(eventDetails[0]);
+ if (propertyId == null) {
+ propertyId = ROW_HEADER_FAKE_PROPERTY_ID;
+ }
+ int width = Integer.valueOf(eventDetails[1]);
+ setColumnWidth(propertyId, width);
+ }
+ }
+ }
+
+ /**
+ * Go to mode where content updates are not done. This is due we want to
+ * bypass expensive content for some reason (like when we know we may have
+ * other content changes on their way).
+ *
+ * @return true if content refresh flag was enabled prior this call
+ */
+ protected boolean disableContentRefreshing() {
+ boolean wasDisabled = isContentRefreshesEnabled;
+ isContentRefreshesEnabled = false;
+ return wasDisabled;
+ }
+
+ /**
+ * Go to mode where content content refreshing has effect.
+ *
+ * @param refreshContent
+ * true if content refresh needs to be done
+ */
+ protected void enableContentRefreshing(boolean refreshContent) {
+ isContentRefreshesEnabled = true;
+ if (refreshContent) {
+ refreshRenderedCells();
+ // Ensure that client gets a response
+ markAsDirty();
+ }
+ }
+
+ @Override
+ public void beforeClientResponse(boolean initial) {
+ super.beforeClientResponse(initial);
+
+ // Ensure pageBuffer is filled before sending the response to avoid
+ // calls to markAsDirty during paint
+ getVisibleCells();
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.ui.AbstractSelect#paintContent(com.vaadin.
+ * terminal.PaintTarget)
+ */
+
+ @Override
+ public void paintContent(PaintTarget target) throws PaintException {
+ isBeingPainted = true;
+ try {
+ doPaintContent(target);
+ } finally {
+ isBeingPainted = false;
+ }
+ }
+
+ private void doPaintContent(PaintTarget target) throws PaintException {
+ /*
+ * Body actions - Actions which has the target null and can be invoked
+ * by right clicking on the table body.
+ */
+ final Set<Action> actionSet = findAndPaintBodyActions(target);
+
+ final Object[][] cells = getVisibleCells();
+ int rows = findNumRowsToPaint(target, cells);
+
+ int total = size();
+ if (shouldHideNullSelectionItem()) {
+ total--;
+ rows--;
+ }
+
+ // Table attributes
+ paintTableAttributes(target, rows, total);
+
+ paintVisibleColumnOrder(target);
+
+ // Rows
+ if (isPartialRowUpdate() && painted && !target.isFullRepaint()) {
+ paintPartialRowUpdate(target, actionSet);
+ } else if (target.isFullRepaint() || isRowCacheInvalidated()) {
+ paintRows(target, cells, actionSet);
+ setRowCacheInvalidated(false);
+ }
+
+ /*
+ * Send the page buffer indexes to ensure that the client side stays in
+ * sync. Otherwise we _might_ have the situation where the client side
+ * discards too few or too many rows, causing out of sync issues.
+ */
+ int pageBufferLastIndex = pageBufferFirstIndex
+ + pageBuffer[CELL_ITEMID].length - 1;
+ target.addAttribute(TableConstants.ATTRIBUTE_PAGEBUFFER_FIRST,
+ pageBufferFirstIndex);
+ target.addAttribute(TableConstants.ATTRIBUTE_PAGEBUFFER_LAST,
+ pageBufferLastIndex);
+
+ paintSorting(target);
+
+ resetVariablesAndPageBuffer(target);
+
+ // Actions
+ paintActions(target, actionSet);
+
+ paintColumnOrder(target);
+
+ // Available columns
+ paintAvailableColumns(target);
+
+ paintVisibleColumns(target);
+
+ if (keyMapperReset) {
+ keyMapperReset = false;
+ target.addAttribute(TableConstants.ATTRIBUTE_KEY_MAPPER_RESET,
+ true);
+ }
+
+ if (dropHandler != null) {
+ dropHandler.getAcceptCriterion().paint(target);
+ }
+
+ painted = true;
+ }
+
+ private void setRowCacheInvalidated(boolean invalidated) {
+ rowCacheInvalidated = invalidated;
+ }
+
+ protected boolean isRowCacheInvalidated() {
+ return rowCacheInvalidated;
+ }
+
+ private void paintPartialRowUpdate(PaintTarget target,
+ Set<Action> actionSet) throws PaintException {
+ paintPartialRowUpdates(target, actionSet);
+ paintPartialRowAdditions(target, actionSet);
+ }
+
+ private void paintPartialRowUpdates(PaintTarget target,
+ Set<Action> actionSet) throws PaintException {
+ final boolean[] iscomponent = findCellsWithComponents();
+
+ int firstIx = getFirstUpdatedItemIndex();
+ int count = getUpdatedRowCount();
+
+ target.startTag("urows");
+ target.addAttribute("firsturowix", firstIx);
+ target.addAttribute("numurows", count);
+
+ // Partial row updates bypass the normal caching mechanism.
+ Object[][] cells = getVisibleCellsUpdateCacheRows(firstIx, count);
+ for (int indexInRowbuffer = 0; indexInRowbuffer < count; indexInRowbuffer++) {
+ final Object itemId = cells[CELL_ITEMID][indexInRowbuffer];
+
+ if (shouldHideNullSelectionItem()) {
+ // Remove null selection item if null selection is not allowed
+ continue;
+ }
+
+ paintRow(target, cells, isEditable(), actionSet, iscomponent,
+ indexInRowbuffer, itemId);
+ }
+ target.endTag("urows");
+ maybeThrowCacheUpdateExceptions();
+ }
+
+ private void paintPartialRowAdditions(PaintTarget target,
+ Set<Action> actionSet) throws PaintException {
+ final boolean[] iscomponent = findCellsWithComponents();
+
+ int firstIx = getFirstAddedItemIndex();
+ int count = getAddedRowCount();
+
+ target.startTag("prows");
+
+ if (!shouldHideAddedRows()) {
+ getLogger().log(Level.FINEST,
+ "Paint rows for add. Index: {0}, count: {1}.",
+ new Object[] { firstIx, count });
+
+ // Partial row additions bypass the normal caching mechanism.
+ Object[][] cells = getVisibleCellsInsertIntoCache(firstIx, count);
+ if (cells[0].length < count) {
+ // delete the rows below, since they will fall beyond the cache
+ // page.
+ target.addAttribute("delbelow", true);
+ count = cells[0].length;
+ }
+
+ for (int indexInRowbuffer = 0; indexInRowbuffer < count; indexInRowbuffer++) {
+ final Object itemId = cells[CELL_ITEMID][indexInRowbuffer];
+ if (shouldHideNullSelectionItem()) {
+ // Remove null selection item if null selection is not
+ // allowed
+ continue;
+ }
+
+ paintRow(target, cells, isEditable(), actionSet, iscomponent,
+ indexInRowbuffer, itemId);
+ }
+ } else {
+ getLogger().log(Level.FINEST,
+ "Paint rows for remove. Index: {0}, count: {1}.",
+ new Object[] { firstIx, count });
+ removeRowsFromCacheAndFillBottom(firstIx, count);
+ target.addAttribute("hide", true);
+ }
+
+ target.addAttribute("firstprowix", firstIx);
+ target.addAttribute("numprows", count);
+ target.endTag("prows");
+ maybeThrowCacheUpdateExceptions();
+ }
+
+ /**
+ * Subclass and override this to enable partial row updates and additions,
+ * which bypass the normal caching mechanism. This is useful for e.g.
+ * TreeTable.
+ *
+ * @return true if this update is a partial row update, false if not. For
+ * plain Table it is always false.
+ */
+ protected boolean isPartialRowUpdate() {
+ return false;
+ }
+
+ /**
+ * Subclass and override this to enable partial row additions, bypassing the
+ * normal caching mechanism. This is useful for e.g. TreeTable, where
+ * expanding a node should only fetch and add the items inside of that node.
+ *
+ * @return The index of the first added item. For plain Table it is always
+ * 0.
+ */
+ protected int getFirstAddedItemIndex() {
+ return 0;
+ }
+
+ /**
+ * Subclass and override this to enable partial row additions, bypassing the
+ * normal caching mechanism. This is useful for e.g. TreeTable, where
+ * expanding a node should only fetch and add the items inside of that node.
+ *
+ * @return the number of rows to be added, starting at the index returned by
+ * {@link #getFirstAddedItemIndex()}. For plain Table it is always
+ * 0.
+ */
+ protected int getAddedRowCount() {
+ return 0;
+ }
+
+ /**
+ * Subclass and override this to enable removing of rows, bypassing the
+ * normal caching and lazy loading mechanism. This is useful for e.g.
+ * TreeTable, when you need to hide certain rows as a node is collapsed.
+ *
+ * This should return true if the rows pointed to by
+ * {@link #getFirstAddedItemIndex()} and {@link #getAddedRowCount()} should
+ * be hidden instead of added.
+ *
+ * @return whether the rows to add (see {@link #getFirstAddedItemIndex()}
+ * and {@link #getAddedRowCount()}) should be added or hidden. For
+ * plain Table it is always false.
+ */
+ protected boolean shouldHideAddedRows() {
+ return false;
+ }
+
+ /**
+ * Subclass and override this to enable partial row updates, bypassing the
+ * normal caching and lazy loading mechanism. This is useful for updating
+ * the state of certain rows, e.g. in the TreeTable the collapsed state of a
+ * single node is updated using this mechanism.
+ *
+ * @return the index of the first item to be updated. For plain Table it is
+ * always 0.
+ */
+ protected int getFirstUpdatedItemIndex() {
+ return 0;
+ }
+
+ /**
+ * Subclass and override this to enable partial row updates, bypassing the
+ * normal caching and lazy loading mechanism. This is useful for updating
+ * the state of certain rows, e.g. in the TreeTable the collapsed state of a
+ * single node is updated using this mechanism.
+ *
+ * @return the number of rows to update, starting at the index returned by
+ * {@link #getFirstUpdatedItemIndex()}. For plain table it is always
+ * 0.
+ */
+ protected int getUpdatedRowCount() {
+ return 0;
+ }
+
+ private void paintTableAttributes(PaintTarget target, int rows, int total)
+ throws PaintException {
+ paintTabIndex(target);
+ paintDragMode(target);
+ paintSelectMode(target);
+ paintTableChildLayoutMeasureMode(target);
+
+ if (cacheRate != CACHE_RATE_DEFAULT) {
+ target.addAttribute("cr", cacheRate);
+ }
+
+ target.addAttribute("cols", getVisibleColumns().length);
+ target.addAttribute("rows", rows);
+
+ target.addAttribute("firstrow", (reqFirstRowToPaint >= 0
+ ? reqFirstRowToPaint : firstToBeRenderedInClient));
+ target.addAttribute("totalrows", total);
+ if (getPageLength() != 0) {
+ target.addAttribute("pagelength", getPageLength());
+ }
+ if (areColumnHeadersEnabled()) {
+ target.addAttribute("colheaders", true);
+ }
+ if (rowHeadersAreEnabled()) {
+ target.addAttribute("rowheaders", true);
+ }
+
+ target.addAttribute("colfooters", columnFootersVisible);
+
+ // The cursors are only shown on pageable table
+ if (getCurrentPageFirstItemIndex() != 0 || getPageLength() > 0) {
+ target.addVariable(this, "firstvisible",
+ getCurrentPageFirstItemIndex());
+ target.addVariable(this, "firstvisibleonlastpage",
+ currentPageFirstItemIndexOnLastPage);
+ }
+ }
+
+ /**
+ * Resets and paints "to be painted next" variables. Also reset pageBuffer
+ */
+ private void resetVariablesAndPageBuffer(PaintTarget target)
+ throws PaintException {
+ reqFirstRowToPaint = -1;
+ reqRowsToPaint = -1;
+ containerChangeToBeRendered = false;
+ target.addVariable(this, "reqrows", reqRowsToPaint);
+ target.addVariable(this, "reqfirstrow", reqFirstRowToPaint);
+ }
+
+ private boolean areColumnHeadersEnabled() {
+ return getColumnHeaderMode() != ColumnHeaderMode.HIDDEN;
+ }
+
+ private void paintVisibleColumns(PaintTarget target) throws PaintException {
+ target.startTag("visiblecolumns");
+ if (rowHeadersAreEnabled()) {
+ target.startTag("column");
+ target.addAttribute("cid", ROW_HEADER_COLUMN_KEY);
+ paintColumnWidth(target, ROW_HEADER_FAKE_PROPERTY_ID);
+ paintColumnExpandRatio(target, ROW_HEADER_FAKE_PROPERTY_ID);
+ target.endTag("column");
+ }
+ final Collection<?> sortables = getSortableContainerPropertyIds();
+ for (Object colId : visibleColumns) {
+ if (colId != null) {
+ target.startTag("column");
+ target.addAttribute("cid", columnIdMap.key(colId));
+ final String head = getColumnHeader(colId);
+ target.addAttribute("caption", (head != null ? head : ""));
+ final String foot = getColumnFooter(colId);
+ target.addAttribute("fcaption", (foot != null ? foot : ""));
+ if (isColumnCollapsed(colId)) {
+ target.addAttribute("collapsed", true);
+ }
+ if (areColumnHeadersEnabled()) {
+ if (getColumnIcon(colId) != null) {
+ target.addAttribute("icon", getColumnIcon(colId));
+ }
+ if (sortables.contains(colId)) {
+ target.addAttribute("sortable", true);
+ }
+ }
+ if (!Align.LEFT.equals(getColumnAlignment(colId))) {
+ target.addAttribute("align",
+ getColumnAlignment(colId).toString());
+ }
+ paintColumnWidth(target, colId);
+ paintColumnExpandRatio(target, colId);
+ target.endTag("column");
+ }
+ }
+ target.endTag("visiblecolumns");
+ }
+
+ private void paintAvailableColumns(PaintTarget target)
+ throws PaintException {
+ if (columnCollapsingAllowed) {
+ final HashSet<Object> collapsedCols = new HashSet<Object>();
+ for (Object colId : visibleColumns) {
+ if (isColumnCollapsed(colId)) {
+ collapsedCols.add(colId);
+ }
+ }
+ final String[] collapsedKeys = new String[collapsedCols.size()];
+ int nextColumn = 0;
+ for (Object colId : visibleColumns) {
+ if (isColumnCollapsed(colId)) {
+ collapsedKeys[nextColumn++] = columnIdMap.key(colId);
+ }
+ }
+ target.addVariable(this, "collapsedcolumns", collapsedKeys);
+
+ final String[] noncollapsibleKeys = new String[noncollapsibleColumns
+ .size()];
+ nextColumn = 0;
+ for (Object colId : noncollapsibleColumns) {
+ noncollapsibleKeys[nextColumn++] = columnIdMap.key(colId);
+ }
+ target.addVariable(this, "noncollapsiblecolumns",
+ noncollapsibleKeys);
+ }
+
+ }
+
+ private void paintActions(PaintTarget target, final Set<Action> actionSet)
+ throws PaintException {
+ if (!actionSet.isEmpty()) {
+ target.addVariable(this, "action", "");
+ target.startTag("actions");
+ for (Action a : actionSet) {
+ target.startTag("action");
+ if (a.getCaption() != null) {
+ target.addAttribute("caption", a.getCaption());
+ }
+ if (a.getIcon() != null) {
+ target.addAttribute("icon", a.getIcon());
+ }
+ target.addAttribute("key", actionMapper.key(a));
+ target.endTag("action");
+ }
+ target.endTag("actions");
+ }
+ }
+
+ private void paintColumnOrder(PaintTarget target) throws PaintException {
+ if (columnReorderingAllowed) {
+ final String[] colorder = new String[visibleColumns.size()];
+ int i = 0;
+ for (Object colId : visibleColumns) {
+ colorder[i++] = columnIdMap.key(colId);
+ }
+ target.addVariable(this, "columnorder", colorder);
+ }
+ }
+
+ private void paintSorting(PaintTarget target) throws PaintException {
+ // Sorting
+ if (getContainerDataSource() instanceof Container.Sortable) {
+ target.addVariable(this, "sortcolumn",
+ columnIdMap.key(sortContainerPropertyId));
+ target.addVariable(this, "sortascending", sortAscending);
+ }
+ }
+
+ private void paintRows(PaintTarget target, final Object[][] cells,
+ final Set<Action> actionSet) throws PaintException {
+ final boolean[] iscomponent = findCellsWithComponents();
+
+ target.startTag("rows");
+ // cells array contains all that are supposed to be visible on client,
+ // but we'll start from the one requested by client
+ int start = 0;
+ if (reqFirstRowToPaint != -1 && firstToBeRenderedInClient != -1) {
+ start = reqFirstRowToPaint - firstToBeRenderedInClient;
+ }
+ int end = cells[0].length;
+ if (reqRowsToPaint != -1) {
+ end = start + reqRowsToPaint;
+ }
+ // sanity check
+ if (lastToBeRenderedInClient != -1 && lastToBeRenderedInClient < end) {
+ end = lastToBeRenderedInClient + 1;
+ }
+ if (start > cells[CELL_ITEMID].length || start < 0) {
+ start = 0;
+ }
+ if (end > cells[CELL_ITEMID].length) {
+ end = cells[CELL_ITEMID].length;
+ }
+
+ for (int indexInRowbuffer = start; indexInRowbuffer < end; indexInRowbuffer++) {
+ final Object itemId = cells[CELL_ITEMID][indexInRowbuffer];
+
+ if (shouldHideNullSelectionItem()) {
+ // Remove null selection item if null selection is not allowed
+ continue;
+ }
+
+ paintRow(target, cells, isEditable(), actionSet, iscomponent,
+ indexInRowbuffer, itemId);
+ }
+ target.endTag("rows");
+ }
+
+ private boolean[] findCellsWithComponents() {
+ final boolean[] isComponent = new boolean[visibleColumns.size()];
+ int ix = 0;
+ for (Object columnId : visibleColumns) {
+ if (columnGenerators.containsKey(columnId)) {
+ isComponent[ix++] = true;
+ } else {
+ final Class<?> colType = getType(columnId);
+ isComponent[ix++] = colType != null
+ && Component.class.isAssignableFrom(colType);
+ }
+ }
+ return isComponent;
+ }
+
+ private void paintVisibleColumnOrder(PaintTarget target) {
+ // Visible column order
+ final ArrayList<String> visibleColOrder = new ArrayList<String>();
+ for (Object columnId : visibleColumns) {
+ if (!isColumnCollapsed(columnId)) {
+ visibleColOrder.add(columnIdMap.key(columnId));
+ }
+ }
+ target.addAttribute("vcolorder", visibleColOrder.toArray());
+ }
+
+ private Set<Action> findAndPaintBodyActions(PaintTarget target) {
+ Set<Action> actionSet = new LinkedHashSet<Action>();
+ if (actionHandlers != null) {
+ final ArrayList<String> keys = new ArrayList<String>();
+ for (Handler ah : actionHandlers) {
+ // Getting actions for the null item, which in this case means
+ // the body item
+ final Action[] actions = ah.getActions(null, this);
+ if (actions != null) {
+ for (Action action : actions) {
+ actionSet.add(action);
+ keys.add(actionMapper.key(action));
+ }
+ }
+ }
+ target.addAttribute("alb", keys.toArray());
+ }
+ return actionSet;
+ }
+
+ private boolean shouldHideNullSelectionItem() {
+ return !isNullSelectionAllowed() && getNullSelectionItemId() != null
+ && containsId(getNullSelectionItemId());
+ }
+
+ private int findNumRowsToPaint(PaintTarget target, final Object[][] cells)
+ throws PaintException {
+ int rows;
+ if (reqRowsToPaint >= 0) {
+ rows = reqRowsToPaint;
+ } else {
+ rows = cells[0].length;
+ if (alwaysRecalculateColumnWidths) {
+ // TODO experimental feature for now: tell the client to
+ // recalculate column widths.
+ // We'll only do this for paints that do not originate from
+ // table scroll/cache requests (i.e when reqRowsToPaint<0)
+ target.addAttribute("recalcWidths", true);
+ }
+ }
+ return rows;
+ }
+
+ private void paintSelectMode(PaintTarget target) throws PaintException {
+ if (multiSelectMode != MultiSelectMode.DEFAULT) {
+ target.addAttribute("multiselectmode", multiSelectMode.ordinal());
+ }
+ if (isSelectable()) {
+ target.addAttribute("selectmode",
+ (isMultiSelect() ? "multi" : "single"));
+ } else {
+ target.addAttribute("selectmode", "none");
+ }
+ if (!isNullSelectionAllowed()) {
+ target.addAttribute("nsa", false);
+ }
+
+ // selection support
+ // The select variable is only enabled if selectable
+ if (isSelectable()) {
+ target.addVariable(this, "selected", findSelectedKeys());
+ }
+ }
+
+ private String[] findSelectedKeys() {
+ LinkedList<String> selectedKeys = new LinkedList<String>();
+ if (isMultiSelect()) {
+ HashSet<?> sel = new HashSet<Object>((Set<?>) getValue());
+ Collection<?> vids = getVisibleItemIds();
+ for (Iterator<?> it = vids.iterator(); it.hasNext();) {
+ Object id = it.next();
+ if (sel.contains(id)) {
+ selectedKeys.add(itemIdMapper.key(id));
+ }
+ }
+ } else {
+ Object value = getValue();
+ if (value == null) {
+ value = getNullSelectionItemId();
+ }
+ if (value != null) {
+ selectedKeys.add(itemIdMapper.key(value));
+ }
+ }
+ return selectedKeys.toArray(new String[selectedKeys.size()]);
+ }
+
+ private void paintDragMode(PaintTarget target) throws PaintException {
+ if (dragMode != TableDragMode.NONE) {
+ target.addAttribute("dragmode", dragMode.ordinal());
+ }
+ }
+
+ private void paintTabIndex(PaintTarget target) throws PaintException {
+ // The tab ordering number
+ if (getTabIndex() > 0) {
+ target.addAttribute("tabindex", getTabIndex());
+ }
+ }
+
+ private void paintColumnWidth(PaintTarget target, final Object columnId)
+ throws PaintException {
+ if (columnWidths.containsKey(columnId)) {
+ target.addAttribute("width", getColumnWidth(columnId));
+ }
+ }
+
+ private void paintColumnExpandRatio(PaintTarget target,
+ final Object columnId) throws PaintException {
+ if (columnExpandRatios.containsKey(columnId)) {
+ target.addAttribute("er", getColumnExpandRatio(columnId));
+ }
+ }
+
+ private void paintTableChildLayoutMeasureMode(PaintTarget target)
+ throws PaintException {
+ target.addAttribute("measurehint", getChildMeasurementHint().ordinal());
+ }
+
+ /**
+ * Checks whether row headers are visible.
+ *
+ * @return {@code false} if row headers are hidden, {@code true} otherwise
+ * @since 7.3.9
+ */
+ protected boolean rowHeadersAreEnabled() {
+ return getRowHeaderMode() != RowHeaderMode.HIDDEN;
+ }
+
+ private void paintRow(PaintTarget target, final Object[][] cells,
+ final boolean iseditable, final Set<Action> actionSet,
+ final boolean[] iscomponent, int indexInRowbuffer,
+ final Object itemId) throws PaintException {
+ target.startTag("tr");
+
+ paintRowAttributes(target, cells, actionSet, indexInRowbuffer, itemId);
+
+ // cells
+ int currentColumn = 0;
+ for (final Iterator<Object> it = visibleColumns.iterator(); it
+ .hasNext(); currentColumn++) {
+ final Object columnId = it.next();
+ if (columnId == null || isColumnCollapsed(columnId)) {
+ continue;
+ }
+ /*
+ * For each cell, if a cellStyleGenerator is specified, get the
+ * specific style for the cell. If there is any, add it to the
+ * target.
+ */
+ if (cellStyleGenerator != null) {
+ String cellStyle = cellStyleGenerator.getStyle(this, itemId,
+ columnId);
+ if (cellStyle != null && !cellStyle.equals("")) {
+ target.addAttribute("style-" + columnIdMap.key(columnId),
+ cellStyle);
+ }
+ }
+
+ if ((iscomponent[currentColumn] || iseditable
+ || cells[CELL_GENERATED_ROW][indexInRowbuffer] != null)
+ && Component.class.isInstance(cells[CELL_FIRSTCOL
+ + currentColumn][indexInRowbuffer])) {
+ final Component c = (Component) cells[CELL_FIRSTCOL
+ + currentColumn][indexInRowbuffer];
+ if (c == null || !LegacyCommunicationManager
+ .isComponentVisibleToClient(c)) {
+ target.addText("");
+ } else {
+ LegacyPaint.paint(c, target);
+ }
+ } else {
+ target.addText((String) cells[CELL_FIRSTCOL
+ + currentColumn][indexInRowbuffer]);
+ }
+ paintCellTooltips(target, itemId, columnId);
+ }
+
+ target.endTag("tr");
+ }
+
+ private void paintCellTooltips(PaintTarget target, Object itemId,
+ Object columnId) throws PaintException {
+ if (itemDescriptionGenerator != null) {
+ String itemDescription = itemDescriptionGenerator
+ .generateDescription(this, itemId, columnId);
+ if (itemDescription != null && !itemDescription.equals("")) {
+ target.addAttribute("descr-" + columnIdMap.key(columnId),
+ itemDescription);
+ }
+ }
+ }
+
+ private void paintRowTooltips(PaintTarget target, Object itemId)
+ throws PaintException {
+ if (itemDescriptionGenerator != null) {
+ String rowDescription = itemDescriptionGenerator
+ .generateDescription(this, itemId, null);
+ if (rowDescription != null && !rowDescription.equals("")) {
+ target.addAttribute("rowdescr", rowDescription);
+ }
+ }
+ }
+
+ private void paintRowAttributes(PaintTarget target, final Object[][] cells,
+ final Set<Action> actionSet, int indexInRowbuffer,
+ final Object itemId) throws PaintException {
+ // tr attributes
+
+ paintRowIcon(target, cells, indexInRowbuffer);
+ paintRowHeader(target, cells, indexInRowbuffer);
+ paintGeneratedRowInfo(target, cells, indexInRowbuffer);
+ target.addAttribute("key",
+ Integer.parseInt(cells[CELL_KEY][indexInRowbuffer].toString()));
+
+ if (isSelected(itemId)) {
+ target.addAttribute("selected", true);
+ }
+
+ // Actions
+ if (actionHandlers != null) {
+ final ArrayList<String> keys = new ArrayList<String>();
+ for (Handler ah : actionHandlers) {
+ final Action[] aa = ah.getActions(itemId, this);
+ if (aa != null) {
+ for (int ai = 0; ai < aa.length; ai++) {
+ final String key = actionMapper.key(aa[ai]);
+ actionSet.add(aa[ai]);
+ keys.add(key);
+ }
+ }
+ }
+ target.addAttribute("al", keys.toArray());
+ }
+
+ /*
+ * For each row, if a cellStyleGenerator is specified, get the specific
+ * style for the cell, using null as propertyId. If there is any, add it
+ * to the target.
+ */
+ if (cellStyleGenerator != null) {
+ String rowStyle = cellStyleGenerator.getStyle(this, itemId, null);
+ if (rowStyle != null && !rowStyle.equals("")) {
+ target.addAttribute("rowstyle", rowStyle);
+ }
+ }
+
+ paintRowTooltips(target, itemId);
+
+ paintRowAttributes(target, itemId);
+ }
+
+ private void paintGeneratedRowInfo(PaintTarget target, Object[][] cells,
+ int indexInRowBuffer) throws PaintException {
+ GeneratedRow generatedRow = (GeneratedRow) cells[CELL_GENERATED_ROW][indexInRowBuffer];
+ if (generatedRow != null) {
+ target.addAttribute("gen_html",
+ generatedRow.isHtmlContentAllowed());
+ target.addAttribute("gen_span", generatedRow.isSpanColumns());
+ target.addAttribute("gen_widget",
+ generatedRow.getValue() instanceof Component);
+ }
+ }
+
+ protected void paintRowHeader(PaintTarget target, Object[][] cells,
+ int indexInRowbuffer) throws PaintException {
+ if (rowHeadersAreEnabled()) {
+ if (cells[CELL_HEADER][indexInRowbuffer] != null) {
+ target.addAttribute("caption",
+ (String) cells[CELL_HEADER][indexInRowbuffer]);
+ }
+ }
+
+ }
+
+ protected void paintRowIcon(PaintTarget target, final Object[][] cells,
+ int indexInRowbuffer) throws PaintException {
+ if (rowHeadersAreEnabled()
+ && cells[CELL_ICON][indexInRowbuffer] != null) {
+ target.addAttribute("icon",
+ (Resource) cells[CELL_ICON][indexInRowbuffer]);
+ }
+ }
+
+ /**
+ * A method where extended Table implementations may add their custom
+ * attributes for rows.
+ *
+ * @param target
+ * @param itemId
+ */
+ protected void paintRowAttributes(PaintTarget target, Object itemId)
+ throws PaintException {
+
+ }
+
+ /**
+ * Gets the cached visible table contents.
+ *
+ * @return the cached visible table contents.
+ */
+ private Object[][] getVisibleCells() {
+ if (pageBuffer == null) {
+ refreshRenderedCells();
+ }
+ return pageBuffer;
+ }
+
+ /**
+ * Gets the value of property.
+ *
+ * By default if the table is editable the fieldFactory is used to create
+ * editors for table cells. Otherwise formatPropertyValue is used to format
+ * the value representation.
+ *
+ * @param rowId
+ * the Id of the row (same as item Id).
+ * @param colId
+ * the Id of the column.
+ * @param property
+ * the Property to be presented.
+ * @return Object Either formatted value or Component for field.
+ * @see #setTableFieldFactory(TableFieldFactory)
+ */
+ protected Object getPropertyValue(Object rowId, Object colId,
+ Property property) {
+ if (isEditable() && fieldFactory != null) {
+ final Field<?> f = fieldFactory
+ .createField(getContainerDataSource(), rowId, colId, this);
+ if (f != null) {
+ // Remember that we have made this association so we can remove
+ // it when the component is removed
+ associatedProperties.put(f, property);
+ bindPropertyToField(rowId, colId, property, f);
+ return f;
+ }
+ }
+
+ return formatPropertyValue(rowId, colId, property);
+ }
+
+ /**
+ * Binds an item property to a field generated by TableFieldFactory. The
+ * default behavior is to bind property straight to LegacyField. If
+ * Property.Viewer type property (e.g. PropertyFormatter) is already set for
+ * field, the property is bound to that Property.Viewer.
+ *
+ * @param rowId
+ * @param colId
+ * @param property
+ * @param field
+ * @since 6.7.3
+ */
+ protected void bindPropertyToField(Object rowId, Object colId,
+ Property property, Field field) {
+ // check if field has a property that is Viewer set. In that case we
+ // expect developer has e.g. PropertyFormatter that he wishes to use and
+ // assign the property to the Viewer instead.
+ boolean hasFilterProperty = field.getPropertyDataSource() != null
+ && (field.getPropertyDataSource() instanceof Property.Viewer);
+ if (hasFilterProperty) {
+ ((Property.Viewer) field.getPropertyDataSource())
+ .setPropertyDataSource(property);
+ } else {
+ field.setPropertyDataSource(property);
+ }
+ }
+
+ /**
+ * Formats table cell property values. By default the property.toString()
+ * and return a empty string for null properties.
+ *
+ * @param rowId
+ * the Id of the row (same as item Id).
+ * @param colId
+ * the Id of the column.
+ * @param property
+ * the Property to be formatted.
+ * @return the String representation of property and its value.
+ * @since 3.1
+ */
+ protected String formatPropertyValue(Object rowId, Object colId,
+ Property<?> property) {
+ if (property == null) {
+ return "";
+ }
+ Converter<String, Object> converter = null;
+
+ if (hasConverter(colId)) {
+ converter = getConverter(colId);
+ } else {
+ converter = (Converter) ConverterUtil.getConverter(
+ String.class, property.getType(), getSession());
+ }
+ Object value = property.getValue();
+ if (converter != null) {
+ return converter.convertToPresentation(value, String.class,
+ getLocale());
+ }
+ return (null != value) ? value.toString() : "";
+ }
+
+ /* Action container */
+
+ /**
+ * Registers a new action handler for this container
+ *
+ * @see com.vaadin.event.Action.Container#addActionHandler(Action.Handler)
+ */
+
+ @Override
+ public void addActionHandler(Action.Handler actionHandler) {
+
+ if (actionHandler != null) {
+
+ if (actionHandlers == null) {
+ actionHandlers = new LinkedList<Handler>();
+ actionMapper = new KeyMapper<Action>();
+ }
+
+ if (!actionHandlers.contains(actionHandler)) {
+ actionHandlers.add(actionHandler);
+ // Assures the visual refresh. No need to reset the page buffer
+ // before as the content has not changed, only the action
+ // handlers.
+ refreshRenderedCells();
+ }
+
+ }
+ }
+
+ /**
+ * Removes a previously registered action handler for the contents of this
+ * container.
+ *
+ * @see com.vaadin.event.Action.Container#removeActionHandler(Action.Handler)
+ */
+
+ @Override
+ public void removeActionHandler(Action.Handler actionHandler) {
+
+ if (actionHandlers != null && actionHandlers.contains(actionHandler)) {
+
+ actionHandlers.remove(actionHandler);
+
+ if (actionHandlers.isEmpty()) {
+ actionHandlers = null;
+ actionMapper = null;
+ }
+
+ // Assures the visual refresh. No need to reset the page buffer
+ // before as the content has not changed, only the action
+ // handlers.
+ refreshRenderedCells();
+ }
+ }
+
+ /**
+ * Removes all action handlers
+ */
+ public void removeAllActionHandlers() {
+ actionHandlers = null;
+ actionMapper = null;
+ // Assures the visual refresh. No need to reset the page buffer
+ // before as the content has not changed, only the action
+ // handlers.
+ refreshRenderedCells();
+ }
+
+ /* Property value change listening support */
+
+ /**
+ * Notifies this listener that the Property's value has changed.
+ *
+ * Also listens changes in rendered items to refresh content area.
+ *
+ * @see com.vaadin.v7.data.Property.ValueChangeListener#valueChange(Property.ValueChangeEvent)
+ */
+
+ @Override
+ public void valueChange(Property.ValueChangeEvent event) {
+ if (equals(event.getProperty())
+ || event.getProperty() == getPropertyDataSource()) {
+ super.valueChange(event);
+ } else {
+ refreshRowCache();
+ containerChangeToBeRendered = true;
+ }
+ markAsDirty();
+ }
+
+ /**
+ * Clears the current page buffer. Call this before
+ * {@link #refreshRenderedCells()} to ensure that all content is updated
+ * from the properties.
+ */
+ protected void resetPageBuffer() {
+ firstToBeRenderedInClient = -1;
+ lastToBeRenderedInClient = -1;
+ reqFirstRowToPaint = -1;
+ reqRowsToPaint = -1;
+ pageBuffer = null;
+ }
+
+ /**
+ * Notifies the component that it is connected to an application.
+ *
+ * @see com.vaadin.ui.Component#attach()
+ */
+
+ @Override
+ public void attach() {
+ super.attach();
+
+ refreshRenderedCells();
+ }
+
+ /**
+ * Notifies the component that it is detached from the application
+ *
+ * @see com.vaadin.ui.Component#detach()
+ */
+
+ @Override
+ public void detach() {
+ super.detach();
+ }
+
+ /**
+ * Removes all Items from the Container.
+ *
+ * @see com.vaadin.v7.data.Container#removeAllItems()
+ */
+
+ @Override
+ public boolean removeAllItems() {
+ currentPageFirstItemId = null;
+ currentPageFirstItemIndex = 0;
+ return super.removeAllItems();
+ }
+
+ /**
+ * Removes the Item identified by <code>ItemId</code> from the Container.
+ *
+ * @see com.vaadin.v7.data.Container#removeItem(Object)
+ */
+
+ @Override
+ public boolean removeItem(Object itemId) {
+ final Object nextItemId = nextItemId(itemId);
+ final boolean ret = super.removeItem(itemId);
+ if (ret && (itemId != null)
+ && (itemId.equals(currentPageFirstItemId))) {
+ currentPageFirstItemId = nextItemId;
+ }
+ if (!(items instanceof Container.ItemSetChangeNotifier)) {
+ refreshRowCache();
+ }
+ return ret;
+ }
+
+ /**
+ * Removes a Property specified by the given Property ID from the Container.
+ *
+ * @see com.vaadin.v7.data.Container#removeContainerProperty(Object)
+ */
+
+ @Override
+ public boolean removeContainerProperty(Object propertyId)
+ throws UnsupportedOperationException {
+
+ // If a visible property is removed, remove the corresponding column
+ visibleColumns.remove(propertyId);
+ columnAlignments.remove(propertyId);
+ columnIcons.remove(propertyId);
+ columnHeaders.remove(propertyId);
+ columnFooters.remove(propertyId);
+ // If a propertyValueConverter was defined for the property, remove it.
+ propertyValueConverters.remove(propertyId);
+
+ return super.removeContainerProperty(propertyId);
+ }
+
+ /**
+ * Adds a new property to the table and show it as a visible column.
+ *
+ * @param propertyId
+ * the Id of the property.
+ * @param type
+ * the class of the property.
+ * @param defaultValue
+ * the default value given for all existing items.
+ * @see com.vaadin.v7.data.Container#addContainerProperty(Object, Class,
+ * Object)
+ */
+
+ @Override
+ public boolean addContainerProperty(Object propertyId, Class<?> type,
+ Object defaultValue) throws UnsupportedOperationException {
+
+ boolean visibleColAdded = false;
+ if (!visibleColumns.contains(propertyId)) {
+ visibleColumns.add(propertyId);
+ visibleColAdded = true;
+ }
+
+ if (!super.addContainerProperty(propertyId, type, defaultValue)) {
+ if (visibleColAdded) {
+ visibleColumns.remove(propertyId);
+ }
+ return false;
+ }
+ if (!(items instanceof Container.PropertySetChangeNotifier)) {
+ refreshRowCache();
+ }
+ return true;
+ }
+
+ /**
+ * Adds a new property to the table and show it as a visible column.
+ *
+ * @param propertyId
+ * the Id of the property
+ * @param type
+ * the class of the property
+ * @param defaultValue
+ * the default value given for all existing items
+ * @param columnHeader
+ * the Explicit header of the column. If explicit header is not
+ * needed, this should be set null.
+ * @param columnIcon
+ * the Icon of the column. If icon is not needed, this should be
+ * set null.
+ * @param columnAlignment
+ * the Alignment of the column. Null implies align left.
+ * @throws UnsupportedOperationException
+ * if the operation is not supported.
+ * @see com.vaadin.v7.data.Container#addContainerProperty(Object, Class,
+ * Object)
+ */
+ public boolean addContainerProperty(Object propertyId, Class<?> type,
+ Object defaultValue, String columnHeader, Resource columnIcon,
+ Align columnAlignment) throws UnsupportedOperationException {
+ if (!this.addContainerProperty(propertyId, type, defaultValue)) {
+ return false;
+ }
+ setColumnAlignment(propertyId, columnAlignment);
+ setColumnHeader(propertyId, columnHeader);
+ setColumnIcon(propertyId, columnIcon);
+ return true;
+ }
+
+ /**
+ * Adds a generated column to the Table.
+ * <p>
+ * A generated column is a column that exists only in the Table, not as a
+ * property in the underlying Container. It shows up just as a regular
+ * column.
+ * </p>
+ * <p>
+ * A generated column will override a property with the same id, so that the
+ * generated column is shown instead of the column representing the
+ * property. Note that getContainerProperty() will still get the real
+ * property.
+ * </p>
+ * <p>
+ * Table will not listen to value change events from properties overridden
+ * by generated columns. If the content of your generated column depends on
+ * properties that are not directly visible in the table, attach value
+ * change listener to update the content on all depended properties.
+ * Otherwise your UI might not get updated as expected.
+ * </p>
+ * <p>
+ * Also note that getVisibleColumns() will return the generated columns,
+ * while getContainerPropertyIds() will not.
+ * </p>
+ *
+ * @param id
+ * the id of the column to be added
+ * @param generatedColumn
+ * the {@link ColumnGenerator} to use for this column
+ */
+ public void addGeneratedColumn(Object id, ColumnGenerator generatedColumn) {
+ if (generatedColumn == null) {
+ throw new IllegalArgumentException(
+ "Can not add null as a GeneratedColumn");
+ }
+ if (columnGenerators.containsKey(id)) {
+ throw new IllegalArgumentException(
+ "Can not add the same GeneratedColumn twice, id:" + id);
+ } else {
+ columnGenerators.put(id, generatedColumn);
+ /*
+ * add to visible column list unless already there (overriding
+ * column from DS)
+ */
+ if (!visibleColumns.contains(id)) {
+ visibleColumns.add(id);
+ }
+ refreshRowCache();
+ }
+ }
+
+ /**
+ * Returns the ColumnGenerator used to generate the given column.
+ *
+ * @param columnId
+ * The id of the generated column
+ * @return The ColumnGenerator used for the given columnId or null.
+ */
+ public ColumnGenerator getColumnGenerator(Object columnId)
+ throws IllegalArgumentException {
+ return columnGenerators.get(columnId);
+ }
+
+ /**
+ * Removes a generated column previously added with addGeneratedColumn.
+ *
+ * @param columnId
+ * id of the generated column to remove
+ * @return true if the column could be removed (existed in the Table)
+ */
+ public boolean removeGeneratedColumn(Object columnId) {
+ if (columnGenerators.containsKey(columnId)) {
+ columnGenerators.remove(columnId);
+ // remove column from visibleColumns list unless it exists in
+ // container (generator previously overrode this column)
+ if (!items.getContainerPropertyIds().contains(columnId)) {
+ visibleColumns.remove(columnId);
+ }
+ refreshRowCache();
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Returns item identifiers of the items which are currently rendered on the
+ * client.
+ * <p>
+ * Note, that some due to historical reasons the name of the method is bit
+ * misleading. Some items may be partly or totally out of the viewport of
+ * the table's scrollable area. Actually detecting rows which can be
+ * actually seen by the end user may be problematic due to the client server
+ * architecture. Using {@link #getCurrentPageFirstItemId()} combined with
+ * {@link #getPageLength()} may produce good enough estimates in some
+ * situations.
+ *
+ * @see com.vaadin.v7.ui.Select#getVisibleItemIds()
+ */
+
+ @Override
+ public Collection<?> getVisibleItemIds() {
+
+ final LinkedList<Object> visible = new LinkedList<Object>();
+
+ final Object[][] cells = getVisibleCells();
+ // may be null if the table has not been rendered yet (e.g. not attached
+ // to a layout)
+ if (null != cells) {
+ for (int i = 0; i < cells[CELL_ITEMID].length; i++) {
+ visible.add(cells[CELL_ITEMID][i]);
+ }
+ }
+
+ return visible;
+ }
+
+ /**
+ * Container datasource item set change. Table must flush its buffers on
+ * change.
+ *
+ * @see com.vaadin.v7.data.Container.ItemSetChangeListener#containerItemSetChange(com.vaadin.v7.data.Container.ItemSetChangeEvent)
+ */
+
+ @Override
+ public void containerItemSetChange(Container.ItemSetChangeEvent event) {
+ if (isBeingPainted) {
+ return;
+ }
+
+ super.containerItemSetChange(event);
+
+ // super method clears the key map, must inform client about this to
+ // avoid getting invalid keys back (#8584)
+ keyMapperReset = true;
+
+ int currentFirstItemIndex = getCurrentPageFirstItemIndex();
+
+ if (event.getContainer().size() == 0) {
+ repairOnReAddAllRowsDataScrollPositionItemIndex = getCurrentPageFirstItemIndex();
+ } else {
+ if (repairOnReAddAllRowsDataScrollPositionItemIndex != -1) {
+ currentFirstItemIndex = repairOnReAddAllRowsDataScrollPositionItemIndex;
+ /*
+ * Reset repairOnReAddAllRowsDataScrollPositionItemIndex.
+ *
+ * Next string should be commented (removed) if we want to have
+ * possibility to restore scroll position during adding items to
+ * container one by one via add() but not only addAll(). The
+ * problem in this case: we cannot track what happened between
+ * add() and add()... So it is ambiguous where to stop restore
+ * scroll position.
+ */
+ repairOnReAddAllRowsDataScrollPositionItemIndex = -1;
+ }
+ }
+
+ // ensure that page still has first item in page, ignore buffer refresh
+ // (forced in this method)
+ setCurrentPageFirstItemIndex(currentFirstItemIndex, false);
+ refreshRowCache();
+ }
+
+ /**
+ * Container datasource property set change. Table must flush its buffers on
+ * change.
+ *
+ * @see com.vaadin.v7.data.Container.PropertySetChangeListener#containerPropertySetChange(com.vaadin.v7.data.Container.PropertySetChangeEvent)
+ */
+
+ @Override
+ public void containerPropertySetChange(
+ Container.PropertySetChangeEvent event) {
+ if (isBeingPainted) {
+ return;
+ }
+
+ disableContentRefreshing();
+ super.containerPropertySetChange(event);
+
+ // sanitize visibleColumns. note that we are not adding previously
+ // non-existing properties as columns
+ Collection<?> containerPropertyIds = getContainerDataSource()
+ .getContainerPropertyIds();
+
+ LinkedList<Object> newVisibleColumns = new LinkedList<Object>(
+ visibleColumns);
+ for (Iterator<Object> iterator = newVisibleColumns.iterator(); iterator
+ .hasNext();) {
+ Object id = iterator.next();
+ if (!(containerPropertyIds.contains(id)
+ || columnGenerators.containsKey(id))) {
+ iterator.remove();
+ }
+ }
+ setVisibleColumns(newVisibleColumns.toArray());
+ // same for collapsed columns
+ for (Iterator<Object> iterator = collapsedColumns.iterator(); iterator
+ .hasNext();) {
+ Object id = iterator.next();
+ if (!(containerPropertyIds.contains(id)
+ || columnGenerators.containsKey(id))) {
+ iterator.remove();
+ }
+ }
+
+ resetPageBuffer();
+ enableContentRefreshing(true);
+ }
+
+ /**
+ * Adding new items is not supported.
+ *
+ * @throws UnsupportedOperationException
+ * if set to true.
+ * @see com.vaadin.v7.ui.Select#setNewItemsAllowed(boolean)
+ */
+
+ @Override
+ public void setNewItemsAllowed(boolean allowNewOptions)
+ throws UnsupportedOperationException {
+ if (allowNewOptions) {
+ throw new UnsupportedOperationException();
+ }
+ }
+
+ /**
+ * Gets the ID of the Item following the Item that corresponds to itemId.
+ *
+ * @see com.vaadin.v7.data.Container.Ordered#nextItemId(java.lang.Object)
+ */
+
+ @Override
+ public Object nextItemId(Object itemId) {
+ return ((Container.Ordered) items).nextItemId(itemId);
+ }
+
+ /**
+ * Gets the ID of the Item preceding the Item that corresponds to the
+ * itemId.
+ *
+ * @see com.vaadin.v7.data.Container.Ordered#prevItemId(java.lang.Object)
+ */
+
+ @Override
+ public Object prevItemId(Object itemId) {
+ return ((Container.Ordered) items).prevItemId(itemId);
+ }
+
+ /**
+ * Gets the ID of the first Item in the Container.
+ *
+ * @see com.vaadin.v7.data.Container.Ordered#firstItemId()
+ */
+
+ @Override
+ public Object firstItemId() {
+ return ((Container.Ordered) items).firstItemId();
+ }
+
+ /**
+ * Gets the ID of the last Item in the Container.
+ *
+ * @see com.vaadin.v7.data.Container.Ordered#lastItemId()
+ */
+
+ @Override
+ public Object lastItemId() {
+ return ((Container.Ordered) items).lastItemId();
+ }
+
+ /**
+ * Tests if the Item corresponding to the given Item ID is the first Item in
+ * the Container.
+ *
+ * @see com.vaadin.v7.data.Container.Ordered#isFirstId(java.lang.Object)
+ */
+
+ @Override
+ public boolean isFirstId(Object itemId) {
+ return ((Container.Ordered) items).isFirstId(itemId);
+ }
+
+ /**
+ * Tests if the Item corresponding to the given Item ID is the last Item in
+ * the Container.
+ *
+ * @see com.vaadin.v7.data.Container.Ordered#isLastId(java.lang.Object)
+ */
+
+ @Override
+ public boolean isLastId(Object itemId) {
+ return ((Container.Ordered) items).isLastId(itemId);
+ }
+
+ /**
+ * Adds new item after the given item.
+ *
+ * @see com.vaadin.v7.data.Container.Ordered#addItemAfter(java.lang.Object)
+ */
+
+ @Override
+ public Object addItemAfter(Object previousItemId)
+ throws UnsupportedOperationException {
+ Object itemId = ((Container.Ordered) items)
+ .addItemAfter(previousItemId);
+ if (!(items instanceof Container.ItemSetChangeNotifier)) {
+ refreshRowCache();
+ }
+ return itemId;
+ }
+
+ /**
+ * Adds new item after the given item.
+ *
+ * @see com.vaadin.v7.data.Container.Ordered#addItemAfter(java.lang.Object,
+ * java.lang.Object)
+ */
+
+ @Override
+ public Item addItemAfter(Object previousItemId, Object newItemId)
+ throws UnsupportedOperationException {
+ Item item = ((Container.Ordered) items).addItemAfter(previousItemId,
+ newItemId);
+ if (!(items instanceof Container.ItemSetChangeNotifier)) {
+ refreshRowCache();
+ }
+ return item;
+ }
+
+ /**
+ * Sets the TableFieldFactory that is used to create editor for table cells.
+ *
+ * The TableFieldFactory is only used if the Table is editable. By default
+ * the DefaultFieldFactory is used.
+ *
+ * @param fieldFactory
+ * the field factory to set.
+ * @see #isEditable
+ * @see DefaultFieldFactory
+ */
+ public void setTableFieldFactory(TableFieldFactory fieldFactory) {
+ this.fieldFactory = fieldFactory;
+
+ // Assure visual refresh
+ refreshRowCache();
+ }
+
+ /**
+ * Gets the TableFieldFactory that is used to create editor for table cells.
+ *
+ * The FieldFactory is only used if the Table is editable.
+ *
+ * @return TableFieldFactory used to create the LegacyField instances.
+ * @see #isEditable
+ */
+ public TableFieldFactory getTableFieldFactory() {
+ return fieldFactory;
+ }
+
+ /**
+ * Is table editable.
+ *
+ * If table is editable a editor of type LegacyField is created for each
+ * table cell. The assigned FieldFactory is used to create the instances.
+ *
+ * To provide custom editors for table cells create a class implementing the
+ * FieldFactory interface, and assign it to table, and set the editable
+ * property to true.
+ *
+ * @return true if table is editable, false otherwise.
+ * @see Field
+ * @see FieldFactory
+ *
+ */
+ public boolean isEditable() {
+ return editable;
+ }
+
+ /**
+ * Sets the editable property.
+ *
+ * If table is editable a editor of type LegacyField is created for each
+ * table cell. The assigned FieldFactory is used to create the instances.
+ *
+ * To provide custom editors for table cells create a class implementing the
+ * FieldFactory interface, and assign it to table, and set the editable
+ * property to true.
+ *
+ * @param editable
+ * true if table should be editable by user.
+ * @see Field
+ * @see FieldFactory
+ *
+ */
+ public void setEditable(boolean editable) {
+ this.editable = editable;
+
+ // Assure visual refresh
+ refreshRowCache();
+ }
+
+ /**
+ * Sorts the table.
+ *
+ * @throws UnsupportedOperationException
+ * if the container data source does not implement
+ * Container.Sortable
+ * @see com.vaadin.v7.data.Container.Sortable#sort(java.lang.Object[],
+ * boolean[])
+ *
+ */
+
+ @Override
+ public void sort(Object[] propertyId, boolean[] ascending)
+ throws UnsupportedOperationException {
+ final Container c = getContainerDataSource();
+ if (c instanceof Container.Sortable) {
+ final int pageIndex = getCurrentPageFirstItemIndex();
+ boolean refreshingPreviouslyEnabled = disableContentRefreshing();
+ ((Container.Sortable) c).sort(propertyId, ascending);
+ setCurrentPageFirstItemIndex(pageIndex);
+ if (refreshingPreviouslyEnabled) {
+ enableContentRefreshing(true);
+ }
+ if (propertyId.length > 0 && ascending.length > 0) {
+ // The first propertyId is the primary sorting criterion,
+ // therefore the sort indicator should be there
+ sortAscending = ascending[0];
+ sortContainerPropertyId = propertyId[0];
+ } else {
+ sortAscending = true;
+ sortContainerPropertyId = null;
+ }
+ } else if (c != null) {
+ throw new UnsupportedOperationException(
+ "Underlying Data does not allow sorting");
+ }
+ }
+
+ /**
+ * Sorts the table by currently selected sorting column.
+ *
+ * @throws UnsupportedOperationException
+ * if the container data source does not implement
+ * Container.Sortable
+ */
+ public void sort() {
+ if (getSortContainerPropertyId() == null) {
+ return;
+ }
+ sort(new Object[] { sortContainerPropertyId },
+ new boolean[] { sortAscending });
+ }
+
+ /**
+ * Gets the container property IDs, which can be used to sort the item.
+ * <p>
+ * Note that the {@link #isSortEnabled()} state affects what this method
+ * returns. Disabling sorting causes this method to always return an empty
+ * collection.
+ * </p>
+ *
+ * @see com.vaadin.v7.data.Container.Sortable#getSortableContainerPropertyIds()
+ */
+
+ @Override
+ public Collection<?> getSortableContainerPropertyIds() {
+ final Container c = getContainerDataSource();
+ if (c instanceof Container.Sortable && isSortEnabled()) {
+ return ((Container.Sortable) c).getSortableContainerPropertyIds();
+ } else {
+ return Collections.EMPTY_LIST;
+ }
+ }
+
+ /**
+ * Gets the currently sorted column property ID.
+ *
+ * @return the Container property id of the currently sorted column.
+ */
+ public Object getSortContainerPropertyId() {
+ return sortContainerPropertyId;
+ }
+
+ /**
+ * Sets the currently sorted column property id.
+ *
+ * @param propertyId
+ * the Container property id of the currently sorted column.
+ */
+ public void setSortContainerPropertyId(Object propertyId) {
+ setSortContainerPropertyId(propertyId, true);
+ }
+
+ /**
+ * Internal method to set currently sorted column property id. With doSort
+ * flag actual sorting may be bypassed.
+ *
+ * @param propertyId
+ * @param doSort
+ */
+ private void setSortContainerPropertyId(Object propertyId, boolean doSort) {
+ if ((sortContainerPropertyId != null
+ && !sortContainerPropertyId.equals(propertyId))
+ || (sortContainerPropertyId == null && propertyId != null)) {
+ sortContainerPropertyId = propertyId;
+
+ if (doSort) {
+ sort();
+ // Assures the visual refresh. This should not be necessary as
+ // sort() calls refreshRowCache
+ refreshRenderedCells();
+ }
+ }
+ }
+
+ /**
+ * Is the table currently sorted in ascending order.
+ *
+ * @return <code>true</code> if ascending, <code>false</code> if descending.
+ */
+ public boolean isSortAscending() {
+ return sortAscending;
+ }
+
+ /**
+ * Sets the table in ascending order.
+ *
+ * @param ascending
+ * <code>true</code> if ascending, <code>false</code> if
+ * descending.
+ */
+ public void setSortAscending(boolean ascending) {
+ setSortAscending(ascending, true);
+ }
+
+ /**
+ * Internal method to set sort ascending. With doSort flag actual sort can
+ * be bypassed.
+ *
+ * @param ascending
+ * @param doSort
+ */
+ private void setSortAscending(boolean ascending, boolean doSort) {
+ if (sortAscending != ascending) {
+ sortAscending = ascending;
+ if (doSort) {
+ sort();
+ // Assures the visual refresh. This should not be necessary as
+ // sort() calls refreshRowCache
+ refreshRenderedCells();
+ }
+ }
+ }
+
+ /**
+ * Is sorting disabled altogether.
+ *
+ * True iff no sortable columns are given even in the case where data source
+ * would support this.
+ *
+ * @return True iff sorting is disabled.
+ * @deprecated As of 7.0, use {@link #isSortEnabled()} instead
+ */
+ @Deprecated
+ public boolean isSortDisabled() {
+ return !isSortEnabled();
+ }
+
+ /**
+ * Checks if sorting is enabled.
+ *
+ * @return true if sorting by the user is allowed, false otherwise
+ */
+ public boolean isSortEnabled() {
+ return sortEnabled;
+ }
+
+ /**
+ * Disables the sorting by the user altogether.
+ *
+ * @param sortDisabled
+ * True iff sorting is disabled.
+ * @deprecated As of 7.0, use {@link #setSortEnabled(boolean)} instead
+ */
+ @Deprecated
+ public void setSortDisabled(boolean sortDisabled) {
+ setSortEnabled(!sortDisabled);
+ }
+
+ /**
+ * Enables or disables sorting.
+ * <p>
+ * Setting this to false disallows sorting by the user. It is still possible
+ * to call {@link #sort()}.
+ * </p>
+ *
+ * @param sortEnabled
+ * true to allow the user to sort the table, false to disallow it
+ */
+ public void setSortEnabled(boolean sortEnabled) {
+ if (this.sortEnabled != sortEnabled) {
+ this.sortEnabled = sortEnabled;
+ markAsDirty();
+ }
+ }
+
+ /**
+ * Used to create "generated columns"; columns that exist only in the Table,
+ * not in the underlying Container. Implement this interface and pass it to
+ * Table.addGeneratedColumn along with an id for the column to be generated.
+ *
+ */
+ public interface ColumnGenerator extends Serializable {
+
+ /**
+ * Called by Table when a cell in a generated column needs to be
+ * generated.
+ *
+ * @param source
+ * the source Table
+ * @param itemId
+ * the itemId (aka rowId) for the of the cell to be generated
+ * @param columnId
+ * the id for the generated column (as specified in
+ * addGeneratedColumn)
+ * @return A {@link Component} that should be rendered in the cell or a
+ * {@link String} that should be displayed in the cell. Other
+ * return values are not supported.
+ */
+ public abstract Object generateCell(Table source, Object itemId,
+ Object columnId);
+ }
+
+ /**
+ * Set cell style generator for Table.
+ *
+ * @param cellStyleGenerator
+ * New cell style generator or null to remove generator.
+ */
+ public void setCellStyleGenerator(CellStyleGenerator cellStyleGenerator) {
+ this.cellStyleGenerator = cellStyleGenerator;
+ // Assures the visual refresh. No need to reset the page buffer
+ // before as the content has not changed, only the style generators
+ refreshRenderedCells();
+
+ }
+
+ /**
+ * Get the current cell style generator.
+ *
+ */
+ public CellStyleGenerator getCellStyleGenerator() {
+ return cellStyleGenerator;
+ }
+
+ /**
+ * Allow to define specific style on cells (and rows) contents. Implements
+ * this interface and pass it to Table.setCellStyleGenerator. Row styles are
+ * generated when porpertyId is null. The CSS class name that will be added
+ * to the cell content is <tt>v-table-cell-content-[style name]</tt>, and
+ * the row style will be <tt>v-table-row-[style name]</tt>.
+ */
+ public interface CellStyleGenerator extends Serializable {
+
+ /**
+ * Called by Table when a cell (and row) is painted.
+ *
+ * @param source
+ * the source Table
+ * @param itemId
+ * The itemId of the painted cell
+ * @param propertyId
+ * The propertyId of the cell, null when getting row style
+ * @return The style name to add to this cell or row. (the CSS class
+ * name will be v-table-cell-content-[style name], or
+ * v-table-row-[style name] for rows)
+ */
+ public abstract String getStyle(Table source, Object itemId,
+ Object propertyId);
+ }
+
+ @Override
+ public void addItemClickListener(ItemClickListener listener) {
+ addListener(TableConstants.ITEM_CLICK_EVENT_ID, ItemClickEvent.class,
+ listener, ItemClickEvent.ITEM_CLICK_METHOD);
+ }
+
+ /**
+ * @deprecated As of 7.0, replaced by
+ * {@link #addItemClickListener(ItemClickListener)}
+ **/
+ @Override
+ @Deprecated
+ public void addListener(ItemClickListener listener) {
+ addItemClickListener(listener);
+ }
+
+ @Override
+ public void removeItemClickListener(ItemClickListener listener) {
+ removeListener(TableConstants.ITEM_CLICK_EVENT_ID, ItemClickEvent.class,
+ listener);
+ }
+
+ /**
+ * @deprecated As of 7.0, replaced by
+ * {@link #removeItemClickListener(ItemClickListener)}
+ **/
+ @Override
+ @Deprecated
+ public void removeListener(ItemClickListener listener) {
+ removeItemClickListener(listener);
+ }
+
+ // Identical to AbstractCompoenentContainer.setEnabled();
+
+ @Override
+ public void setEnabled(boolean enabled) {
+ super.setEnabled(enabled);
+ if (getParent() != null && !getParent().isEnabled()) {
+ // some ancestor still disabled, don't update children
+ return;
+ } else {
+ markAsDirtyRecursive();
+ }
+ }
+
+ /**
+ * Sets the drag start mode of the Table. Drag start mode controls how Table
+ * behaves as a drag source.
+ *
+ * @param newDragMode
+ */
+ public void setDragMode(TableDragMode newDragMode) {
+ dragMode = newDragMode;
+ markAsDirty();
+ }
+
+ /**
+ * @return the current start mode of the Table. Drag start mode controls how
+ * Table behaves as a drag source.
+ */
+ public TableDragMode getDragMode() {
+ return dragMode;
+ }
+
+ /**
+ * Concrete implementation of {@link DataBoundTransferable} for data
+ * transferred from a table.
+ *
+ * @see {@link DataBoundTransferable}.
+ *
+ * @since 6.3
+ */
+ public class TableTransferable extends DataBoundTransferable {
+
+ protected TableTransferable(Map<String, Object> rawVariables) {
+ super(Table.this, rawVariables);
+ Object object = rawVariables.get("itemId");
+ if (object != null) {
+ setData("itemId", itemIdMapper.get((String) object));
+ }
+ object = rawVariables.get("propertyId");
+ if (object != null) {
+ setData("propertyId", columnIdMap.get((String) object));
+ }
+ }
+
+ @Override
+ public Object getItemId() {
+ return getData("itemId");
+ }
+
+ @Override
+ public Object getPropertyId() {
+ return getData("propertyId");
+ }
+
+ @Override
+ public Table getSourceComponent() {
+ return (Table) super.getSourceComponent();
+ }
+
+ }
+
+ @Override
+ public TableTransferable getTransferable(Map<String, Object> rawVariables) {
+ TableTransferable transferable = new TableTransferable(rawVariables);
+ return transferable;
+ }
+
+ @Override
+ public DropHandler getDropHandler() {
+ return dropHandler;
+ }
+
+ public void setDropHandler(DropHandler dropHandler) {
+ this.dropHandler = dropHandler;
+ }
+
+ @Override
+ public AbstractSelectTargetDetails translateDropTargetDetails(
+ Map<String, Object> clientVariables) {
+ return new AbstractSelectTargetDetails(clientVariables);
+ }
+
+ /**
+ * Sets the behavior of how the multi-select mode should behave when the
+ * table is both selectable and in multi-select mode.
+ * <p>
+ * Note, that on some clients the mode may not be respected. E.g. on touch
+ * based devices CTRL/SHIFT base selection method is invalid, so touch based
+ * browsers always use the {@link MultiSelectMode#SIMPLE}.
+ *
+ * @param mode
+ * The select mode of the table
+ */
+ public void setMultiSelectMode(MultiSelectMode mode) {
+ multiSelectMode = mode;
+ markAsDirty();
+ }
+
+ /**
+ * Returns the select mode in which multi-select is used.
+ *
+ * @return The multi select mode
+ */
+ public MultiSelectMode getMultiSelectMode() {
+ return multiSelectMode;
+ }
+
+ /**
+ * Lazy loading accept criterion for Table. Accepted target rows are loaded
+ * from server once per drag and drop operation. Developer must override one
+ * method that decides on which rows the currently dragged data can be
+ * dropped.
+ *
+ * <p>
+ * Initially pretty much no data is sent to client. On first required
+ * criterion check (per drag request) the client side data structure is
+ * initialized from server and no subsequent requests requests are needed
+ * during that drag and drop operation.
+ */
+ public static abstract class TableDropCriterion
+ extends ServerSideCriterion {
+
+ private Table table;
+
+ private Set<Object> allowedItemIds;
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.event.dd.acceptcriteria.ServerSideCriterion#getIdentifier
+ * ()
+ */
+
+ @Override
+ protected String getIdentifier() {
+ return TableDropCriterion.class.getCanonicalName();
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.event.dd.acceptcriteria.AcceptCriterion#accepts(com.vaadin
+ * .event.dd.DragAndDropEvent)
+ */
+ @Override
+ @SuppressWarnings("unchecked")
+ public boolean accept(DragAndDropEvent dragEvent) {
+ AbstractSelectTargetDetails dropTargetData = (AbstractSelectTargetDetails) dragEvent
+ .getTargetDetails();
+ table = (Table) dragEvent.getTargetDetails().getTarget();
+ Collection<?> visibleItemIds = table.getVisibleItemIds();
+ allowedItemIds = getAllowedItemIds(dragEvent, table,
+ (Collection<Object>) visibleItemIds);
+
+ return allowedItemIds.contains(dropTargetData.getItemIdOver());
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.event.dd.acceptcriteria.AcceptCriterion#paintResponse(
+ * com.vaadin.server.PaintTarget)
+ */
+
+ @Override
+ public void paintResponse(PaintTarget target) throws PaintException {
+ /*
+ * send allowed nodes to client so subsequent requests can be
+ * avoided
+ */
+ Object[] array = allowedItemIds.toArray();
+ for (int i = 0; i < array.length; i++) {
+ String key = table.itemIdMapper.key(array[i]);
+ array[i] = key;
+ }
+ target.addAttribute("allowedIds", array);
+ }
+
+ /**
+ * @param dragEvent
+ * @param table
+ * the table for which the allowed item identifiers are
+ * defined
+ * @param visibleItemIds
+ * the list of currently rendered item identifiers, accepted
+ * item id's need to be detected only for these visible items
+ * @return the set of identifiers for items on which the dragEvent will
+ * be accepted
+ */
+ protected abstract Set<Object> getAllowedItemIds(
+ DragAndDropEvent dragEvent, Table table,
+ Collection<Object> visibleItemIds);
+
+ }
+
+ /**
+ * Click event fired when clicking on the Table headers. The event includes
+ * a reference the the Table the event originated from, the property id of
+ * the column which header was pressed and details about the mouse event
+ * itself.
+ */
+ public static class HeaderClickEvent extends ClickEvent {
+ public static final Method HEADER_CLICK_METHOD;
+
+ static {
+ try {
+ // Set the header click method
+ HEADER_CLICK_METHOD = HeaderClickListener.class
+ .getDeclaredMethod("headerClick",
+ new Class[] { HeaderClickEvent.class });
+ } catch (final java.lang.NoSuchMethodException e) {
+ // This should never happen
+ throw new java.lang.RuntimeException(e);
+ }
+ }
+
+ // The property id of the column which header was pressed
+ private final Object columnPropertyId;
+
+ public HeaderClickEvent(Component source, Object propertyId,
+ MouseEventDetails details) {
+ super(source, details);
+ columnPropertyId = propertyId;
+ }
+
+ /**
+ * Gets the property id of the column which header was pressed
+ *
+ * @return The column property id
+ */
+ public Object getPropertyId() {
+ return columnPropertyId;
+ }
+ }
+
+ /**
+ * Click event fired when clicking on the Table footers. The event includes
+ * a reference the the Table the event originated from, the property id of
+ * the column which header was pressed and details about the mouse event
+ * itself.
+ */
+ public static class FooterClickEvent extends ClickEvent {
+ public static final Method FOOTER_CLICK_METHOD;
+
+ static {
+ try {
+ // Set the header click method
+ FOOTER_CLICK_METHOD = FooterClickListener.class
+ .getDeclaredMethod("footerClick",
+ new Class[] { FooterClickEvent.class });
+ } catch (final java.lang.NoSuchMethodException e) {
+ // This should never happen
+ throw new java.lang.RuntimeException(e);
+ }
+ }
+
+ // The property id of the column which header was pressed
+ private final Object columnPropertyId;
+
+ /**
+ * Constructor
+ *
+ * @param source
+ * The source of the component
+ * @param propertyId
+ * The propertyId of the column
+ * @param details
+ * The mouse details of the click
+ */
+ public FooterClickEvent(Component source, Object propertyId,
+ MouseEventDetails details) {
+ super(source, details);
+ columnPropertyId = propertyId;
+ }
+
+ /**
+ * Gets the property id of the column which header was pressed
+ *
+ * @return The column property id
+ */
+ public Object getPropertyId() {
+ return columnPropertyId;
+ }
+ }
+
+ /**
+ * Interface for the listener for column header mouse click events. The
+ * headerClick method is called when the user presses a header column cell.
+ */
+ public interface HeaderClickListener extends Serializable {
+
+ /**
+ * Called when a user clicks a header column cell
+ *
+ * @param event
+ * The event which contains information about the column and
+ * the mouse click event
+ */
+ public void headerClick(HeaderClickEvent event);
+ }
+
+ /**
+ * Interface for the listener for column footer mouse click events. The
+ * footerClick method is called when the user presses a footer column cell.
+ */
+ public interface FooterClickListener extends Serializable {
+
+ /**
+ * Called when a user clicks a footer column cell
+ *
+ * @param event
+ * The event which contains information about the column and
+ * the mouse click event
+ */
+ public void footerClick(FooterClickEvent event);
+ }
+
+ /**
+ * Adds a header click listener which handles the click events when the user
+ * clicks on a column header cell in the Table.
+ * <p>
+ * The listener will receive events which contain information about which
+ * column was clicked and some details about the mouse event.
+ * </p>
+ *
+ * @param listener
+ * The handler which should handle the header click events.
+ */
+ public void addHeaderClickListener(HeaderClickListener listener) {
+ addListener(TableConstants.HEADER_CLICK_EVENT_ID,
+ HeaderClickEvent.class, listener,
+ HeaderClickEvent.HEADER_CLICK_METHOD);
+ }
+
+ /**
+ * @deprecated As of 7.0, replaced by
+ * {@link #addHeaderClickListener(HeaderClickListener)}
+ **/
+ @Deprecated
+ public void addListener(HeaderClickListener listener) {
+ addHeaderClickListener(listener);
+ }
+
+ /**
+ * Removes a header click listener
+ *
+ * @param listener
+ * The listener to remove.
+ */
+ public void removeHeaderClickListener(HeaderClickListener listener) {
+ removeListener(TableConstants.HEADER_CLICK_EVENT_ID,
+ HeaderClickEvent.class, listener);
+ }
+
+ /**
+ * @deprecated As of 7.0, replaced by
+ * {@link #removeHeaderClickListener(HeaderClickListener)}
+ **/
+ @Deprecated
+ public void removeListener(HeaderClickListener listener) {
+ removeHeaderClickListener(listener);
+ }
+
+ /**
+ * Adds a footer click listener which handles the click events when the user
+ * clicks on a column footer cell in the Table.
+ * <p>
+ * The listener will receive events which contain information about which
+ * column was clicked and some details about the mouse event.
+ * </p>
+ *
+ * @param listener
+ * The handler which should handle the footer click events.
+ */
+ public void addFooterClickListener(FooterClickListener listener) {
+ addListener(TableConstants.FOOTER_CLICK_EVENT_ID,
+ FooterClickEvent.class, listener,
+ FooterClickEvent.FOOTER_CLICK_METHOD);
+ }
+
+ /**
+ * @deprecated As of 7.0, replaced by
+ * {@link #addFooterClickListener(FooterClickListener)}
+ **/
+ @Deprecated
+ public void addListener(FooterClickListener listener) {
+ addFooterClickListener(listener);
+ }
+
+ /**
+ * Removes a footer click listener
+ *
+ * @param listener
+ * The listener to remove.
+ */
+ public void removeFooterClickListener(FooterClickListener listener) {
+ removeListener(TableConstants.FOOTER_CLICK_EVENT_ID,
+ FooterClickEvent.class, listener);
+ }
+
+ /**
+ * @deprecated As of 7.0, replaced by
+ * {@link #removeFooterClickListener(FooterClickListener)}
+ **/
+ @Deprecated
+ public void removeListener(FooterClickListener listener) {
+ removeFooterClickListener(listener);
+ }
+
+ /**
+ * Gets the footer caption beneath the rows
+ *
+ * @param propertyId
+ * The propertyId of the column *
+ * @return The caption of the footer or NULL if not set
+ */
+ public String getColumnFooter(Object propertyId) {
+ return columnFooters.get(propertyId);
+ }
+
+ /**
+ * Sets the column footer caption. The column footer caption is the text
+ * displayed beneath the column if footers have been set visible.
+ *
+ * @param propertyId
+ * The properyId of the column
+ *
+ * @param footer
+ * The caption of the footer
+ */
+ public void setColumnFooter(Object propertyId, String footer) {
+ if (footer == null) {
+ columnFooters.remove(propertyId);
+ } else {
+ columnFooters.put(propertyId, footer);
+ }
+
+ markAsDirty();
+ }
+
+ /**
+ * Sets the footer visible in the bottom of the table.
+ * <p>
+ * The footer can be used to add column related data like sums to the bottom
+ * of the Table using setColumnFooter(Object propertyId, String footer).
+ * </p>
+ *
+ * @param visible
+ * Should the footer be visible
+ */
+ public void setFooterVisible(boolean visible) {
+ if (visible != columnFootersVisible) {
+ columnFootersVisible = visible;
+ markAsDirty();
+ }
+ }
+
+ /**
+ * Is the footer currently visible?
+ *
+ * @return Returns true if visible else false
+ */
+ public boolean isFooterVisible() {
+ return columnFootersVisible;
+ }
+
+ /**
+ * This event is fired when a column is resized. The event contains the
+ * columns property id which was fired, the previous width of the column and
+ * the width of the column after the resize.
+ */
+ public static class ColumnResizeEvent extends Component.Event {
+ public static final Method COLUMN_RESIZE_METHOD;
+
+ static {
+ try {
+ COLUMN_RESIZE_METHOD = ColumnResizeListener.class
+ .getDeclaredMethod("columnResize",
+ new Class[] { ColumnResizeEvent.class });
+ } catch (final java.lang.NoSuchMethodException e) {
+ // This should never happen
+ throw new java.lang.RuntimeException(e);
+ }
+ }
+
+ private final int previousWidth;
+ private final int currentWidth;
+ private final Object columnPropertyId;
+
+ /**
+ * Constructor
+ *
+ * @param source
+ * The source of the event
+ * @param propertyId
+ * The columns property id
+ * @param previous
+ * The width in pixels of the column before the resize event
+ * @param current
+ * The width in pixels of the column after the resize event
+ */
+ public ColumnResizeEvent(Component source, Object propertyId,
+ int previous, int current) {
+ super(source);
+ previousWidth = previous;
+ currentWidth = current;
+ columnPropertyId = propertyId;
+ }
+
+ /**
+ * Get the column property id of the column that was resized.
+ *
+ * @return The column property id
+ */
+ public Object getPropertyId() {
+ return columnPropertyId;
+ }
+
+ /**
+ * Get the width in pixels of the column before the resize event
+ *
+ * @return Width in pixels
+ */
+ public int getPreviousWidth() {
+ return previousWidth;
+ }
+
+ /**
+ * Get the width in pixels of the column after the resize event
+ *
+ * @return Width in pixels
+ */
+ public int getCurrentWidth() {
+ return currentWidth;
+ }
+ }
+
+ /**
+ * Interface for listening to column resize events.
+ */
+ public interface ColumnResizeListener extends Serializable {
+
+ /**
+ * This method is triggered when the column has been resized
+ *
+ * @param event
+ * The event which contains the column property id, the
+ * previous width of the column and the current width of the
+ * column
+ */
+ public void columnResize(ColumnResizeEvent event);
+ }
+
+ /**
+ * Adds a column resize listener to the Table. A column resize listener is
+ * called when a user resizes a columns width.
+ *
+ * @param listener
+ * The listener to attach to the Table
+ */
+ public void addColumnResizeListener(ColumnResizeListener listener) {
+ addListener(TableConstants.COLUMN_RESIZE_EVENT_ID,
+ ColumnResizeEvent.class, listener,
+ ColumnResizeEvent.COLUMN_RESIZE_METHOD);
+ }
+
+ /**
+ * @deprecated As of 7.0, replaced by
+ * {@link #addColumnResizeListener(ColumnResizeListener)}
+ **/
+ @Deprecated
+ public void addListener(ColumnResizeListener listener) {
+ addColumnResizeListener(listener);
+ }
+
+ /**
+ * Removes a column resize listener from the Table.
+ *
+ * @param listener
+ * The listener to remove
+ */
+ public void removeColumnResizeListener(ColumnResizeListener listener) {
+ removeListener(TableConstants.COLUMN_RESIZE_EVENT_ID,
+ ColumnResizeEvent.class, listener);
+ }
+
+ /**
+ * @deprecated As of 7.0, replaced by
+ * {@link #removeColumnResizeListener(ColumnResizeListener)}
+ **/
+ @Deprecated
+ public void removeListener(ColumnResizeListener listener) {
+ removeColumnResizeListener(listener);
+ }
+
+ /**
+ * This event is fired when a columns are reordered by the end user user.
+ */
+ public static class ColumnReorderEvent extends Component.Event {
+ public static final Method METHOD;
+
+ static {
+ try {
+ METHOD = ColumnReorderListener.class.getDeclaredMethod(
+ "columnReorder",
+ new Class[] { ColumnReorderEvent.class });
+ } catch (final java.lang.NoSuchMethodException e) {
+ // This should never happen
+ throw new java.lang.RuntimeException(e);
+ }
+ }
+
+ /**
+ * Constructor
+ *
+ * @param source
+ * The source of the event
+ */
+ public ColumnReorderEvent(Component source) {
+ super(source);
+ }
+
+ }
+
+ /**
+ * Interface for listening to column reorder events.
+ */
+ public interface ColumnReorderListener extends Serializable {
+
+ /**
+ * This method is triggered when the column has been reordered
+ *
+ * @param event
+ */
+ public void columnReorder(ColumnReorderEvent event);
+ }
+
+ /**
+ * This event is fired when the collapse state of a column changes.
+ *
+ * @since 7.6
+ */
+ public static class ColumnCollapseEvent extends Component.Event {
+
+ public static final Method METHOD = ReflectTools.findMethod(
+ ColumnCollapseListener.class, "columnCollapseStateChange",
+ ColumnCollapseEvent.class);
+ private Object propertyId;
+
+ /**
+ * Constructor
+ *
+ * @param source
+ * The source of the event
+ * @param propertyId
+ * The id of the column
+ */
+ public ColumnCollapseEvent(Component source, Object propertyId) {
+ super(source);
+ this.propertyId = propertyId;
+ }
+
+ /**
+ * Gets the id of the column whose collapse state changed
+ *
+ * @return the property id of the column
+ */
+ public Object getPropertyId() {
+ return propertyId;
+ }
+ }
+
+ /**
+ * Interface for listening to column collapse events.
+ *
+ * @since 7.6
+ */
+ public interface ColumnCollapseListener extends Serializable {
+
+ /**
+ * This method is triggered when the collapse state for a column has
+ * changed
+ *
+ * @param event
+ */
+ public void columnCollapseStateChange(ColumnCollapseEvent event);
+ }
+
+ /**
+ * Adds a column reorder listener to the Table. A column reorder listener is
+ * called when a user reorders columns.
+ *
+ * @param listener
+ * The listener to attach to the Table
+ */
+ public void addColumnReorderListener(ColumnReorderListener listener) {
+ addListener(TableConstants.COLUMN_REORDER_EVENT_ID,
+ ColumnReorderEvent.class, listener, ColumnReorderEvent.METHOD);
+ }
+
+ /**
+ * @deprecated As of 7.0, replaced by
+ * {@link #addColumnReorderListener(ColumnReorderListener)}
+ **/
+ @Deprecated
+ public void addListener(ColumnReorderListener listener) {
+ addColumnReorderListener(listener);
+ }
+
+ /**
+ * Removes a column reorder listener from the Table.
+ *
+ * @param listener
+ * The listener to remove
+ */
+ public void removeColumnReorderListener(ColumnReorderListener listener) {
+ removeListener(TableConstants.COLUMN_REORDER_EVENT_ID,
+ ColumnReorderEvent.class, listener);
+ }
+
+ /**
+ * @deprecated As of 7.0, replaced by
+ * {@link #removeColumnReorderListener(ColumnReorderListener)}
+ **/
+ @Deprecated
+ public void removeListener(ColumnReorderListener listener) {
+ removeColumnReorderListener(listener);
+ }
+
+ /**
+ * Adds a column collapse listener to the Table. A column collapse listener
+ * is called when the collapsed state of a column changes.
+ *
+ * @since 7.6
+ *
+ * @param listener
+ * The listener to attach
+ */
+ public void addColumnCollapseListener(ColumnCollapseListener listener) {
+ addListener(TableConstants.COLUMN_COLLAPSE_EVENT_ID,
+ ColumnCollapseEvent.class, listener,
+ ColumnCollapseEvent.METHOD);
+ }
+
+ /**
+ * Removes a column reorder listener from the Table.
+ *
+ * @since 7.6
+ * @param listener
+ * The listener to remove
+ */
+ public void removeColumnCollapseListener(ColumnCollapseListener listener) {
+ removeListener(TableConstants.COLUMN_COLLAPSE_EVENT_ID,
+ ColumnCollapseEvent.class, listener);
+ }
+
+ /**
+ * Set the item description generator which generates tooltips for cells and
+ * rows in the Table
+ *
+ * @param generator
+ * The generator to use or null to disable
+ */
+ public void setItemDescriptionGenerator(
+ ItemDescriptionGenerator generator) {
+ if (generator != itemDescriptionGenerator) {
+ itemDescriptionGenerator = generator;
+ // Assures the visual refresh. No need to reset the page buffer
+ // before as the content has not changed, only the descriptions
+ refreshRenderedCells();
+ }
+ }
+
+ /**
+ * Get the item description generator which generates tooltips for cells and
+ * rows in the Table.
+ */
+ public ItemDescriptionGenerator getItemDescriptionGenerator() {
+ return itemDescriptionGenerator;
+ }
+
+ /**
+ * Row generators can be used to replace certain items in a table with a
+ * generated string. The generator is called each time the table is
+ * rendered, which means that new strings can be generated each time.
+ *
+ * Row generators can be used for e.g. summary rows or grouping of items.
+ */
+ public interface RowGenerator extends Serializable {
+ /**
+ * Called for every row that is painted in the Table. Returning a
+ * GeneratedRow object will cause the row to be painted based on the
+ * contents of the GeneratedRow. A generated row is by default styled
+ * similarly to a header or footer row.
+ * <p>
+ * The GeneratedRow data object contains the text that should be
+ * rendered in the row. The itemId in the container thus works only as a
+ * placeholder.
+ * <p>
+ * If GeneratedRow.setSpanColumns(true) is used, there will be one
+ * String spanning all columns (use setText("Spanning text")). Otherwise
+ * you can define one String per visible column.
+ * <p>
+ * If GeneratedRow.setRenderAsHtml(true) is used, the strings can
+ * contain HTML markup, otherwise all strings will be rendered as text
+ * (the default).
+ * <p>
+ * A "v-table-generated-row" CSS class is added to all generated rows.
+ * For custom styling of a generated row you can combine a RowGenerator
+ * with a CellStyleGenerator.
+ * <p>
+ *
+ * @param table
+ * The Table that is being painted
+ * @param itemId
+ * The itemId for the row
+ * @return A GeneratedRow describing how the row should be painted or
+ * null to paint the row with the contents from the container
+ */
+ public GeneratedRow generateRow(Table table, Object itemId);
+ }
+
+ public static class GeneratedRow implements Serializable {
+ private boolean htmlContentAllowed = false;
+ private boolean spanColumns = false;
+ private String[] text = null;
+
+ /**
+ * Creates a new generated row. If only one string is passed in, columns
+ * are automatically spanned.
+ *
+ * @param text
+ */
+ public GeneratedRow(String... text) {
+ setHtmlContentAllowed(false);
+ setSpanColumns(text == null || text.length == 1);
+ setText(text);
+ }
+
+ /**
+ * Pass one String if spanColumns is used, one String for each visible
+ * column otherwise
+ */
+ public void setText(String... text) {
+ if (text == null || (text.length == 1 && text[0] == null)) {
+ text = new String[] { "" };
+ }
+ this.text = text;
+ }
+
+ protected String[] getText() {
+ return text;
+ }
+
+ protected Object getValue() {
+ return getText();
+ }
+
+ protected boolean isHtmlContentAllowed() {
+ return htmlContentAllowed;
+ }
+
+ /**
+ * If set to true, all strings passed to {@link #setText(String...)}
+ * will be rendered as HTML.
+ *
+ * @param htmlContentAllowed
+ */
+ public void setHtmlContentAllowed(boolean htmlContentAllowed) {
+ this.htmlContentAllowed = htmlContentAllowed;
+ }
+
+ protected boolean isSpanColumns() {
+ return spanColumns;
+ }
+
+ /**
+ * If set to true, only one string will be rendered, spanning the entire
+ * row.
+ *
+ * @param spanColumns
+ */
+ public void setSpanColumns(boolean spanColumns) {
+ this.spanColumns = spanColumns;
+ }
+ }
+
+ /**
+ * Assigns a row generator to the table. The row generator will be able to
+ * replace rows in the table when it is rendered.
+ *
+ * @param generator
+ * the new row generator
+ */
+ public void setRowGenerator(RowGenerator generator) {
+ rowGenerator = generator;
+ refreshRowCache();
+ }
+
+ /**
+ * @return the current row generator
+ */
+ public RowGenerator getRowGenerator() {
+ return rowGenerator;
+ }
+
+ /**
+ * Sets a converter for a property id.
+ * <p>
+ * The converter is used to format the the data for the given property id
+ * before displaying it in the table.
+ * </p>
+ *
+ * @param propertyId
+ * The propertyId to format using the converter
+ * @param converter
+ * The converter to use for the property id
+ */
+ public void setConverter(Object propertyId,
+ Converter<String, ?> converter) {
+ if (!getContainerPropertyIds().contains(propertyId)) {
+ throw new IllegalArgumentException(
+ "PropertyId " + propertyId + " must be in the container");
+ }
+
+ if (!typeIsCompatible(converter.getModelType(), getType(propertyId))) {
+ throw new IllegalArgumentException(
+ "Property type (" + getType(propertyId)
+ + ") must match converter source type ("
+ + converter.getModelType() + ")");
+ }
+ propertyValueConverters.put(propertyId,
+ (Converter<String, Object>) converter);
+ refreshRowCache();
+ }
+
+ /**
+ * Checks if there is a converter set explicitly for the given property id.
+ *
+ * @param propertyId
+ * The propertyId to check
+ * @return true if a converter has been set for the property id, false
+ * otherwise
+ */
+ protected boolean hasConverter(Object propertyId) {
+ return propertyValueConverters.containsKey(propertyId);
+ }
+
+ /**
+ * Returns the converter used to format the given propertyId.
+ *
+ * @param propertyId
+ * The propertyId to check
+ * @return The converter used to format the propertyId or null if no
+ * converter has been set
+ */
+ public Converter<String, Object> getConverter(Object propertyId) {
+ return propertyValueConverters.get(propertyId);
+ }
+
+ @Override
+ public void setVisible(boolean visible) {
+ if (visible) {
+ // We need to ensure that the rows are sent to the client when the
+ // Table is made visible if it has been rendered as invisible.
+ setRowCacheInvalidated(true);
+ }
+ super.setVisible(visible);
+ }
+
+ @Override
+ public Iterator<Component> iterator() {
+ if (visibleComponents == null) {
+ Collection<Component> empty = Collections.emptyList();
+ return empty.iterator();
+ }
+ return visibleComponents.iterator();
+ }
+
+ /**
+ * @deprecated As of 7.0, use {@link #iterator()} instead.
+ */
+ @Deprecated
+ public Iterator<Component> getComponentIterator() {
+ return iterator();
+ }
+
+ @Override
+ public void readDesign(Element design, DesignContext context) {
+ super.readDesign(design, context);
+
+ if (design.hasAttr("sortable")) {
+ setSortEnabled(DesignAttributeHandler.readAttribute("sortable",
+ design.attributes(), boolean.class));
+ }
+
+ readColumns(design);
+ readHeader(design);
+ readBody(design, context);
+ readFooter(design);
+ }
+
+ private void readColumns(Element design) {
+ Element colgroup = design.select("> table > colgroup").first();
+
+ if (colgroup != null) {
+ int i = 0;
+ List<Object> pIds = new ArrayList<Object>();
+ for (Element col : colgroup.children()) {
+ if (!col.tagName().equals("col")) {
+ throw new DesignException("invalid column");
+ }
+
+ String id = DesignAttributeHandler.readAttribute("property-id",
+ col.attributes(), "property-" + i++, String.class);
+ pIds.add(id);
+
+ addContainerProperty(id, String.class, null);
+
+ if (col.hasAttr("width")) {
+ setColumnWidth(id, DesignAttributeHandler.readAttribute(
+ "width", col.attributes(), Integer.class));
+ }
+ if (col.hasAttr("center")) {
+ setColumnAlignment(id, Align.CENTER);
+ } else if (col.hasAttr("right")) {
+ setColumnAlignment(id, Align.RIGHT);
+ }
+ if (col.hasAttr("expand")) {
+ if (col.attr("expand").isEmpty()) {
+ setColumnExpandRatio(id, 1);
+ } else {
+ setColumnExpandRatio(id,
+ DesignAttributeHandler.readAttribute("expand",
+ col.attributes(), float.class));
+ }
+ }
+ if (col.hasAttr("collapsible")) {
+ setColumnCollapsible(id,
+ DesignAttributeHandler.readAttribute("collapsible",
+ col.attributes(), boolean.class));
+ }
+ if (col.hasAttr("collapsed")) {
+ setColumnCollapsed(id, DesignAttributeHandler.readAttribute(
+ "collapsed", col.attributes(), boolean.class));
+ }
+ }
+ setVisibleColumns(pIds.toArray());
+ }
+ }
+
+ private void readFooter(Element design) {
+ readHeaderOrFooter(design, false);
+ }
+
+ private void readHeader(Element design) {
+ readHeaderOrFooter(design, true);
+ }
+
+ @Override
+ protected void readItems(Element design, DesignContext context) {
+ // Do nothing - header/footer and inline data must be handled after
+ // colgroup.
+ }
+
+ private void readHeaderOrFooter(Element design, boolean header) {
+ String selector = header ? "> table > thead" : "> table > tfoot";
+ Element elem = design.select(selector).first();
+ if (elem != null) {
+ if (!header) {
+ setFooterVisible(true);
+ }
+ if (elem.children().size() != 1) {
+ throw new DesignException(
+ "Table header and footer should contain exactly one <tr> element");
+ }
+ Element tr = elem.child(0);
+ Elements elems = tr.children();
+ Collection<?> propertyIds = visibleColumns;
+ if (elems.size() != propertyIds.size()) {
+ throw new DesignException(
+ "Table header and footer should contain as many items as there"
+ + " are columns in the Table.");
+ }
+ Iterator<?> propertyIt = propertyIds.iterator();
+ for (Element e : elems) {
+ String columnValue = DesignFormatter
+ .decodeFromTextNode(e.html());
+ Object propertyId = propertyIt.next();
+ if (header) {
+ setColumnHeader(propertyId, columnValue);
+ if (e.hasAttr("icon")) {
+ setColumnIcon(propertyId,
+ DesignAttributeHandler.readAttribute("icon",
+ e.attributes(), Resource.class));
+ }
+ } else {
+ setColumnFooter(propertyId, columnValue);
+ }
+ }
+ }
+ }
+
+ protected void readBody(Element design, DesignContext context) {
+ Element tbody = design.select("> table > tbody").first();
+ if (tbody == null) {
+ return;
+ }
+
+ Set<String> selected = new HashSet<String>();
+ for (Element tr : tbody.children()) {
+ readItem(tr, selected, context);
+ }
+ }
+
+ @Override
+ protected Object readItem(Element tr, Set<String> selected,
+ DesignContext context) {
+ Elements cells = tr.children();
+ if (visibleColumns.size() != cells.size()) {
+ throw new DesignException(
+ "Wrong number of columns in a Table row. Expected "
+ + visibleColumns.size() + ", was " + cells.size()
+ + ".");
+ }
+ Object[] data = new String[cells.size()];
+ for (int c = 0; c < cells.size(); ++c) {
+ data[c] = DesignFormatter.decodeFromTextNode(cells.get(c).html());
+ }
+
+ Object itemId = addItem(data,
+ tr.hasAttr("item-id") ? tr.attr("item-id") : null);
+
+ if (itemId == null) {
+ throw new DesignException("Failed to add a Table row: " + data);
+ }
+
+ return itemId;
+ }
+
+ @Override
+ public void writeDesign(Element design, DesignContext context) {
+ Table def = context.getDefaultInstance(this);
+
+ DesignAttributeHandler.writeAttribute("sortable", design.attributes(),
+ isSortEnabled(), def.isSortEnabled(), boolean.class);
+
+ Element table = null;
+ boolean hasColumns = getVisibleColumns().length != 0;
+ if (hasColumns) {
+ table = design.appendElement("table");
+ writeColumns(table, def);
+ writeHeader(table, def);
+ }
+ super.writeDesign(design, context);
+ if (hasColumns) {
+ writeFooter(table);
+ }
+ }
+
+ private void writeColumns(Element table, Table def) {
+ Object[] columns = getVisibleColumns();
+ if (columns.length == 0) {
+ return;
+ }
+
+ Element colgroup = table.appendElement("colgroup");
+ for (Object id : columns) {
+ Element col = colgroup.appendElement("col");
+
+ col.attr("property-id", id.toString());
+
+ if (getColumnAlignment(id) == Align.CENTER) {
+ col.attr("center", true);
+ } else if (getColumnAlignment(id) == Align.RIGHT) {
+ col.attr("right", true);
+ }
+
+ DesignAttributeHandler.writeAttribute("width", col.attributes(),
+ getColumnWidth(id), def.getColumnWidth(null), int.class);
+
+ DesignAttributeHandler.writeAttribute("expand", col.attributes(),
+ getColumnExpandRatio(id), def.getColumnExpandRatio(null),
+ float.class);
+
+ DesignAttributeHandler.writeAttribute("collapsible",
+ col.attributes(), isColumnCollapsible(id),
+ def.isColumnCollapsible(null), boolean.class);
+
+ DesignAttributeHandler.writeAttribute("collapsed", col.attributes(),
+ isColumnCollapsed(id), def.isColumnCollapsed(null),
+ boolean.class);
+ }
+ }
+
+ private void writeHeader(Element table, Table def) {
+ Object[] columns = getVisibleColumns();
+ if (columns.length == 0
+ || (columnIcons.isEmpty() && columnHeaders.isEmpty())) {
+ return;
+ }
+
+ Element header = table.appendElement("thead").appendElement("tr");
+ for (Object id : columns) {
+ Element th = header.appendElement("th");
+ th.html(getColumnHeader(id));
+ DesignAttributeHandler.writeAttribute("icon", th.attributes(),
+ getColumnIcon(id), def.getColumnIcon(null), Resource.class);
+ }
+
+ }
+
+ private void writeFooter(Element table) {
+ Object[] columns = getVisibleColumns();
+ if (columns.length == 0 || columnFooters.isEmpty()) {
+ return;
+ }
+
+ Element footer = table.appendElement("tfoot").appendElement("tr");
+ for (Object id : columns) {
+ footer.appendElement("td").text(getColumnFooter(id));
+ }
+ }
+
+ @Override
+ protected void writeItems(Element design, DesignContext context) {
+ if (getVisibleColumns().length == 0) {
+ return;
+ }
+ Element tbody = design.child(0).appendElement("tbody");
+ super.writeItems(tbody, context);
+ }
+
+ @Override
+ protected Element writeItem(Element tbody, Object itemId,
+ DesignContext context) {
+ Element tr = tbody.appendElement("tr");
+ tr.attr("item-id", String.valueOf(itemId));
+ Item item = getItem(itemId);
+ for (Object id : getVisibleColumns()) {
+ Element td = tr.appendElement("td");
+ Object value = item.getItemProperty(id).getValue();
+ td.html(value != null ? value.toString() : "");
+ }
+ return tr;
+ }
+
+ @Override
+ protected Collection<String> getCustomAttributes() {
+ Collection<String> result = super.getCustomAttributes();
+ result.add("sortable");
+ result.add("sort-enabled");
+ result.add("sort-disabled");
+ result.add("footer-visible");
+ result.add("item-caption-mode");
+ result.add("current-page-first-item-id");
+ result.add("current-page-first-item-index");
+ return result;
+ }
+
+ /**
+ * ContextClickEvent for the Table Component.
+ *
+ * @since 7.6
+ */
+ public static class TableContextClickEvent extends ContextClickEvent {
+
+ private final Object itemId;
+ private final Object propertyId;
+ private final Section section;
+
+ public TableContextClickEvent(Table source,
+ MouseEventDetails mouseEventDetails, Object itemId,
+ Object propertyId, Section section) {
+ super(source, mouseEventDetails);
+
+ this.itemId = itemId;
+ this.propertyId = propertyId;
+ this.section = section;
+ }
+
+ /**
+ * Returns the item id of context clicked row.
+ *
+ * @return item id of clicked row; <code>null</code> if header, footer
+ * or empty area of Table
+ */
+ public Object getItemId() {
+ return itemId;
+ }
+
+ /**
+ * Returns the property id of context clicked column.
+ *
+ * @return property id; or <code>null</code> if we've clicked on the
+ * empty area of the Table
+ */
+ public Object getPropertyId() {
+ return propertyId;
+ }
+
+ /**
+ * Returns the clicked section of Table.
+ *
+ * @return section of Table
+ */
+ public Section getSection() {
+ return section;
+ }
+
+ @Override
+ public Table getComponent() {
+ return (Table) super.getComponent();
+ }
+ }
+
+ @Override
+ protected TableState getState() {
+ return getState(true);
+ }
+
+ @Override
+ protected TableState getState(boolean markAsDirty) {
+ return (TableState) super.getState(markAsDirty);
+ }
+
+ private final Logger getLogger() {
+ if (logger == null) {
+ logger = Logger.getLogger(Table.class.getName());
+ }
+ return logger;
+ }
+
+ @Override
+ public void setChildMeasurementHint(ChildMeasurementHint hint) {
+ if (hint == null) {
+ childMeasurementHint = ChildMeasurementHint.MEASURE_ALWAYS;
+ } else {
+ childMeasurementHint = hint;
+ }
+ }
+
+ @Override
+ public ChildMeasurementHint getChildMeasurementHint() {
+ return childMeasurementHint;
+ }
+
+ /**
+ * Sets whether only collapsible columns should be shown to the user in the
+ * column collapse menu. The default is
+ * {@link CollapseMenuContent#ALL_COLUMNS}.
+ *
+ *
+ * @since 7.6
+ * @param content
+ * the desired collapsible menu content setting
+ */
+ public void setCollapseMenuContent(CollapseMenuContent content) {
+ getState().collapseMenuContent = content;
+ }
+
+ /**
+ * Checks whether only collapsible columns are shown to the user in the
+ * column collapse menu. The default is
+ * {@link CollapseMenuContent#ALL_COLUMNS} .
+ *
+ * @since 7.6
+ * @return the current collapsible menu content setting
+ */
+ public CollapseMenuContent getCollapseMenuContent() {
+ return getState(false).collapseMenuContent;
+ }
+}
diff --git a/compatibility-server/src/main/java/com/vaadin/v7/ui/TableFieldFactory.java b/compatibility-server/src/main/java/com/vaadin/v7/ui/TableFieldFactory.java
new file mode 100644
index 0000000000..1ed286738d
--- /dev/null
+++ b/compatibility-server/src/main/java/com/vaadin/v7/ui/TableFieldFactory.java
@@ -0,0 +1,56 @@
+/*
+ * 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.v7.ui;
+
+import java.io.Serializable;
+
+import com.vaadin.ui.Component;
+import com.vaadin.v7.data.Container;
+
+/**
+ * Factory interface for creating new LegacyField-instances based on Container
+ * (datasource), item id, property id and uiContext (the component responsible
+ * for displaying fields). Currently this interface is used by {@link Table},
+ * but might later be used by some other components for {@link Field}
+ * generation.
+ *
+ * <p>
+ *
+ * @author Vaadin Ltd.
+ * @since 6.0
+ * @see FormFieldFactory
+ */
+public interface TableFieldFactory extends Serializable {
+ /**
+ * Creates a field based on the Container, item id, property id and the
+ * component responsible for displaying the field (most commonly
+ * {@link Table}).
+ *
+ * @param container
+ * the Container where the property belongs to.
+ * @param itemId
+ * the item Id.
+ * @param propertyId
+ * the Id of the property.
+ * @param uiContext
+ * the component where the field is presented.
+ * @return A field suitable for editing the specified data or null if the
+ * property should not be editable.
+ */
+ Field<?> createField(Container container, Object itemId,
+ Object propertyId, Component uiContext);
+
+}
diff --git a/compatibility-server/src/main/java/com/vaadin/v7/ui/TextArea.java b/compatibility-server/src/main/java/com/vaadin/v7/ui/TextArea.java
new file mode 100644
index 0000000000..e4cd20a59b
--- /dev/null
+++ b/compatibility-server/src/main/java/com/vaadin/v7/ui/TextArea.java
@@ -0,0 +1,170 @@
+/*
+ * 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.v7.ui;
+
+import org.jsoup.nodes.Element;
+
+import com.vaadin.shared.ui.textarea.TextAreaState;
+import com.vaadin.ui.declarative.DesignContext;
+import com.vaadin.ui.declarative.DesignFormatter;
+import com.vaadin.v7.data.Property;
+
+/**
+ * A text field that supports multi line editing.
+ */
+public class TextArea extends AbstractTextField {
+
+ /**
+ * Constructs an empty TextArea.
+ */
+ public TextArea() {
+ setValue("");
+ }
+
+ /**
+ * Constructs an empty TextArea with given caption.
+ *
+ * @param caption
+ * the caption for the field.
+ */
+ public TextArea(String caption) {
+ this();
+ setCaption(caption);
+ }
+
+ /**
+ * Constructs a TextArea with given property data source.
+ *
+ * @param dataSource
+ * the data source for the field
+ */
+ public TextArea(Property dataSource) {
+ this();
+ setPropertyDataSource(dataSource);
+ }
+
+ /**
+ * Constructs a TextArea with given caption and property data source.
+ *
+ * @param caption
+ * the caption for the field
+ * @param dataSource
+ * the data source for the field
+ */
+ public TextArea(String caption, Property dataSource) {
+ this(dataSource);
+ setCaption(caption);
+ }
+
+ /**
+ * Constructs a TextArea with given caption and value.
+ *
+ * @param caption
+ * the caption for the field
+ * @param value
+ * the value for the field
+ */
+ public TextArea(String caption, String value) {
+ this(caption);
+ setValue(value);
+
+ }
+
+ @Override
+ protected TextAreaState getState() {
+ return (TextAreaState) super.getState();
+ }
+
+ @Override
+ protected TextAreaState getState(boolean markAsDirty) {
+ return (TextAreaState) super.getState(markAsDirty);
+ }
+
+ /**
+ * Sets the number of rows in the text area.
+ *
+ * @param rows
+ * the number of rows for this text area.
+ */
+ public void setRows(int rows) {
+ if (rows < 0) {
+ rows = 0;
+ }
+ getState().rows = rows;
+ }
+
+ /**
+ * Gets the number of rows in the text area.
+ *
+ * @return number of explicitly set rows.
+ */
+ public int getRows() {
+ return getState(false).rows;
+ }
+
+ /**
+ * Sets the text area's word-wrap mode on or off.
+ *
+ * @param wordwrap
+ * the boolean value specifying if the text area should be in
+ * word-wrap mode.
+ */
+ public void setWordwrap(boolean wordwrap) {
+ getState().wordwrap = wordwrap;
+ }
+
+ /**
+ * Tests if the text area is in word-wrap mode.
+ *
+ * @return <code>true</code> if the component is in word-wrap mode,
+ * <code>false</code> if not.
+ */
+ public boolean isWordwrap() {
+ return getState(false).wordwrap;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.ui.AbstractField#readDesign(org.jsoup.nodes.Element ,
+ * com.vaadin.ui.declarative.DesignContext)
+ */
+ @Override
+ public void readDesign(Element design, DesignContext designContext) {
+ super.readDesign(design, designContext);
+ setValue(DesignFormatter.decodeFromTextNode(design.html()), false,
+ true);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.ui.AbstractTextField#writeDesign(org.jsoup.nodes.Element
+ * , com.vaadin.ui.declarative.DesignContext)
+ */
+ @Override
+ public void writeDesign(Element design, DesignContext designContext) {
+ super.writeDesign(design, designContext);
+ design.html(DesignFormatter.encodeForTextNode(getValue()));
+ }
+
+ @Override
+ public void clear() {
+ setValue("");
+ }
+
+}
diff --git a/compatibility-server/src/main/java/com/vaadin/v7/ui/Tree.java b/compatibility-server/src/main/java/com/vaadin/v7/ui/Tree.java
new file mode 100644
index 0000000000..83f805ffb4
--- /dev/null
+++ b/compatibility-server/src/main/java/com/vaadin/v7/ui/Tree.java
@@ -0,0 +1,1985 @@
+/*
+ * 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.v7.ui;
+
+import java.io.Serializable;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.LinkedList;
+import java.util.Map;
+import java.util.Set;
+import java.util.Stack;
+import java.util.StringTokenizer;
+
+import org.jsoup.nodes.Element;
+
+import com.vaadin.event.Action;
+import com.vaadin.event.Action.Handler;
+import com.vaadin.event.ContextClickEvent;
+import com.vaadin.event.DataBoundTransferable;
+import com.vaadin.event.ItemClickEvent;
+import com.vaadin.event.ItemClickEvent.ItemClickListener;
+import com.vaadin.event.ItemClickEvent.ItemClickNotifier;
+import com.vaadin.event.Transferable;
+import com.vaadin.event.dd.DragAndDropEvent;
+import com.vaadin.event.dd.DragSource;
+import com.vaadin.event.dd.DropHandler;
+import com.vaadin.event.dd.DropTarget;
+import com.vaadin.event.dd.TargetDetails;
+import com.vaadin.event.dd.acceptcriteria.ClientSideCriterion;
+import com.vaadin.event.dd.acceptcriteria.ServerSideCriterion;
+import com.vaadin.event.dd.acceptcriteria.TargetDetailIs;
+import com.vaadin.server.KeyMapper;
+import com.vaadin.server.PaintException;
+import com.vaadin.server.PaintTarget;
+import com.vaadin.server.Resource;
+import com.vaadin.shared.MouseEventDetails;
+import com.vaadin.shared.ui.MultiSelectMode;
+import com.vaadin.shared.ui.dd.VerticalDropLocation;
+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.Component;
+import com.vaadin.ui.declarative.DesignAttributeHandler;
+import com.vaadin.ui.declarative.DesignContext;
+import com.vaadin.ui.declarative.DesignException;
+import com.vaadin.util.ReflectTools;
+import com.vaadin.v7.data.Container;
+import com.vaadin.v7.data.Item;
+import com.vaadin.v7.data.util.ContainerHierarchicalWrapper;
+import com.vaadin.v7.data.util.HierarchicalContainer;
+
+/**
+ * Tree component. A Tree can be used to select an item (or multiple items) from
+ * a hierarchical set of items.
+ *
+ * @author Vaadin Ltd.
+ * @since 3.0
+ */
+@SuppressWarnings({ "serial", "deprecation" })
+public class Tree extends AbstractSelect implements Container.Hierarchical,
+ Action.Container, ItemClickNotifier, DragSource, DropTarget {
+
+ /**
+ * ContextClickEvent for the Tree Component.
+ *
+ * @since 7.6
+ */
+ public static class TreeContextClickEvent extends ContextClickEvent {
+
+ private final Object itemId;
+
+ public TreeContextClickEvent(Tree source, Object itemId,
+ MouseEventDetails mouseEventDetails) {
+ super(source, mouseEventDetails);
+ this.itemId = itemId;
+ }
+
+ @Override
+ public Tree getComponent() {
+ return (Tree) super.getComponent();
+ }
+
+ /**
+ * Returns the item id of context clicked row.
+ *
+ * @return item id of clicked row; <code>null</code> if no row is
+ * present at the location
+ */
+ public Object getItemId() {
+ return itemId;
+ }
+ }
+
+ /* Private members */
+
+ private static final String NULL_ALT_EXCEPTION_MESSAGE = "Parameter 'altText' needs to be non null";
+
+ /**
+ * Item icons alt texts.
+ */
+ private final HashMap<Object, String> itemIconAlts = new HashMap<Object, String>();
+
+ /**
+ * Set of expanded nodes.
+ */
+ private HashSet<Object> expanded = new HashSet<Object>();
+
+ /**
+ * List of action handlers.
+ */
+ private LinkedList<Action.Handler> actionHandlers = null;
+
+ /**
+ * Action mapper.
+ */
+ private KeyMapper<Action> actionMapper = null;
+
+ /**
+ * Is the tree selectable on the client side.
+ */
+ private boolean selectable = true;
+
+ /**
+ * Flag to indicate sub-tree loading
+ */
+ private boolean partialUpdate = false;
+
+ /**
+ * Holds a itemId which was recently expanded
+ */
+ private Object expandedItemId;
+
+ /**
+ * a flag which indicates initial paint. After this flag set true partial
+ * updates are allowed.
+ */
+ private boolean initialPaint = true;
+
+ /**
+ * Item tooltip generator
+ */
+ private ItemDescriptionGenerator itemDescriptionGenerator;
+
+ /**
+ * Supported drag modes for Tree.
+ */
+ public enum TreeDragMode {
+ /**
+ * When drag mode is NONE, dragging from Tree is not supported. Browsers
+ * may still support selecting text/icons from Tree which can initiate
+ * HTML 5 style drag and drop operation.
+ */
+ NONE,
+ /**
+ * When drag mode is NODE, users can initiate drag from Tree nodes that
+ * represent {@link Item}s in from the backed {@link Container}.
+ */
+ NODE
+ // , SUBTREE
+ }
+
+ private TreeDragMode dragMode = TreeDragMode.NONE;
+
+ private MultiSelectMode multiSelectMode = MultiSelectMode.DEFAULT;
+
+ /* Tree constructors */
+
+ /**
+ * Creates a new empty tree.
+ */
+ public Tree() {
+ this(null);
+
+ registerRpc(new TreeServerRpc() {
+ @Override
+ public void contextClick(String rowKey, MouseEventDetails details) {
+ fireEvent(new TreeContextClickEvent(Tree.this,
+ itemIdMapper.get(rowKey), details));
+ }
+ });
+ }
+
+ /**
+ * Creates a new empty tree with caption.
+ *
+ * @param caption
+ */
+ public Tree(String caption) {
+ this(caption, new HierarchicalContainer());
+ }
+
+ /**
+ * Creates a new tree with caption and connect it to a Container.
+ *
+ * @param caption
+ * @param dataSource
+ */
+ public Tree(String caption, Container dataSource) {
+ super(caption, dataSource);
+ }
+
+ @Override
+ public void setItemIcon(Object itemId, Resource icon) {
+ setItemIcon(itemId, icon, "");
+ }
+
+ /**
+ * Sets the icon for an item.
+ *
+ * @param itemId
+ * the id of the item to be assigned an icon.
+ * @param icon
+ * the icon to use or null.
+ *
+ * @param altText
+ * the alternative text for the icon
+ */
+ public void setItemIcon(Object itemId, Resource icon, String altText) {
+ if (itemId != null) {
+ super.setItemIcon(itemId, icon);
+
+ if (icon == null) {
+ itemIconAlts.remove(itemId);
+ } else if (altText == null) {
+ throw new IllegalArgumentException(NULL_ALT_EXCEPTION_MESSAGE);
+ } else {
+ itemIconAlts.put(itemId, altText);
+ }
+ markAsDirty();
+ }
+ }
+
+ /**
+ * Set the alternate text for an item.
+ *
+ * Used when the item has an icon.
+ *
+ * @param itemId
+ * the id of the item to be assigned an icon.
+ * @param altText
+ * the alternative text for the icon
+ */
+ public void setItemIconAlternateText(Object itemId, String altText) {
+ if (itemId != null) {
+ if (altText == null) {
+ throw new IllegalArgumentException(NULL_ALT_EXCEPTION_MESSAGE);
+ } else {
+ itemIconAlts.put(itemId, altText);
+ }
+ }
+ }
+
+ /**
+ * Return the alternate text of an icon in a tree item.
+ *
+ * @param itemId
+ * Object with the ID of the item
+ * @return String with the alternate text of the icon, or null when no icon
+ * was set
+ */
+ public String getItemIconAlternateText(Object itemId) {
+ String storedAlt = itemIconAlts.get(itemId);
+ return storedAlt == null ? "" : storedAlt;
+ }
+
+ /* Expanding and collapsing */
+
+ /**
+ * Check is an item is expanded
+ *
+ * @param itemId
+ * the item id.
+ * @return true iff the item is expanded.
+ */
+ public boolean isExpanded(Object itemId) {
+ return expanded.contains(itemId);
+ }
+
+ /**
+ * Expands an item.
+ *
+ * @param itemId
+ * the item id.
+ * @return True iff the expand operation succeeded
+ */
+ public boolean expandItem(Object itemId) {
+ boolean success = expandItem(itemId, true);
+ markAsDirty();
+ return success;
+ }
+
+ /**
+ * Expands an item.
+ *
+ * @param itemId
+ * the item id.
+ * @param sendChildTree
+ * flag to indicate if client needs subtree or not (may be
+ * cached)
+ * @return True if the expand operation succeeded
+ */
+ private boolean expandItem(Object itemId, boolean sendChildTree) {
+
+ // Succeeds if the node is already expanded
+ if (isExpanded(itemId)) {
+ return true;
+ }
+
+ // Nodes that can not have children are not expandable
+ if (!areChildrenAllowed(itemId)) {
+ return false;
+ }
+
+ // Expands
+ expanded.add(itemId);
+
+ expandedItemId = itemId;
+ if (initialPaint) {
+ markAsDirty();
+ } else if (sendChildTree) {
+ requestPartialRepaint();
+ }
+ fireExpandEvent(itemId);
+
+ return true;
+ }
+
+ @Override
+ public void markAsDirty() {
+ super.markAsDirty();
+ partialUpdate = false;
+ }
+
+ private void requestPartialRepaint() {
+ super.markAsDirty();
+ partialUpdate = true;
+ }
+
+ /**
+ * Expands the items recursively
+ *
+ * Expands all the children recursively starting from an item. Operation
+ * succeeds only if all expandable items are expanded.
+ *
+ * @param startItemId
+ * @return True iff the expand operation succeeded
+ */
+ public boolean expandItemsRecursively(Object startItemId) {
+
+ boolean result = true;
+
+ // Initial stack
+ final Stack<Object> todo = new Stack<Object>();
+ todo.add(startItemId);
+
+ // Expands recursively
+ while (!todo.isEmpty()) {
+ final Object id = todo.pop();
+ if (areChildrenAllowed(id) && !expandItem(id, false)) {
+ result = false;
+ }
+ if (hasChildren(id)) {
+ todo.addAll(getChildren(id));
+ }
+ }
+ markAsDirty();
+ return result;
+ }
+
+ /**
+ * Collapses an item.
+ *
+ * @param itemId
+ * the item id.
+ * @return True iff the collapse operation succeeded
+ */
+ public boolean collapseItem(Object itemId) {
+
+ // Succeeds if the node is already collapsed
+ if (!isExpanded(itemId)) {
+ return true;
+ }
+
+ // Collapse
+ expanded.remove(itemId);
+ markAsDirty();
+ fireCollapseEvent(itemId);
+
+ return true;
+ }
+
+ /**
+ * Collapses the items recursively.
+ *
+ * Collapse all the children recursively starting from an item. Operation
+ * succeeds only if all expandable items are collapsed.
+ *
+ * @param startItemId
+ * @return True iff the collapse operation succeeded
+ */
+ public boolean collapseItemsRecursively(Object startItemId) {
+
+ boolean result = true;
+
+ // Initial stack
+ final Stack<Object> todo = new Stack<Object>();
+ todo.add(startItemId);
+
+ // Collapse recursively
+ while (!todo.isEmpty()) {
+ final Object id = todo.pop();
+ if (areChildrenAllowed(id) && !collapseItem(id)) {
+ result = false;
+ }
+ if (hasChildren(id)) {
+ todo.addAll(getChildren(id));
+ }
+ }
+
+ return result;
+ }
+
+ /**
+ * Returns the current selectable state. Selectable determines if the a node
+ * can be selected on the client side. Selectable does not affect
+ * {@link #setValue(Object)} or {@link #select(Object)}.
+ *
+ * <p>
+ * The tree is selectable by default.
+ * </p>
+ *
+ * @return the current selectable state.
+ */
+ public boolean isSelectable() {
+ return selectable;
+ }
+
+ /**
+ * Sets the selectable state. Selectable determines if the a node can be
+ * selected on the client side. Selectable does not affect
+ * {@link #setValue(Object)} or {@link #select(Object)}.
+ *
+ * <p>
+ * The tree is selectable by default.
+ * </p>
+ *
+ * @param selectable
+ * The new selectable state.
+ */
+ public void setSelectable(boolean selectable) {
+ if (this.selectable != selectable) {
+ this.selectable = selectable;
+ markAsDirty();
+ }
+ }
+
+ /**
+ * Sets the behavior of the multiselect mode
+ *
+ * @param mode
+ * The mode to set
+ */
+ public void setMultiselectMode(MultiSelectMode mode) {
+ if (multiSelectMode != mode && mode != null) {
+ multiSelectMode = mode;
+ markAsDirty();
+ }
+ }
+
+ /**
+ * Returns the mode the multiselect is in. The mode controls how
+ * multiselection can be done.
+ *
+ * @return The mode
+ */
+ public MultiSelectMode getMultiselectMode() {
+ return multiSelectMode;
+ }
+
+ /* Component API */
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.ui.AbstractSelect#changeVariables(java.lang.Object,
+ * java.util.Map)
+ */
+ @Override
+ public void changeVariables(Object source, Map<String, Object> variables) {
+
+ if (variables.containsKey("clickedKey")) {
+ String key = (String) variables.get("clickedKey");
+
+ Object id = itemIdMapper.get(key);
+ MouseEventDetails details = MouseEventDetails
+ .deSerialize((String) variables.get("clickEvent"));
+ Item item = getItem(id);
+ if (item != null) {
+ fireEvent(new ItemClickEvent(this, item, id, null, details));
+ }
+ }
+
+ if (!isSelectable() && variables.containsKey("selected")) {
+ // Not-selectable is a special case, AbstractSelect does not support
+ // TODO could be optimized.
+ variables = new HashMap<String, Object>(variables);
+ variables.remove("selected");
+ }
+
+ // Collapses the nodes
+ if (variables.containsKey("collapse")) {
+ final String[] keys = (String[]) variables.get("collapse");
+ for (int i = 0; i < keys.length; i++) {
+ final Object id = itemIdMapper.get(keys[i]);
+ if (id != null && isExpanded(id)) {
+ expanded.remove(id);
+ if (expandedItemId == id) {
+ expandedItemId = null;
+ }
+ fireCollapseEvent(id);
+ }
+ }
+ }
+
+ // Expands the nodes
+ if (variables.containsKey("expand")) {
+ boolean sendChildTree = false;
+ if (variables.containsKey("requestChildTree")) {
+ sendChildTree = true;
+ }
+ final String[] keys = (String[]) variables.get("expand");
+ for (int i = 0; i < keys.length; i++) {
+ final Object id = itemIdMapper.get(keys[i]);
+ if (id != null) {
+ expandItem(id, sendChildTree);
+ }
+ }
+ }
+
+ // AbstractSelect cannot handle multiselection so we handle
+ // it ourself
+ if (variables.containsKey("selected") && isMultiSelect()
+ && multiSelectMode == MultiSelectMode.DEFAULT) {
+ handleSelectedItems(variables);
+ variables = new HashMap<String, Object>(variables);
+ variables.remove("selected");
+ }
+
+ // Selections are handled by the select component
+ super.changeVariables(source, variables);
+
+ // Actions
+ if (variables.containsKey("action")) {
+ final StringTokenizer st = new StringTokenizer(
+ (String) variables.get("action"), ",");
+ if (st.countTokens() == 2) {
+ final Object itemId = itemIdMapper.get(st.nextToken());
+ final Action action = actionMapper.get(st.nextToken());
+ if (action != null && (itemId == null || containsId(itemId))
+ && actionHandlers != null) {
+ for (Handler ah : actionHandlers) {
+ ah.handleAction(action, this, itemId);
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Handles the selection
+ *
+ * @param variables
+ * The variables sent to the server from the client
+ */
+ private void handleSelectedItems(Map<String, Object> variables) {
+ final String[] ka = (String[]) variables.get("selected");
+
+ // Converts the key-array to id-set
+ final LinkedList<Object> s = new LinkedList<Object>();
+ for (int i = 0; i < ka.length; i++) {
+ final Object id = itemIdMapper.get(ka[i]);
+ if (!isNullSelectionAllowed()
+ && (id == null || id == getNullSelectionItemId())) {
+ // skip empty selection if nullselection is not allowed
+ markAsDirty();
+ } else if (id != null && containsId(id)) {
+ s.add(id);
+ }
+ }
+
+ if (!isNullSelectionAllowed() && s.size() < 1) {
+ // empty selection not allowed, keep old value
+ markAsDirty();
+ return;
+ }
+
+ setValue(s, true);
+ }
+
+ /**
+ * Paints any needed component-specific things to the given UIDL stream.
+ *
+ * @see com.vaadin.ui.AbstractComponent#paintContent(PaintTarget)
+ */
+ @Override
+ public void paintContent(PaintTarget target) throws PaintException {
+ initialPaint = false;
+
+ if (partialUpdate) {
+ target.addAttribute("partialUpdate", true);
+ target.addAttribute("rootKey", itemIdMapper.key(expandedItemId));
+ } else {
+ getCaptionChangeListener().clear();
+
+ // The tab ordering number
+ if (getTabIndex() > 0) {
+ target.addAttribute("tabindex", getTabIndex());
+ }
+
+ // Paint tree attributes
+ if (isSelectable()) {
+ target.addAttribute("selectmode",
+ (isMultiSelect() ? "multi" : "single"));
+ if (isMultiSelect()) {
+ target.addAttribute("multiselectmode",
+ multiSelectMode.toString());
+ }
+ } else {
+ target.addAttribute("selectmode", "none");
+ }
+ if (isNewItemsAllowed()) {
+ target.addAttribute("allownewitem", true);
+ }
+
+ if (isNullSelectionAllowed()) {
+ target.addAttribute("nullselect", true);
+ }
+
+ if (dragMode != TreeDragMode.NONE) {
+ target.addAttribute("dragMode", dragMode.ordinal());
+ }
+
+ if (isHtmlContentAllowed()) {
+ target.addAttribute(TreeConstants.ATTRIBUTE_HTML_ALLOWED, true);
+ }
+
+ }
+
+ // Initialize variables
+ final Set<Action> actionSet = new LinkedHashSet<Action>();
+
+ // rendered selectedKeys
+ LinkedList<String> selectedKeys = new LinkedList<String>();
+
+ final LinkedList<String> expandedKeys = new LinkedList<String>();
+
+ // Iterates through hierarchical tree using a stack of iterators
+ final Stack<Iterator<?>> iteratorStack = new Stack<Iterator<?>>();
+ Collection<?> ids;
+ if (partialUpdate) {
+ ids = getChildren(expandedItemId);
+ } else {
+ ids = rootItemIds();
+ }
+
+ if (ids != null) {
+ iteratorStack.push(ids.iterator());
+ }
+
+ /*
+ * Body actions - Actions which has the target null and can be invoked
+ * by right clicking on the Tree body
+ */
+ if (actionHandlers != null) {
+ final ArrayList<String> keys = new ArrayList<String>();
+ for (Handler ah : actionHandlers) {
+
+ // Getting action for the null item, which in this case
+ // means the body item
+ final Action[] aa = ah.getActions(null, this);
+ if (aa != null) {
+ for (int ai = 0; ai < aa.length; ai++) {
+ final String akey = actionMapper.key(aa[ai]);
+ actionSet.add(aa[ai]);
+ keys.add(akey);
+ }
+ }
+ }
+ target.addAttribute("alb", keys.toArray());
+ }
+
+ while (!iteratorStack.isEmpty()) {
+
+ // Gets the iterator for current tree level
+ final Iterator<?> i = iteratorStack.peek();
+
+ // If the level is finished, back to previous tree level
+ if (!i.hasNext()) {
+
+ // Removes used iterator from the stack
+ iteratorStack.pop();
+
+ // Closes node
+ if (!iteratorStack.isEmpty()) {
+ target.endTag("node");
+ }
+ }
+
+ // Adds the item on current level
+ else {
+ final Object itemId = i.next();
+
+ // Starts the item / node
+ final boolean isNode = areChildrenAllowed(itemId);
+ if (isNode) {
+ target.startTag("node");
+ } else {
+ target.startTag("leaf");
+ }
+
+ if (itemStyleGenerator != null) {
+ String stylename = itemStyleGenerator.getStyle(this,
+ itemId);
+ if (stylename != null) {
+ target.addAttribute(TreeConstants.ATTRIBUTE_NODE_STYLE,
+ stylename);
+ }
+ }
+
+ if (itemDescriptionGenerator != null) {
+ String description = itemDescriptionGenerator
+ .generateDescription(this, itemId, null);
+ if (description != null && !description.equals("")) {
+ target.addAttribute("descr", description);
+ }
+ }
+
+ // Adds the attributes
+ target.addAttribute(TreeConstants.ATTRIBUTE_NODE_CAPTION,
+ getItemCaption(itemId));
+ final Resource icon = getItemIcon(itemId);
+ if (icon != null) {
+ target.addAttribute(TreeConstants.ATTRIBUTE_NODE_ICON,
+ getItemIcon(itemId));
+ target.addAttribute(TreeConstants.ATTRIBUTE_NODE_ICON_ALT,
+ getItemIconAlternateText(itemId));
+ }
+ final String key = itemIdMapper.key(itemId);
+ target.addAttribute("key", key);
+ if (isSelected(itemId)) {
+ target.addAttribute("selected", true);
+ selectedKeys.add(key);
+ }
+ if (areChildrenAllowed(itemId) && isExpanded(itemId)) {
+ target.addAttribute("expanded", true);
+ expandedKeys.add(key);
+ }
+
+ // Add caption change listener
+ getCaptionChangeListener().addNotifierForItem(itemId);
+
+ // Actions
+ if (actionHandlers != null) {
+ final ArrayList<String> keys = new ArrayList<String>();
+ final Iterator<Action.Handler> ahi = actionHandlers
+ .iterator();
+ while (ahi.hasNext()) {
+ final Action[] aa = ahi.next().getActions(itemId, this);
+ if (aa != null) {
+ for (int ai = 0; ai < aa.length; ai++) {
+ final String akey = actionMapper.key(aa[ai]);
+ actionSet.add(aa[ai]);
+ keys.add(akey);
+ }
+ }
+ }
+ target.addAttribute("al", keys.toArray());
+ }
+
+ // Adds the children if expanded, or close the tag
+ if (isExpanded(itemId) && hasChildren(itemId)
+ && areChildrenAllowed(itemId)) {
+ iteratorStack.push(getChildren(itemId).iterator());
+ } else {
+ if (isNode) {
+ target.endTag("node");
+ } else {
+ target.endTag("leaf");
+ }
+ }
+ }
+ }
+
+ // Actions
+ if (!actionSet.isEmpty()) {
+ target.addVariable(this, "action", "");
+ target.startTag("actions");
+ final Iterator<Action> i = actionSet.iterator();
+ while (i.hasNext()) {
+ final Action a = i.next();
+ target.startTag("action");
+ if (a.getCaption() != null) {
+ target.addAttribute(TreeConstants.ATTRIBUTE_ACTION_CAPTION,
+ a.getCaption());
+ }
+ if (a.getIcon() != null) {
+ target.addAttribute(TreeConstants.ATTRIBUTE_ACTION_ICON,
+ a.getIcon());
+ }
+ target.addAttribute("key", actionMapper.key(a));
+ target.endTag("action");
+ }
+ target.endTag("actions");
+ }
+
+ if (partialUpdate) {
+ partialUpdate = false;
+ } else {
+ // Selected
+ target.addVariable(this, "selected",
+ selectedKeys.toArray(new String[selectedKeys.size()]));
+
+ // Expand and collapse
+ target.addVariable(this, "expand", new String[] {});
+ target.addVariable(this, "collapse", new String[] {});
+
+ // New items
+ target.addVariable(this, "newitem", new String[] {});
+
+ if (dropHandler != null) {
+ dropHandler.getAcceptCriterion().paint(target);
+ }
+
+ }
+ }
+
+ /* Container.Hierarchical API */
+
+ /**
+ * Tests if the Item with given ID can have any children.
+ *
+ * @see com.vaadin.v7.data.Container.Hierarchical#areChildrenAllowed(Object)
+ */
+ @Override
+ public boolean areChildrenAllowed(Object itemId) {
+ return ((Container.Hierarchical) items).areChildrenAllowed(itemId);
+ }
+
+ /**
+ * Gets the IDs of all Items that are children of the specified Item.
+ *
+ * @see com.vaadin.v7.data.Container.Hierarchical#getChildren(Object)
+ */
+ @Override
+ public Collection<?> getChildren(Object itemId) {
+ return ((Container.Hierarchical) items).getChildren(itemId);
+ }
+
+ /**
+ * Gets the ID of the parent Item of the specified Item.
+ *
+ * @see com.vaadin.v7.data.Container.Hierarchical#getParent(Object)
+ */
+ @Override
+ public Object getParent(Object itemId) {
+ return ((Container.Hierarchical) items).getParent(itemId);
+ }
+
+ /**
+ * Tests if the Item specified with <code>itemId</code> has child Items.
+ *
+ * @see com.vaadin.v7.data.Container.Hierarchical#hasChildren(Object)
+ */
+ @Override
+ public boolean hasChildren(Object itemId) {
+ return ((Container.Hierarchical) items).hasChildren(itemId);
+ }
+
+ /**
+ * Tests if the Item specified with <code>itemId</code> is a root Item.
+ *
+ * @see com.vaadin.v7.data.Container.Hierarchical#isRoot(Object)
+ */
+ @Override
+ public boolean isRoot(Object itemId) {
+ return ((Container.Hierarchical) items).isRoot(itemId);
+ }
+
+ /**
+ * Gets the IDs of all Items in the container that don't have a parent.
+ *
+ * @see com.vaadin.v7.data.Container.Hierarchical#rootItemIds()
+ */
+ @Override
+ public Collection<?> rootItemIds() {
+ return ((Container.Hierarchical) items).rootItemIds();
+ }
+
+ /**
+ * Sets the given Item's capability to have children.
+ *
+ * @see com.vaadin.v7.data.Container.Hierarchical#setChildrenAllowed(Object,
+ * boolean)
+ */
+ @Override
+ public boolean setChildrenAllowed(Object itemId,
+ boolean areChildrenAllowed) {
+ final boolean success = ((Container.Hierarchical) items)
+ .setChildrenAllowed(itemId, areChildrenAllowed);
+ if (success) {
+ markAsDirty();
+ }
+ return success;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.data.Container.Hierarchical#setParent(java.lang.Object ,
+ * java.lang.Object)
+ */
+ @Override
+ public boolean setParent(Object itemId, Object newParentId) {
+ final boolean success = ((Container.Hierarchical) items)
+ .setParent(itemId, newParentId);
+ if (success) {
+ markAsDirty();
+ }
+ return success;
+ }
+
+ /* Overriding select behavior */
+
+ /**
+ * Sets the Container that serves as the data source of the viewer.
+ *
+ * @see com.vaadin.v7.data.Container.Viewer#setContainerDataSource(Container)
+ */
+ @Override
+ public void setContainerDataSource(Container newDataSource) {
+ if (newDataSource == null) {
+ newDataSource = new HierarchicalContainer();
+ }
+
+ // Assure that the data source is ordered by making unordered
+ // containers ordered by wrapping them
+ if (Container.Hierarchical.class
+ .isAssignableFrom(newDataSource.getClass())) {
+ super.setContainerDataSource(newDataSource);
+ } else {
+ super.setContainerDataSource(
+ new ContainerHierarchicalWrapper(newDataSource));
+ }
+
+ /*
+ * Ensure previous expanded items are cleaned up if they don't exist in
+ * the new container
+ */
+ if (expanded != null) {
+ /*
+ * We need to check that the expanded-field is not null since
+ * setContainerDataSource() is called from the parent constructor
+ * (AbstractSelect()) and at that time the expanded field is not yet
+ * initialized.
+ */
+ cleanupExpandedItems();
+ }
+
+ }
+
+ @Override
+ public void containerItemSetChange(
+ com.vaadin.v7.data.Container.ItemSetChangeEvent event) {
+ super.containerItemSetChange(event);
+ if (getContainerDataSource() instanceof Filterable) {
+ boolean hasFilters = !((Filterable) getContainerDataSource())
+ .getContainerFilters().isEmpty();
+ if (!hasFilters) {
+ /*
+ * If Container is not filtered then the itemsetchange is caused
+ * by either adding or removing items to the container. To
+ * prevent a memory leak we should cleanup the expanded list
+ * from items which was removed.
+ *
+ * However, there will still be a leak if the container is
+ * filtered to show only a subset of the items in the tree and
+ * later unfiltered items are removed from the container. In
+ * that case references to the unfiltered item ids will remain
+ * in the expanded list until the Tree instance is removed and
+ * the list is destroyed, or the container data source is
+ * replaced/updated. To force the removal of the removed items
+ * the application developer needs to a) remove the container
+ * filters temporarly or b) re-apply the container datasource
+ * using setContainerDataSource(getContainerDataSource())
+ */
+ cleanupExpandedItems();
+ }
+ }
+
+ }
+
+ /* Expand event and listener */
+
+ /**
+ * Event to fired when a node is expanded. ExapandEvent is fired when a node
+ * is to be expanded. it can me used to dynamically fill the sub-nodes of
+ * the node.
+ *
+ * @author Vaadin Ltd.
+ * @since 3.0
+ */
+ public static class ExpandEvent extends Component.Event {
+
+ private final Object expandedItemId;
+
+ /**
+ * New instance of options change event
+ *
+ * @param source
+ * the Source of the event.
+ * @param expandedItemId
+ */
+ public ExpandEvent(Component source, Object expandedItemId) {
+ super(source);
+ this.expandedItemId = expandedItemId;
+ }
+
+ /**
+ * Node where the event occurred.
+ *
+ * @return the Source of the event.
+ */
+ public Object getItemId() {
+ return expandedItemId;
+ }
+ }
+
+ /**
+ * Expand event listener.
+ *
+ * @author Vaadin Ltd.
+ * @since 3.0
+ */
+ public interface ExpandListener extends Serializable {
+
+ public static final Method EXPAND_METHOD = ReflectTools.findMethod(
+ ExpandListener.class, "nodeExpand", ExpandEvent.class);
+
+ /**
+ * A node has been expanded.
+ *
+ * @param event
+ * the Expand event.
+ */
+ public void nodeExpand(ExpandEvent event);
+ }
+
+ /**
+ * Adds the expand listener.
+ *
+ * @param listener
+ * the Listener to be added.
+ */
+ public void addExpandListener(ExpandListener listener) {
+ addListener(ExpandEvent.class, listener, ExpandListener.EXPAND_METHOD);
+ }
+
+ /**
+ * @deprecated As of 7.0, replaced by
+ * {@link #addExpandListener(ExpandListener)}
+ **/
+ @Deprecated
+ public void addListener(ExpandListener listener) {
+ addExpandListener(listener);
+ }
+
+ /**
+ * Removes the expand listener.
+ *
+ * @param listener
+ * the Listener to be removed.
+ */
+ public void removeExpandListener(ExpandListener listener) {
+ removeListener(ExpandEvent.class, listener,
+ ExpandListener.EXPAND_METHOD);
+ }
+
+ /**
+ * @deprecated As of 7.0, replaced by
+ * {@link #removeExpandListener(ExpandListener)}
+ **/
+ @Deprecated
+ public void removeListener(ExpandListener listener) {
+ removeExpandListener(listener);
+ }
+
+ /**
+ * Emits the expand event.
+ *
+ * @param itemId
+ * the item id.
+ */
+ protected void fireExpandEvent(Object itemId) {
+ fireEvent(new ExpandEvent(this, itemId));
+ }
+
+ /* Collapse event */
+
+ /**
+ * Collapse event
+ *
+ * @author Vaadin Ltd.
+ * @since 3.0
+ */
+ public static class CollapseEvent extends Component.Event {
+
+ private final Object collapsedItemId;
+
+ /**
+ * New instance of options change event.
+ *
+ * @param source
+ * the Source of the event.
+ * @param collapsedItemId
+ */
+ public CollapseEvent(Component source, Object collapsedItemId) {
+ super(source);
+ this.collapsedItemId = collapsedItemId;
+ }
+
+ /**
+ * Gets tge Collapsed Item id.
+ *
+ * @return the collapsed item id.
+ */
+ public Object getItemId() {
+ return collapsedItemId;
+ }
+ }
+
+ /**
+ * Collapse event listener.
+ *
+ * @author Vaadin Ltd.
+ * @since 3.0
+ */
+ public interface CollapseListener extends Serializable {
+
+ public static final Method COLLAPSE_METHOD = ReflectTools.findMethod(
+ CollapseListener.class, "nodeCollapse", CollapseEvent.class);
+
+ /**
+ * A node has been collapsed.
+ *
+ * @param event
+ * the Collapse event.
+ */
+ public void nodeCollapse(CollapseEvent event);
+ }
+
+ /**
+ * Adds the collapse listener.
+ *
+ * @param listener
+ * the Listener to be added.
+ */
+ public void addCollapseListener(CollapseListener listener) {
+ addListener(CollapseEvent.class, listener,
+ CollapseListener.COLLAPSE_METHOD);
+ }
+
+ /**
+ * @deprecated As of 7.0, replaced by
+ * {@link #addCollapseListener(CollapseListener)}
+ **/
+ @Deprecated
+ public void addListener(CollapseListener listener) {
+ addCollapseListener(listener);
+ }
+
+ /**
+ * Removes the collapse listener.
+ *
+ * @param listener
+ * the Listener to be removed.
+ */
+ public void removeCollapseListener(CollapseListener listener) {
+ removeListener(CollapseEvent.class, listener,
+ CollapseListener.COLLAPSE_METHOD);
+ }
+
+ /**
+ * @deprecated As of 7.0, replaced by
+ * {@link #removeCollapseListener(CollapseListener)}
+ **/
+ @Deprecated
+ public void removeListener(CollapseListener listener) {
+ removeCollapseListener(listener);
+ }
+
+ /**
+ * Emits collapse event.
+ *
+ * @param itemId
+ * the item id.
+ */
+ protected void fireCollapseEvent(Object itemId) {
+ fireEvent(new CollapseEvent(this, itemId));
+ }
+
+ /* Action container */
+
+ /**
+ * Adds an action handler.
+ *
+ * @see com.vaadin.event.Action.Container#addActionHandler(Action.Handler)
+ */
+ @Override
+ public void addActionHandler(Action.Handler actionHandler) {
+
+ if (actionHandler != null) {
+
+ if (actionHandlers == null) {
+ actionHandlers = new LinkedList<Action.Handler>();
+ actionMapper = new KeyMapper<Action>();
+ }
+
+ if (!actionHandlers.contains(actionHandler)) {
+ actionHandlers.add(actionHandler);
+ markAsDirty();
+ }
+ }
+ }
+
+ /**
+ * Removes an action handler.
+ *
+ * @see com.vaadin.event.Action.Container#removeActionHandler(Action.Handler)
+ */
+ @Override
+ public void removeActionHandler(Action.Handler actionHandler) {
+
+ if (actionHandlers != null && actionHandlers.contains(actionHandler)) {
+
+ actionHandlers.remove(actionHandler);
+
+ if (actionHandlers.isEmpty()) {
+ actionHandlers = null;
+ actionMapper = null;
+ }
+
+ markAsDirty();
+ }
+ }
+
+ /**
+ * Removes all action handlers
+ */
+ public void removeAllActionHandlers() {
+ actionHandlers = null;
+ actionMapper = null;
+ markAsDirty();
+ }
+
+ /**
+ * Gets the visible item ids.
+ *
+ * @see com.vaadin.v7.ui.Select#getVisibleItemIds()
+ */
+ @Override
+ public Collection<?> getVisibleItemIds() {
+
+ final LinkedList<Object> visible = new LinkedList<Object>();
+
+ // Iterates trough hierarchical tree using a stack of iterators
+ final Stack<Iterator<?>> iteratorStack = new Stack<Iterator<?>>();
+ final Collection<?> ids = rootItemIds();
+ if (ids != null) {
+ iteratorStack.push(ids.iterator());
+ }
+ while (!iteratorStack.isEmpty()) {
+
+ // Gets the iterator for current tree level
+ final Iterator<?> i = iteratorStack.peek();
+
+ // If the level is finished, back to previous tree level
+ if (!i.hasNext()) {
+
+ // Removes used iterator from the stack
+ iteratorStack.pop();
+ }
+
+ // Adds the item on current level
+ else {
+ final Object itemId = i.next();
+
+ visible.add(itemId);
+
+ // Adds children if expanded, or close the tag
+ if (isExpanded(itemId) && hasChildren(itemId)) {
+ iteratorStack.push(getChildren(itemId).iterator());
+ }
+ }
+ }
+
+ return visible;
+ }
+
+ /**
+ * Tree does not support <code>setNullSelectionItemId</code>.
+ *
+ * @see com.vaadin.v7.ui.AbstractSelect#setNullSelectionItemId(java.lang.Object)
+ */
+ @Override
+ public void setNullSelectionItemId(Object nullSelectionItemId)
+ throws UnsupportedOperationException {
+ if (nullSelectionItemId != null) {
+ throw new UnsupportedOperationException();
+ }
+
+ }
+
+ /**
+ * Adding new items is not supported.
+ *
+ * @throws UnsupportedOperationException
+ * if set to true.
+ * @see com.vaadin.v7.ui.Select#setNewItemsAllowed(boolean)
+ */
+ @Override
+ public void setNewItemsAllowed(boolean allowNewOptions)
+ throws UnsupportedOperationException {
+ if (allowNewOptions) {
+ throw new UnsupportedOperationException();
+ }
+ }
+
+ private ItemStyleGenerator itemStyleGenerator;
+
+ private DropHandler dropHandler;
+
+ private boolean htmlContentAllowed;
+
+ @Override
+ public void addItemClickListener(ItemClickListener listener) {
+ addListener(TreeConstants.ITEM_CLICK_EVENT_ID, ItemClickEvent.class,
+ listener, ItemClickEvent.ITEM_CLICK_METHOD);
+ }
+
+ /**
+ * @deprecated As of 7.0, replaced by
+ * {@link #addItemClickListener(ItemClickListener)}
+ **/
+ @Override
+ @Deprecated
+ public void addListener(ItemClickListener listener) {
+ addItemClickListener(listener);
+ }
+
+ @Override
+ public void removeItemClickListener(ItemClickListener listener) {
+ removeListener(TreeConstants.ITEM_CLICK_EVENT_ID, ItemClickEvent.class,
+ listener);
+ }
+
+ /**
+ * @deprecated As of 7.0, replaced by
+ * {@link #removeItemClickListener(ItemClickListener)}
+ **/
+ @Override
+ @Deprecated
+ public void removeListener(ItemClickListener listener) {
+ removeItemClickListener(listener);
+ }
+
+ /**
+ * Sets the {@link ItemStyleGenerator} to be used with this tree.
+ *
+ * @param itemStyleGenerator
+ * item style generator or null to remove generator
+ */
+ public void setItemStyleGenerator(ItemStyleGenerator itemStyleGenerator) {
+ if (this.itemStyleGenerator != itemStyleGenerator) {
+ this.itemStyleGenerator = itemStyleGenerator;
+ markAsDirty();
+ }
+ }
+
+ /**
+ * @return the current {@link ItemStyleGenerator} for this tree. Null if
+ * {@link ItemStyleGenerator} is not set.
+ */
+ public ItemStyleGenerator getItemStyleGenerator() {
+ return itemStyleGenerator;
+ }
+
+ /**
+ * ItemStyleGenerator can be used to add custom styles to tree items. The
+ * CSS class name that will be added to the item content is
+ * <tt>v-tree-node-[style name]</tt>.
+ */
+ public interface ItemStyleGenerator extends Serializable {
+
+ /**
+ * Called by Tree when an item is painted.
+ *
+ * @param source
+ * the source Tree
+ * @param itemId
+ * The itemId of the item to be painted
+ * @return The style name to add to this item. (the CSS class name will
+ * be v-tree-node-[style name]
+ */
+ public abstract String getStyle(Tree source, Object itemId);
+ }
+
+ // Overriden so javadoc comes from Container.Hierarchical
+ @Override
+ public boolean removeItem(Object itemId)
+ throws UnsupportedOperationException {
+ return super.removeItem(itemId);
+ }
+
+ @Override
+ public DropHandler getDropHandler() {
+ return dropHandler;
+ }
+
+ public void setDropHandler(DropHandler dropHandler) {
+ this.dropHandler = dropHandler;
+ }
+
+ /**
+ * A {@link TargetDetails} implementation with Tree specific api.
+ *
+ * @since 6.3
+ */
+ public class TreeTargetDetails extends AbstractSelectTargetDetails {
+
+ TreeTargetDetails(Map<String, Object> rawVariables) {
+ super(rawVariables);
+ }
+
+ @Override
+ public Tree getTarget() {
+ return (Tree) super.getTarget();
+ }
+
+ /**
+ * If the event is on a node that can not have children (see
+ * {@link Tree#areChildrenAllowed(Object)}), this method returns the
+ * parent item id of the target item (see {@link #getItemIdOver()} ).
+ * The identifier of the parent node is also returned if the cursor is
+ * on the top part of node. Else this method returns the same as
+ * {@link #getItemIdOver()}.
+ * <p>
+ * In other words this method returns the identifier of the "folder"
+ * into the drag operation is targeted.
+ * <p>
+ * If the method returns null, the current target is on a root node or
+ * on other undefined area over the tree component.
+ * <p>
+ * The default Tree implementation marks the targetted tree node with
+ * CSS classnames v-tree-node-dragfolder and
+ * v-tree-node-caption-dragfolder (for the caption element).
+ */
+ public Object getItemIdInto() {
+
+ Object itemIdOver = getItemIdOver();
+ if (areChildrenAllowed(itemIdOver)
+ && getDropLocation() == VerticalDropLocation.MIDDLE) {
+ return itemIdOver;
+ }
+ return getParent(itemIdOver);
+ }
+
+ /**
+ * If drop is targeted into "folder node" (see {@link #getItemIdInto()}
+ * ), this method returns the item id of the node after the drag was
+ * targeted. This method is useful when implementing drop into specific
+ * location (between specific nodes) in tree.
+ *
+ * @return the id of the item after the user targets the drop or null if
+ * "target" is a first item in node list (or the first in root
+ * node list)
+ */
+ public Object getItemIdAfter() {
+ Object itemIdOver = getItemIdOver();
+ Object itemIdInto2 = getItemIdInto();
+ if (itemIdOver.equals(itemIdInto2)) {
+ return null;
+ }
+ VerticalDropLocation dropLocation = getDropLocation();
+ if (VerticalDropLocation.TOP == dropLocation) {
+ // if on top of the caption area, add before
+ Collection<?> children;
+ Object itemIdInto = getItemIdInto();
+ if (itemIdInto != null) {
+ // seek the previous from child list
+ children = getChildren(itemIdInto);
+ } else {
+ children = rootItemIds();
+ }
+ Object ref = null;
+ for (Object object : children) {
+ if (object.equals(itemIdOver)) {
+ return ref;
+ }
+ ref = object;
+ }
+ }
+ return itemIdOver;
+ }
+
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.event.dd.DropTarget#translateDropTargetDetails(java.util.Map)
+ */
+ @Override
+ public TreeTargetDetails translateDropTargetDetails(
+ Map<String, Object> clientVariables) {
+ return new TreeTargetDetails(clientVariables);
+ }
+
+ /**
+ * Helper API for {@link TreeDropCriterion}
+ *
+ * @param itemId
+ * @return
+ */
+ private String key(Object itemId) {
+ return itemIdMapper.key(itemId);
+ }
+
+ /**
+ * Sets the drag mode that controls how Tree behaves as a {@link DragSource}
+ * .
+ *
+ * @param dragMode
+ */
+ public void setDragMode(TreeDragMode dragMode) {
+ this.dragMode = dragMode;
+ markAsDirty();
+ }
+
+ /**
+ * @return the drag mode that controls how Tree behaves as a
+ * {@link DragSource}.
+ *
+ * @see TreeDragMode
+ */
+ public TreeDragMode getDragMode() {
+ return dragMode;
+ }
+
+ /**
+ * Concrete implementation of {@link DataBoundTransferable} for data
+ * transferred from a tree.
+ *
+ * @see {@link DataBoundTransferable}.
+ *
+ * @since 6.3
+ */
+ protected class TreeTransferable extends DataBoundTransferable {
+
+ public TreeTransferable(Component sourceComponent,
+ Map<String, Object> rawVariables) {
+ super(sourceComponent, rawVariables);
+ }
+
+ @Override
+ public Object getItemId() {
+ return getData("itemId");
+ }
+
+ @Override
+ public Object getPropertyId() {
+ return getItemCaptionPropertyId();
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.event.dd.DragSource#getTransferable(java.util.Map)
+ */
+ @Override
+ public Transferable getTransferable(Map<String, Object> payload) {
+ TreeTransferable transferable = new TreeTransferable(this, payload);
+ // updating drag source variables
+ Object object = payload.get("itemId");
+ if (object != null) {
+ transferable.setData("itemId", itemIdMapper.get((String) object));
+ }
+
+ return transferable;
+ }
+
+ /**
+ * Lazy loading accept criterion for Tree. Accepted target nodes are loaded
+ * from server once per drag and drop operation. Developer must override one
+ * method that decides accepted tree nodes for the whole Tree.
+ *
+ * <p>
+ * Initially pretty much no data is sent to client. On first required
+ * criterion check (per drag request) the client side data structure is
+ * initialized from server and no subsequent requests requests are needed
+ * during that drag and drop operation.
+ */
+ public static abstract class TreeDropCriterion extends ServerSideCriterion {
+
+ private Tree tree;
+
+ private Set<Object> allowedItemIds;
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.event.dd.acceptCriteria.ServerSideCriterion#getIdentifier
+ * ()
+ */
+ @Override
+ protected String getIdentifier() {
+ return TreeDropCriterion.class.getCanonicalName();
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.event.dd.acceptCriteria.AcceptCriterion#accepts(com.vaadin
+ * .event.dd.DragAndDropEvent)
+ */
+ @Override
+ public boolean accept(DragAndDropEvent dragEvent) {
+ AbstractSelectTargetDetails dropTargetData = (AbstractSelectTargetDetails) dragEvent
+ .getTargetDetails();
+ tree = (Tree) dragEvent.getTargetDetails().getTarget();
+ allowedItemIds = getAllowedItemIds(dragEvent, tree);
+
+ return allowedItemIds.contains(dropTargetData.getItemIdOver());
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.event.dd.acceptCriteria.AcceptCriterion#paintResponse(
+ * com.vaadin.server.PaintTarget)
+ */
+ @Override
+ public void paintResponse(PaintTarget target) throws PaintException {
+ /*
+ * send allowed nodes to client so subsequent requests can be
+ * avoided
+ */
+ Object[] array = allowedItemIds.toArray();
+ for (int i = 0; i < array.length; i++) {
+ String key = tree.key(array[i]);
+ array[i] = key;
+ }
+ target.addAttribute("allowedIds", array);
+ }
+
+ protected abstract Set<Object> getAllowedItemIds(
+ DragAndDropEvent dragEvent, Tree tree);
+
+ }
+
+ /**
+ * A criterion that accepts {@link Transferable} only directly on a tree
+ * node that can have children.
+ * <p>
+ * Class is singleton, use {@link TargetItemAllowsChildren#get()} to get the
+ * instance.
+ *
+ * @see Tree#setChildrenAllowed(Object, boolean)
+ *
+ * @since 6.3
+ */
+ public static class TargetItemAllowsChildren extends TargetDetailIs {
+
+ private static TargetItemAllowsChildren instance = new TargetItemAllowsChildren();
+
+ public static TargetItemAllowsChildren get() {
+ return instance;
+ }
+
+ private TargetItemAllowsChildren() {
+ super("itemIdOverIsNode", Boolean.TRUE);
+ }
+
+ /*
+ * Uses enhanced server side check
+ */
+ @Override
+ public boolean accept(DragAndDropEvent dragEvent) {
+ try {
+ // must be over tree node and in the middle of it (not top or
+ // bottom
+ // part)
+ TreeTargetDetails eventDetails = (TreeTargetDetails) dragEvent
+ .getTargetDetails();
+
+ Object itemIdOver = eventDetails.getItemIdOver();
+ if (!eventDetails.getTarget().areChildrenAllowed(itemIdOver)) {
+ return false;
+ }
+ // return true if directly over
+ return eventDetails
+ .getDropLocation() == VerticalDropLocation.MIDDLE;
+ } catch (Exception e) {
+ return false;
+ }
+ }
+
+ }
+
+ /**
+ * An accept criterion that checks the parent node (or parent hierarchy) for
+ * the item identifier given in constructor. If the parent is found, content
+ * is accepted. Criterion can be used to accepts drags on a specific sub
+ * tree only.
+ * <p>
+ * The root items is also consider to be valid target.
+ */
+ public class TargetInSubtree extends ClientSideCriterion {
+
+ private Object rootId;
+ private int depthToCheck = -1;
+
+ /**
+ * Constructs a criteria that accepts the drag if the targeted Item is a
+ * descendant of Item identified by given id
+ *
+ * @param parentItemId
+ * the item identifier of the parent node
+ */
+ public TargetInSubtree(Object parentItemId) {
+ rootId = parentItemId;
+ }
+
+ /**
+ * Constructs a criteria that accepts drops within given level below the
+ * subtree root identified by given id.
+ *
+ * @param rootId
+ * the item identifier to be sought for
+ * @param depthToCheck
+ * the depth that tree is traversed upwards to seek for the
+ * parent, -1 means that the whole structure should be
+ * checked
+ */
+ public TargetInSubtree(Object rootId, int depthToCheck) {
+ this.rootId = rootId;
+ this.depthToCheck = depthToCheck;
+ }
+
+ @Override
+ public boolean accept(DragAndDropEvent dragEvent) {
+ try {
+ TreeTargetDetails eventDetails = (TreeTargetDetails) dragEvent
+ .getTargetDetails();
+
+ if (eventDetails.getItemIdOver() != null) {
+ Object itemId = eventDetails.getItemIdOver();
+ int i = 0;
+ while (itemId != null
+ && (depthToCheck == -1 || i <= depthToCheck)) {
+ if (itemId.equals(rootId)) {
+ return true;
+ }
+ itemId = getParent(itemId);
+ i++;
+ }
+ }
+ return false;
+ } catch (Exception e) {
+ return false;
+ }
+ }
+
+ @Override
+ public void paintContent(PaintTarget target) throws PaintException {
+ super.paintContent(target);
+ target.addAttribute("depth", depthToCheck);
+ target.addAttribute("key", key(rootId));
+ }
+ }
+
+ /**
+ * Set the item description generator which generates tooltips for the tree
+ * items
+ *
+ * @param generator
+ * The generator to use or null to disable
+ */
+ public void setItemDescriptionGenerator(
+ ItemDescriptionGenerator generator) {
+ if (generator != itemDescriptionGenerator) {
+ itemDescriptionGenerator = generator;
+ markAsDirty();
+ }
+ }
+
+ /**
+ * Get the item description generator which generates tooltips for tree
+ * items
+ */
+ public ItemDescriptionGenerator getItemDescriptionGenerator() {
+ return itemDescriptionGenerator;
+ }
+
+ private void cleanupExpandedItems() {
+ Set<Object> removedItemIds = new HashSet<Object>();
+ for (Object expandedItemId : expanded) {
+ if (getItem(expandedItemId) == null) {
+ removedItemIds.add(expandedItemId);
+ if (this.expandedItemId == expandedItemId) {
+ this.expandedItemId = null;
+ }
+ }
+ }
+ expanded.removeAll(removedItemIds);
+ }
+
+ /**
+ * Reads an Item from a design and inserts it into the data source.
+ * Recursively handles any children of the item as well.
+ *
+ * @since 7.5.0
+ * @param node
+ * an element representing the item (tree node).
+ * @param selected
+ * A set accumulating selected items. If the item that is read is
+ * marked as selected, its item id should be added to this set.
+ * @param context
+ * the DesignContext instance used in parsing
+ * @return the item id of the new item
+ *
+ * @throws DesignException
+ * if the tag name of the {@code node} element is not
+ * {@code node}.
+ */
+ @Override
+ protected String readItem(Element node, Set<String> selected,
+ DesignContext context) {
+
+ if (!"node".equals(node.tagName())) {
+ throw new DesignException("Unrecognized child element in "
+ + getClass().getSimpleName() + ": " + node.tagName());
+ }
+
+ String itemId = node.attr("text");
+ addItem(itemId);
+ if (node.hasAttr("icon")) {
+ Resource icon = DesignAttributeHandler.readAttribute("icon",
+ node.attributes(), Resource.class);
+ setItemIcon(itemId, icon);
+ }
+ if (node.hasAttr("selected")) {
+ selected.add(itemId);
+ }
+
+ for (Element child : node.children()) {
+ String childItemId = readItem(child, selected, context);
+ setParent(childItemId, itemId);
+ }
+ return itemId;
+ }
+
+ /**
+ * Recursively writes the root items and their children to a design.
+ *
+ * @since 7.5.0
+ * @param design
+ * the element into which to insert the items
+ * @param context
+ * the DesignContext instance used in writing
+ */
+ @Override
+ protected void writeItems(Element design, DesignContext context) {
+ for (Object itemId : rootItemIds()) {
+ writeItem(design, itemId, context);
+ }
+ }
+
+ /**
+ * Recursively writes a data source Item and its children to a design.
+ *
+ * @since 7.5.0
+ * @param design
+ * the element into which to insert the item
+ * @param itemId
+ * the id of the item to write
+ * @param context
+ * the DesignContext instance used in writing
+ * @return
+ */
+ @Override
+ protected Element writeItem(Element design, Object itemId,
+ DesignContext context) {
+ Element element = design.appendElement("node");
+
+ element.attr("text", itemId.toString());
+
+ Resource icon = getItemIcon(itemId);
+ if (icon != null) {
+ DesignAttributeHandler.writeAttribute("icon", element.attributes(),
+ icon, null, Resource.class);
+ }
+
+ if (isSelected(itemId)) {
+ element.attr("selected", "");
+ }
+
+ Collection<?> children = getChildren(itemId);
+ if (children != null) {
+ // Yeah... see #5864
+ for (Object childItemId : children) {
+ writeItem(element, childItemId, context);
+ }
+ }
+
+ return element;
+ }
+
+ /**
+ * Sets whether html is allowed in the item captions. If set to
+ * <code>true</code>, the captions are passed to the browser as html and the
+ * developer is responsible for ensuring no harmful html is used. If set to
+ * <code>false</code>, the content is passed to the browser as plain text.
+ * The default setting is <code>false</code>
+ *
+ * @since 7.6
+ * @param htmlContentAllowed
+ * <code>true</code> if the captions are used as html,
+ * <code>false</code> if used as plain text
+ */
+ public void setHtmlContentAllowed(boolean htmlContentAllowed) {
+ this.htmlContentAllowed = htmlContentAllowed;
+ markAsDirty();
+ }
+
+ /**
+ * Checks whether captions are interpreted as html or plain text.
+ *
+ * @since 7.6
+ * @return <code>true</code> if the captions are displayed as html,
+ * <code>false</code> if displayed as plain text
+ * @see #setHtmlContentAllowed(boolean)
+ */
+ public boolean isHtmlContentAllowed() {
+ return htmlContentAllowed;
+ }
+
+ @Override
+ protected TreeState getState() {
+ return (TreeState) super.getState();
+ }
+}
diff --git a/compatibility-server/src/main/java/com/vaadin/v7/ui/TreeTable.java b/compatibility-server/src/main/java/com/vaadin/v7/ui/TreeTable.java
new file mode 100644
index 0000000000..446c9c271c
--- /dev/null
+++ b/compatibility-server/src/main/java/com/vaadin/v7/ui/TreeTable.java
@@ -0,0 +1,985 @@
+/*
+ * 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.v7.ui;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.Stack;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import org.jsoup.nodes.Element;
+
+import com.vaadin.server.PaintException;
+import com.vaadin.server.PaintTarget;
+import com.vaadin.server.Resource;
+import com.vaadin.shared.ui.treetable.TreeTableConstants;
+import com.vaadin.shared.ui.treetable.TreeTableState;
+import com.vaadin.ui.declarative.DesignAttributeHandler;
+import com.vaadin.ui.declarative.DesignContext;
+import com.vaadin.ui.declarative.DesignException;
+import com.vaadin.v7.data.Collapsible;
+import com.vaadin.v7.data.Container;
+import com.vaadin.v7.data.Container.Hierarchical;
+import com.vaadin.v7.data.Container.ItemSetChangeEvent;
+import com.vaadin.v7.data.util.ContainerHierarchicalWrapper;
+import com.vaadin.v7.data.util.HierarchicalContainer;
+import com.vaadin.v7.data.util.HierarchicalContainerOrderedWrapper;
+import com.vaadin.v7.ui.Tree.CollapseEvent;
+import com.vaadin.v7.ui.Tree.CollapseListener;
+import com.vaadin.v7.ui.Tree.ExpandEvent;
+import com.vaadin.v7.ui.Tree.ExpandListener;
+
+/**
+ * TreeTable extends the {@link Table} component so that it can also visualize a
+ * hierarchy of its Items in a similar manner that {@link Tree} does. The tree
+ * hierarchy is always displayed in the first actual column of the TreeTable.
+ * <p>
+ * The TreeTable supports the usual {@link Table} features like lazy loading, so
+ * it should be no problem to display lots of items at once. Only required rows
+ * and some cache rows are sent to the client.
+ * <p>
+ * TreeTable supports standard {@link Hierarchical} container interfaces, but
+ * also a more fine tuned version - {@link Collapsible}. A container
+ * implementing the {@link Collapsible} interface stores the collapsed/expanded
+ * state internally and can this way scale better on the server side than with
+ * standard Hierarchical implementations. Developer must however note that
+ * {@link Collapsible} containers can not be shared among several users as they
+ * share UI state in the container.
+ */
+@SuppressWarnings({ "serial" })
+public class TreeTable extends Table implements Hierarchical {
+
+ private interface ContainerStrategy extends Serializable {
+ public int size();
+
+ public boolean isNodeOpen(Object itemId);
+
+ public int getDepth(Object itemId);
+
+ public void toggleChildVisibility(Object itemId);
+
+ public Object getIdByIndex(int index);
+
+ public int indexOfId(Object id);
+
+ public Object nextItemId(Object itemId);
+
+ public Object lastItemId();
+
+ public Object prevItemId(Object itemId);
+
+ public boolean isLastId(Object itemId);
+
+ public Collection<?> getItemIds();
+
+ public void containerItemSetChange(ItemSetChangeEvent event);
+ }
+
+ private abstract class AbstractStrategy implements ContainerStrategy {
+
+ /**
+ * Consider adding getDepth to {@link Collapsible}, might help
+ * scalability with some container implementations.
+ */
+
+ @Override
+ public int getDepth(Object itemId) {
+ int depth = 0;
+ Hierarchical hierarchicalContainer = getContainerDataSource();
+ while (!hierarchicalContainer.isRoot(itemId)) {
+ depth++;
+ itemId = hierarchicalContainer.getParent(itemId);
+ }
+ return depth;
+ }
+
+ @Override
+ public void containerItemSetChange(ItemSetChangeEvent event) {
+ }
+
+ }
+
+ /**
+ * This strategy is used if current container implements {@link Collapsible}
+ * .
+ *
+ * open-collapsed logic diverted to container, otherwise use default
+ * implementations.
+ */
+ private class CollapsibleStrategy extends AbstractStrategy {
+
+ private Collapsible c() {
+ return (Collapsible) getContainerDataSource();
+ }
+
+ @Override
+ public void toggleChildVisibility(Object itemId) {
+ c().setCollapsed(itemId, !c().isCollapsed(itemId));
+ }
+
+ @Override
+ public boolean isNodeOpen(Object itemId) {
+ return !c().isCollapsed(itemId);
+ }
+
+ @Override
+ public int size() {
+ return TreeTable.super.size();
+ }
+
+ @Override
+ public Object getIdByIndex(int index) {
+ return TreeTable.super.getIdByIndex(index);
+ }
+
+ @Override
+ public int indexOfId(Object id) {
+ return TreeTable.super.indexOfId(id);
+ }
+
+ @Override
+ public boolean isLastId(Object itemId) {
+ // using the default impl
+ return TreeTable.super.isLastId(itemId);
+ }
+
+ @Override
+ public Object lastItemId() {
+ // using the default impl
+ return TreeTable.super.lastItemId();
+ }
+
+ @Override
+ public Object nextItemId(Object itemId) {
+ return TreeTable.super.nextItemId(itemId);
+ }
+
+ @Override
+ public Object prevItemId(Object itemId) {
+ return TreeTable.super.prevItemId(itemId);
+ }
+
+ @Override
+ public Collection<?> getItemIds() {
+ return TreeTable.super.getItemIds();
+ }
+
+ }
+
+ /**
+ * Strategy for Hierarchical but not Collapsible container like
+ * {@link HierarchicalContainer}.
+ *
+ * Store collapsed/open states internally, fool Table to use preorder when
+ * accessing items from container via Ordered/Indexed methods.
+ */
+ private class HierarchicalStrategy extends AbstractStrategy {
+
+ private final HashSet<Object> openItems = new HashSet<Object>();
+
+ @Override
+ public boolean isNodeOpen(Object itemId) {
+ return openItems.contains(itemId);
+ }
+
+ @Override
+ public int size() {
+ return getPreOrder().size();
+ }
+
+ @Override
+ public Collection<Object> getItemIds() {
+ return Collections.unmodifiableCollection(getPreOrder());
+ }
+
+ @Override
+ public boolean isLastId(Object itemId) {
+ if (itemId == null) {
+ return false;
+ }
+
+ return itemId.equals(lastItemId());
+ }
+
+ @Override
+ public Object lastItemId() {
+ if (getPreOrder().size() > 0) {
+ return getPreOrder().get(getPreOrder().size() - 1);
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public Object nextItemId(Object itemId) {
+ int indexOf = getPreOrder().indexOf(itemId);
+ if (indexOf == -1) {
+ return null;
+ }
+ indexOf++;
+ if (indexOf == getPreOrder().size()) {
+ return null;
+ } else {
+ return getPreOrder().get(indexOf);
+ }
+ }
+
+ @Override
+ public Object prevItemId(Object itemId) {
+ int indexOf = getPreOrder().indexOf(itemId);
+ indexOf--;
+ if (indexOf < 0) {
+ return null;
+ } else {
+ return getPreOrder().get(indexOf);
+ }
+ }
+
+ @Override
+ public void toggleChildVisibility(Object itemId) {
+ boolean removed = openItems.remove(itemId);
+ if (!removed) {
+ openItems.add(itemId);
+ getLogger().log(Level.FINEST, "Item {0} is now expanded",
+ itemId);
+ } else {
+ getLogger().log(Level.FINEST, "Item {0} is now collapsed",
+ itemId);
+ }
+ clearPreorderCache();
+ }
+
+ private void clearPreorderCache() {
+ preOrder = null; // clear preorder cache
+ }
+
+ List<Object> preOrder;
+
+ /**
+ * Preorder of ids currently visible
+ *
+ * @return
+ */
+ private List<Object> getPreOrder() {
+ if (preOrder == null) {
+ preOrder = new ArrayList<Object>();
+ Collection<?> rootItemIds = getContainerDataSource()
+ .rootItemIds();
+ for (Object id : rootItemIds) {
+ preOrder.add(id);
+ addVisibleChildTree(id);
+ }
+ }
+ return preOrder;
+ }
+
+ private void addVisibleChildTree(Object id) {
+ if (isNodeOpen(id)) {
+ Collection<?> children = getContainerDataSource()
+ .getChildren(id);
+ if (children != null) {
+ for (Object childId : children) {
+ preOrder.add(childId);
+ addVisibleChildTree(childId);
+ }
+ }
+ }
+
+ }
+
+ @Override
+ public int indexOfId(Object id) {
+ return getPreOrder().indexOf(id);
+ }
+
+ @Override
+ public Object getIdByIndex(int index) {
+ return getPreOrder().get(index);
+ }
+
+ @Override
+ public void containerItemSetChange(ItemSetChangeEvent event) {
+ // preorder becomes invalid on sort, item additions etc.
+ clearPreorderCache();
+ super.containerItemSetChange(event);
+ }
+
+ }
+
+ /**
+ * Creates an empty TreeTable with a default container.
+ */
+ public TreeTable() {
+ super(null, new HierarchicalContainer());
+ }
+
+ /**
+ * Creates an empty TreeTable with a default container.
+ *
+ * @param caption
+ * the caption for the TreeTable
+ */
+ public TreeTable(String caption) {
+ this();
+ setCaption(caption);
+ }
+
+ /**
+ * Creates a TreeTable instance with given captions and data source.
+ *
+ * @param caption
+ * the caption for the component
+ * @param dataSource
+ * the dataSource that is used to list items in the component
+ */
+ public TreeTable(String caption, Container dataSource) {
+ super(caption, dataSource);
+ }
+
+ private ContainerStrategy cStrategy;
+ private Object focusedRowId = null;
+ private Object hierarchyColumnId;
+
+ /**
+ * The item id that was expanded or collapsed during this request. Reset at
+ * the end of paint and only used for determining if a partial or full paint
+ * should be done.
+ *
+ * Can safely be reset to null whenever a change occurs that would prevent a
+ * partial update from rendering the correct result, e.g. rows added or
+ * removed during an expand operation.
+ */
+ private Object toggledItemId;
+ private boolean animationsEnabled;
+ private boolean clearFocusedRowPending;
+
+ /**
+ * If the container does not send item set change events, always do a full
+ * repaint instead of a partial update when expanding/collapsing nodes.
+ */
+ private boolean containerSupportsPartialUpdates;
+
+ private ContainerStrategy getContainerStrategy() {
+ if (cStrategy == null) {
+ if (getContainerDataSource() instanceof Collapsible) {
+ cStrategy = new CollapsibleStrategy();
+ } else {
+ cStrategy = new HierarchicalStrategy();
+ }
+ }
+ return cStrategy;
+ }
+
+ @Override
+ protected void paintRowAttributes(PaintTarget target, Object itemId)
+ throws PaintException {
+ super.paintRowAttributes(target, itemId);
+ target.addAttribute("depth", getContainerStrategy().getDepth(itemId));
+ if (getContainerDataSource().areChildrenAllowed(itemId)) {
+ target.addAttribute("ca", true);
+ target.addAttribute("open",
+ getContainerStrategy().isNodeOpen(itemId));
+ }
+ }
+
+ @Override
+ protected void paintRowIcon(PaintTarget target, Object[][] cells,
+ int indexInRowbuffer) throws PaintException {
+ // always paint if present (in parent only if row headers visible)
+ if (getRowHeaderMode() == ROW_HEADER_MODE_HIDDEN) {
+ Resource itemIcon = getItemIcon(
+ cells[CELL_ITEMID][indexInRowbuffer]);
+ if (itemIcon != null) {
+ target.addAttribute("icon", itemIcon);
+ }
+ } else if (cells[CELL_ICON][indexInRowbuffer] != null) {
+ target.addAttribute("icon",
+ (Resource) cells[CELL_ICON][indexInRowbuffer]);
+ }
+ }
+
+ @Override
+ protected boolean rowHeadersAreEnabled() {
+ if (getRowHeaderMode() == RowHeaderMode.ICON_ONLY) {
+ return false;
+ }
+ return super.rowHeadersAreEnabled();
+ }
+
+ @Override
+ public void changeVariables(Object source, Map<String, Object> variables) {
+ super.changeVariables(source, variables);
+
+ if (variables.containsKey("toggleCollapsed")) {
+ String object = (String) variables.get("toggleCollapsed");
+ Object itemId = itemIdMapper.get(object);
+ toggledItemId = itemId;
+ toggleChildVisibility(itemId, false);
+ if (variables.containsKey("selectCollapsed")) {
+ // ensure collapsed is selected unless opened with selection
+ // head
+ if (isSelectable()) {
+ select(itemId);
+ }
+ }
+ } else if (variables.containsKey("focusParent")) {
+ String key = (String) variables.get("focusParent");
+ Object refId = itemIdMapper.get(key);
+ Object itemId = getParent(refId);
+ focusParent(itemId);
+ }
+ }
+
+ private void focusParent(Object itemId) {
+ boolean inView = false;
+ Object inPageId = getCurrentPageFirstItemId();
+ for (int i = 0; inPageId != null && i < getPageLength(); i++) {
+ if (inPageId.equals(itemId)) {
+ inView = true;
+ break;
+ }
+ inPageId = nextItemId(inPageId);
+ i++;
+ }
+ if (!inView) {
+ setCurrentPageFirstItemId(itemId);
+ }
+ // Select the row if it is selectable.
+ if (isSelectable()) {
+ if (isMultiSelect()) {
+ setValue(Collections.singleton(itemId));
+ } else {
+ setValue(itemId);
+ }
+ }
+ setFocusedRow(itemId);
+ }
+
+ private void setFocusedRow(Object itemId) {
+ focusedRowId = itemId;
+ if (focusedRowId == null) {
+ // Must still inform the client that the focusParent request has
+ // been processed
+ clearFocusedRowPending = true;
+ }
+ markAsDirty();
+ }
+
+ @Override
+ public void paintContent(PaintTarget target) throws PaintException {
+ if (focusedRowId != null) {
+ target.addAttribute("focusedRow", itemIdMapper.key(focusedRowId));
+ focusedRowId = null;
+ } else if (clearFocusedRowPending) {
+ // Must still inform the client that the focusParent request has
+ // been processed
+ target.addAttribute("clearFocusPending", true);
+ clearFocusedRowPending = false;
+ }
+ target.addAttribute("animate", animationsEnabled);
+ if (hierarchyColumnId != null) {
+ Object[] visibleColumns2 = getVisibleColumns();
+ for (int i = 0; i < visibleColumns2.length; i++) {
+ Object object = visibleColumns2[i];
+ if (hierarchyColumnId.equals(object)) {
+ target.addAttribute(
+ TreeTableConstants.ATTRIBUTE_HIERARCHY_COLUMN_INDEX,
+ i);
+ break;
+ }
+ }
+ }
+ super.paintContent(target);
+ toggledItemId = null;
+ }
+
+ /*
+ * Override methods for partial row updates and additions when expanding /
+ * collapsing nodes.
+ */
+
+ @Override
+ protected boolean isPartialRowUpdate() {
+ return toggledItemId != null && containerSupportsPartialUpdates
+ && !isRowCacheInvalidated();
+ }
+
+ @Override
+ protected int getFirstAddedItemIndex() {
+ return indexOfId(toggledItemId) + 1;
+ }
+
+ @Override
+ protected int getAddedRowCount() {
+ return countSubNodesRecursively(getContainerDataSource(),
+ toggledItemId);
+ }
+
+ private int countSubNodesRecursively(Hierarchical hc, Object itemId) {
+ int count = 0;
+ // we need the number of children for toggledItemId no matter if its
+ // collapsed or expanded. Other items' children are only counted if the
+ // item is expanded.
+ if (getContainerStrategy().isNodeOpen(itemId)
+ || itemId == toggledItemId) {
+ Collection<?> children = hc.getChildren(itemId);
+ if (children != null) {
+ count += children != null ? children.size() : 0;
+ for (Object id : children) {
+ count += countSubNodesRecursively(hc, id);
+ }
+ }
+ }
+ return count;
+ }
+
+ @Override
+ protected int getFirstUpdatedItemIndex() {
+ return indexOfId(toggledItemId);
+ }
+
+ @Override
+ protected int getUpdatedRowCount() {
+ return 1;
+ }
+
+ @Override
+ protected boolean shouldHideAddedRows() {
+ return !getContainerStrategy().isNodeOpen(toggledItemId);
+ }
+
+ private void toggleChildVisibility(Object itemId,
+ boolean forceFullRefresh) {
+ getContainerStrategy().toggleChildVisibility(itemId);
+ // ensure that page still has first item in page, DON'T clear the
+ // caches.
+ setCurrentPageFirstItemIndex(getCurrentPageFirstItemIndex(), false);
+
+ if (isCollapsed(itemId)) {
+ fireCollapseEvent(itemId);
+ } else {
+ fireExpandEvent(itemId);
+ }
+
+ if (containerSupportsPartialUpdates && !forceFullRefresh) {
+ markAsDirty();
+ } else {
+ // For containers that do not send item set change events, always do
+ // full repaint instead of partial row update.
+ refreshRowCache();
+ }
+ }
+
+ @Override
+ public int size() {
+ return getContainerStrategy().size();
+ }
+
+ @Override
+ public Hierarchical getContainerDataSource() {
+ return (Hierarchical) super.getContainerDataSource();
+ }
+
+ @Override
+ public void setContainerDataSource(Container newDataSource) {
+ cStrategy = null;
+
+ // FIXME: This disables partial updates until TreeTable is fixed so it
+ // does not change component hierarchy during paint
+ containerSupportsPartialUpdates = (newDataSource instanceof ItemSetChangeNotifier)
+ && false;
+
+ if (newDataSource != null && !(newDataSource instanceof Hierarchical)) {
+ newDataSource = new ContainerHierarchicalWrapper(newDataSource);
+ }
+
+ if (newDataSource != null && !(newDataSource instanceof Ordered)) {
+ newDataSource = new HierarchicalContainerOrderedWrapper(
+ (Hierarchical) newDataSource);
+ }
+
+ super.setContainerDataSource(newDataSource);
+ }
+
+ @Override
+ public void containerItemSetChange(
+ com.vaadin.v7.data.Container.ItemSetChangeEvent event) {
+ // Can't do partial repaints if items are added or removed during the
+ // expand/collapse request
+ toggledItemId = null;
+ getContainerStrategy().containerItemSetChange(event);
+ super.containerItemSetChange(event);
+ }
+
+ @Override
+ protected Object getIdByIndex(int index) {
+ return getContainerStrategy().getIdByIndex(index);
+ }
+
+ @Override
+ protected int indexOfId(Object itemId) {
+ return getContainerStrategy().indexOfId(itemId);
+ }
+
+ @Override
+ public Object nextItemId(Object itemId) {
+ return getContainerStrategy().nextItemId(itemId);
+ }
+
+ @Override
+ public Object lastItemId() {
+ return getContainerStrategy().lastItemId();
+ }
+
+ @Override
+ public Object prevItemId(Object itemId) {
+ return getContainerStrategy().prevItemId(itemId);
+ }
+
+ @Override
+ public boolean isLastId(Object itemId) {
+ return getContainerStrategy().isLastId(itemId);
+ }
+
+ @Override
+ public Collection<?> getItemIds() {
+ return getContainerStrategy().getItemIds();
+ }
+
+ @Override
+ public boolean areChildrenAllowed(Object itemId) {
+ return getContainerDataSource().areChildrenAllowed(itemId);
+ }
+
+ @Override
+ public Collection<?> getChildren(Object itemId) {
+ return getContainerDataSource().getChildren(itemId);
+ }
+
+ @Override
+ public Object getParent(Object itemId) {
+ return getContainerDataSource().getParent(itemId);
+ }
+
+ @Override
+ public boolean hasChildren(Object itemId) {
+ return getContainerDataSource().hasChildren(itemId);
+ }
+
+ @Override
+ public boolean isRoot(Object itemId) {
+ return getContainerDataSource().isRoot(itemId);
+ }
+
+ @Override
+ public Collection<?> rootItemIds() {
+ return getContainerDataSource().rootItemIds();
+ }
+
+ @Override
+ public boolean setChildrenAllowed(Object itemId, boolean areChildrenAllowed)
+ throws UnsupportedOperationException {
+ return getContainerDataSource().setChildrenAllowed(itemId,
+ areChildrenAllowed);
+ }
+
+ @Override
+ public boolean setParent(Object itemId, Object newParentId)
+ throws UnsupportedOperationException {
+ return getContainerDataSource().setParent(itemId, newParentId);
+ }
+
+ /**
+ * Sets the Item specified by given identifier as collapsed or expanded. If
+ * the Item is collapsed, its children are not displayed to the user.
+ *
+ * @param itemId
+ * the identifier of the Item
+ * @param collapsed
+ * true if the Item should be collapsed, false if expanded
+ */
+ public void setCollapsed(Object itemId, boolean collapsed) {
+ if (isCollapsed(itemId) != collapsed) {
+ if (null == toggledItemId && !isRowCacheInvalidated()
+ && getVisibleItemIds().contains(itemId)) {
+ // optimization: partial refresh if only one item is
+ // collapsed/expanded
+ toggledItemId = itemId;
+ toggleChildVisibility(itemId, false);
+ } else {
+ // make sure a full refresh takes place - otherwise neither
+ // partial nor full repaint of table content is performed
+ toggledItemId = null;
+ toggleChildVisibility(itemId, true);
+ }
+ }
+ }
+
+ /**
+ * Checks if Item with given identifier is collapsed in the UI.
+ *
+ * <p>
+ *
+ * @param itemId
+ * the identifier of the checked Item
+ * @return true if the Item with given id is collapsed
+ * @see Collapsible#isCollapsed(Object)
+ */
+ public boolean isCollapsed(Object itemId) {
+ return !getContainerStrategy().isNodeOpen(itemId);
+ }
+
+ /**
+ * Explicitly sets the column in which the TreeTable visualizes the
+ * hierarchy. If hierarchyColumnId is not set, the hierarchy is visualized
+ * in the first visible column.
+ *
+ * @param hierarchyColumnId
+ */
+ public void setHierarchyColumn(Object hierarchyColumnId) {
+ this.hierarchyColumnId = hierarchyColumnId;
+ }
+
+ /**
+ * @return the identifier of column into which the hierarchy will be
+ * visualized or null if the column is not explicitly defined.
+ */
+ public Object getHierarchyColumnId() {
+ return hierarchyColumnId;
+ }
+
+ /**
+ * Adds an expand listener.
+ *
+ * @param listener
+ * the Listener to be added.
+ */
+ public void addExpandListener(ExpandListener listener) {
+ addListener(ExpandEvent.class, listener, ExpandListener.EXPAND_METHOD);
+ }
+
+ /**
+ * @deprecated As of 7.0, replaced by
+ * {@link #addExpandListener(ExpandListener)}
+ **/
+ @Deprecated
+ public void addListener(ExpandListener listener) {
+ addExpandListener(listener);
+ }
+
+ /**
+ * Removes an expand listener.
+ *
+ * @param listener
+ * the Listener to be removed.
+ */
+ public void removeExpandListener(ExpandListener listener) {
+ removeListener(ExpandEvent.class, listener,
+ ExpandListener.EXPAND_METHOD);
+ }
+
+ /**
+ * @deprecated As of 7.0, replaced by
+ * {@link #removeExpandListener(ExpandListener)}
+ **/
+ @Deprecated
+ public void removeListener(ExpandListener listener) {
+ removeExpandListener(listener);
+ }
+
+ /**
+ * Emits an expand event.
+ *
+ * @param itemId
+ * the item id.
+ */
+ protected void fireExpandEvent(Object itemId) {
+ fireEvent(new ExpandEvent(this, itemId));
+ }
+
+ /**
+ * Adds a collapse listener.
+ *
+ * @param listener
+ * the Listener to be added.
+ */
+ public void addCollapseListener(CollapseListener listener) {
+ addListener(CollapseEvent.class, listener,
+ CollapseListener.COLLAPSE_METHOD);
+ }
+
+ /**
+ * @deprecated As of 7.0, replaced by
+ * {@link #addCollapseListener(CollapseListener)}
+ **/
+ @Deprecated
+ public void addListener(CollapseListener listener) {
+ addCollapseListener(listener);
+ }
+
+ /**
+ * Removes a collapse listener.
+ *
+ * @param listener
+ * the Listener to be removed.
+ */
+ public void removeCollapseListener(CollapseListener listener) {
+ removeListener(CollapseEvent.class, listener,
+ CollapseListener.COLLAPSE_METHOD);
+ }
+
+ /**
+ * @deprecated As of 7.0, replaced by
+ * {@link #removeCollapseListener(CollapseListener)}
+ **/
+ @Deprecated
+ public void removeListener(CollapseListener listener) {
+ removeCollapseListener(listener);
+ }
+
+ /**
+ * Emits a collapse event.
+ *
+ * @param itemId
+ * the item id.
+ */
+ protected void fireCollapseEvent(Object itemId) {
+ fireEvent(new CollapseEvent(this, itemId));
+ }
+
+ /**
+ * @return true if animations are enabled
+ */
+ public boolean isAnimationsEnabled() {
+ return animationsEnabled;
+ }
+
+ /**
+ * Animations can be enabled by passing true to this method. Currently
+ * expanding rows slide in from the top and collapsing rows slide out the
+ * same way. NOTE! not supported in Internet Explorer 6 or 7.
+ *
+ * @param animationsEnabled
+ * true or false whether to enable animations or not.
+ */
+ public void setAnimationsEnabled(boolean animationsEnabled) {
+ this.animationsEnabled = animationsEnabled;
+ markAsDirty();
+ }
+
+ private static final Logger getLogger() {
+ return Logger.getLogger(TreeTable.class.getName());
+ }
+
+ @Override
+ protected List<Object> getItemIds(int firstIndex, int rows) {
+ List<Object> itemIds = new ArrayList<Object>();
+ for (int i = firstIndex; i < firstIndex + rows; i++) {
+ itemIds.add(getIdByIndex(i));
+ }
+ return itemIds;
+ }
+
+ @Override
+ protected void readBody(Element design, DesignContext context) {
+ Element tbody = design.select("> table > tbody").first();
+ if (tbody == null) {
+ return;
+ }
+
+ Set<String> selected = new HashSet<String>();
+ Stack<Object> parents = new Stack<Object>();
+ int lastDepth = -1;
+
+ for (Element tr : tbody.children()) {
+ int depth = DesignAttributeHandler.readAttribute("depth",
+ tr.attributes(), 0, int.class);
+
+ if (depth < 0 || depth > lastDepth + 1) {
+ throw new DesignException(
+ "Malformed TreeTable item hierarchy at " + tr
+ + ": last depth was " + lastDepth);
+ } else if (depth <= lastDepth) {
+ for (int d = depth; d <= lastDepth; d++) {
+ parents.pop();
+ }
+ }
+
+ Object itemId = readItem(tr, selected, context);
+ setParent(itemId, !parents.isEmpty() ? parents.peek() : null);
+ parents.push(itemId);
+ lastDepth = depth;
+ }
+ }
+
+ @Override
+ protected Object readItem(Element tr, Set<String> selected,
+ DesignContext context) {
+ Object itemId = super.readItem(tr, selected, context);
+
+ if (tr.hasAttr("collapsed")) {
+ boolean collapsed = DesignAttributeHandler
+ .readAttribute("collapsed", tr.attributes(), boolean.class);
+ setCollapsed(itemId, collapsed);
+ }
+
+ return itemId;
+ }
+
+ @Override
+ protected void writeItems(Element design, DesignContext context) {
+ if (getVisibleColumns().length == 0) {
+ return;
+ }
+ Element tbody = design.child(0).appendElement("tbody");
+ writeItems(tbody, rootItemIds(), 0, context);
+ }
+
+ protected void writeItems(Element tbody, Collection<?> itemIds, int depth,
+ DesignContext context) {
+ for (Object itemId : itemIds) {
+ Element tr = writeItem(tbody, itemId, context);
+ DesignAttributeHandler.writeAttribute("depth", tr.attributes(),
+ depth, 0, int.class);
+
+ if (getChildren(itemId) != null) {
+ writeItems(tbody, getChildren(itemId), depth + 1, context);
+ }
+ }
+ }
+
+ @Override
+ protected Element writeItem(Element tbody, Object itemId,
+ DesignContext context) {
+ Element tr = super.writeItem(tbody, itemId, context);
+ DesignAttributeHandler.writeAttribute("collapsed", tr.attributes(),
+ isCollapsed(itemId), true, boolean.class);
+ return tr;
+ }
+
+ @Override
+ protected TreeTableState getState() {
+ return (TreeTableState) super.getState();
+ }
+}
diff --git a/compatibility-server/src/main/java/com/vaadin/v7/ui/TwinColSelect.java b/compatibility-server/src/main/java/com/vaadin/v7/ui/TwinColSelect.java
new file mode 100644
index 0000000000..68853ddaa1
--- /dev/null
+++ b/compatibility-server/src/main/java/com/vaadin/v7/ui/TwinColSelect.java
@@ -0,0 +1,169 @@
+/*
+ * 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.v7.ui;
+
+import java.util.Collection;
+
+import com.vaadin.server.PaintException;
+import com.vaadin.server.PaintTarget;
+import com.vaadin.shared.ui.twincolselect.TwinColSelectConstants;
+import com.vaadin.shared.ui.twincolselect.TwinColSelectState;
+import com.vaadin.v7.data.Container;
+
+/**
+ * Multiselect component with two lists: left side for available items and right
+ * side for selected items.
+ */
+@SuppressWarnings("serial")
+public class TwinColSelect extends AbstractSelect {
+
+ private int rows = 0;
+
+ private String leftColumnCaption;
+ private String rightColumnCaption;
+
+ /**
+ *
+ */
+ public TwinColSelect() {
+ super();
+ setMultiSelect(true);
+ }
+
+ /**
+ * @param caption
+ */
+ public TwinColSelect(String caption) {
+ super(caption);
+ setMultiSelect(true);
+ }
+
+ /**
+ * @param caption
+ * @param dataSource
+ */
+ public TwinColSelect(String caption, Container dataSource) {
+ super(caption, dataSource);
+ setMultiSelect(true);
+ }
+
+ public int getRows() {
+ return rows;
+ }
+
+ /**
+ * Sets the number of rows in the editor. If the number of rows is set to 0,
+ * the actual number of displayed rows is determined implicitly by the
+ * adapter.
+ * <p>
+ * If a height if set (using {@link #setHeight(String)} or
+ * {@link #setHeight(float, int)}) it overrides the number of rows. Leave
+ * the height undefined to use this method. This is the opposite of how
+ * {@link #setColumns(int)} work.
+ *
+ *
+ * @param rows
+ * the number of rows to set.
+ */
+ public void setRows(int rows) {
+ if (rows < 0) {
+ rows = 0;
+ }
+ if (this.rows != rows) {
+ this.rows = rows;
+ markAsDirty();
+ }
+ }
+
+ /**
+ * @param caption
+ * @param options
+ */
+ public TwinColSelect(String caption, Collection<?> options) {
+ super(caption, options);
+ setMultiSelect(true);
+ }
+
+ @Override
+ public void paintContent(PaintTarget target) throws PaintException {
+ // Adds the number of columns
+ // Adds the number of rows
+ if (rows != 0) {
+ target.addAttribute("rows", rows);
+ }
+
+ // Right and left column captions and/or icons (if set)
+ String lc = getLeftColumnCaption();
+ String rc = getRightColumnCaption();
+ if (lc != null) {
+ target.addAttribute(TwinColSelectConstants.ATTRIBUTE_LEFT_CAPTION,
+ lc);
+ }
+ if (rc != null) {
+ target.addAttribute(TwinColSelectConstants.ATTRIBUTE_RIGHT_CAPTION,
+ rc);
+ }
+
+ super.paintContent(target);
+ }
+
+ /**
+ * Sets the text shown above the right column.
+ *
+ * @param caption
+ * The text to show
+ */
+ public void setRightColumnCaption(String rightColumnCaption) {
+ this.rightColumnCaption = rightColumnCaption;
+ markAsDirty();
+ }
+
+ /**
+ * Returns the text shown above the right column.
+ *
+ * @return The text shown or null if not set.
+ */
+ public String getRightColumnCaption() {
+ return rightColumnCaption;
+ }
+
+ /**
+ * Sets the text shown above the left column.
+ *
+ * @param caption
+ * The text to show
+ */
+ public void setLeftColumnCaption(String leftColumnCaption) {
+ this.leftColumnCaption = leftColumnCaption;
+ markAsDirty();
+ }
+
+ /**
+ * Returns the text shown above the left column.
+ *
+ * @return The text shown or null if not set.
+ */
+ public String getLeftColumnCaption() {
+ return leftColumnCaption;
+ }
+
+ @Override
+ protected TwinColSelectState getState() {
+ return (TwinColSelectState) super.getState();
+ }
+
+}
diff --git a/compatibility-server/src/main/java/com/vaadin/v7/ui/components/calendar/CalendarComponentEvent.java b/compatibility-server/src/main/java/com/vaadin/v7/ui/components/calendar/CalendarComponentEvent.java
new file mode 100644
index 0000000000..230d87b7f7
--- /dev/null
+++ b/compatibility-server/src/main/java/com/vaadin/v7/ui/components/calendar/CalendarComponentEvent.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.v7.ui.components.calendar;
+
+import com.vaadin.ui.Component;
+import com.vaadin.v7.ui.Calendar;
+
+/**
+ * All Calendar events extends this class.
+ *
+ * @since 7.1
+ * @author Vaadin Ltd.
+ *
+ */
+@SuppressWarnings("serial")
+public class CalendarComponentEvent extends Component.Event {
+
+ /**
+ * Set the source of the event
+ *
+ * @param source
+ * The source calendar
+ *
+ */
+ public CalendarComponentEvent(Calendar source) {
+ super(source);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.ui.Component.Event#getComponent()
+ */
+ @Override
+ public Calendar getComponent() {
+ return (Calendar) super.getComponent();
+ }
+}
diff --git a/compatibility-server/src/main/java/com/vaadin/v7/ui/components/calendar/CalendarComponentEvents.java b/compatibility-server/src/main/java/com/vaadin/v7/ui/components/calendar/CalendarComponentEvents.java
new file mode 100644
index 0000000000..14494eedbe
--- /dev/null
+++ b/compatibility-server/src/main/java/com/vaadin/v7/ui/components/calendar/CalendarComponentEvents.java
@@ -0,0 +1,603 @@
+/*
+ * 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.v7.ui.components.calendar;
+
+import java.io.Serializable;
+import java.lang.reflect.Method;
+import java.util.Date;
+import java.util.EventListener;
+
+import com.vaadin.shared.ui.calendar.CalendarEventId;
+import com.vaadin.util.ReflectTools;
+import com.vaadin.v7.ui.Calendar;
+import com.vaadin.v7.ui.components.calendar.event.CalendarEvent;
+
+/**
+ * Interface for all Vaadin Calendar events.
+ *
+ * @since 7.1.0
+ * @author Vaadin Ltd.
+ */
+public interface CalendarComponentEvents extends Serializable {
+
+ /**
+ * Notifier interface for notifying listener of calendar events
+ */
+ public interface CalendarEventNotifier extends Serializable {
+ /**
+ * Get the assigned event handler for the given eventId.
+ *
+ * @param eventId
+ * @return the assigned eventHandler, or null if no handler is assigned
+ */
+ public EventListener getHandler(String eventId);
+ }
+
+ /**
+ * Notifier interface for event drag & drops.
+ */
+ public interface EventMoveNotifier extends CalendarEventNotifier {
+
+ /**
+ * Set the EventMoveHandler.
+ *
+ * @param listener
+ * EventMoveHandler to be added
+ */
+ public void setHandler(EventMoveHandler listener);
+
+ }
+
+ /**
+ * MoveEvent is sent when existing event is dragged to a new position.
+ */
+ @SuppressWarnings("serial")
+ public class MoveEvent extends CalendarComponentEvent {
+
+ public static final String EVENT_ID = CalendarEventId.EVENTMOVE;
+
+ /** Index for the moved Schedule.Event. */
+ private CalendarEvent calendarEvent;
+
+ /** New starting date for the moved Calendar.Event. */
+ private Date newStart;
+
+ /**
+ * MoveEvent needs the target event and new start date.
+ *
+ * @param source
+ * Calendar component.
+ * @param calendarEvent
+ * Target event.
+ * @param newStart
+ * Target event's new start date.
+ */
+ public MoveEvent(Calendar source, CalendarEvent calendarEvent,
+ Date newStart) {
+ super(source);
+
+ this.calendarEvent = calendarEvent;
+ this.newStart = newStart;
+ }
+
+ /**
+ * Get target event.
+ *
+ * @return Target event.
+ */
+ public CalendarEvent getCalendarEvent() {
+ return calendarEvent;
+ }
+
+ /**
+ * Get new start date.
+ *
+ * @return New start date.
+ */
+ public Date getNewStart() {
+ return newStart;
+ }
+ }
+
+ /**
+ * Handler interface for when events are being dragged on the calendar
+ *
+ */
+ public interface EventMoveHandler extends EventListener, Serializable {
+
+ /** Trigger method for the MoveEvent. */
+ public static final Method eventMoveMethod = ReflectTools.findMethod(
+ EventMoveHandler.class, "eventMove", MoveEvent.class);
+
+ /**
+ * This method will be called when event has been moved to a new
+ * position.
+ *
+ * @param event
+ * MoveEvent containing specific information of the new
+ * position and target event.
+ */
+ public void eventMove(MoveEvent event);
+ }
+
+ /**
+ * Handler interface for day or time cell drag-marking with mouse.
+ */
+ public interface RangeSelectNotifier
+ extends Serializable, CalendarEventNotifier {
+
+ /**
+ * Set the RangeSelectHandler that listens for drag-marking.
+ *
+ * @param listener
+ * RangeSelectHandler to be added.
+ */
+ public void setHandler(RangeSelectHandler listener);
+ }
+
+ /**
+ * RangeSelectEvent is sent when day or time cells are drag-marked with
+ * mouse.
+ */
+ @SuppressWarnings("serial")
+ public class RangeSelectEvent extends CalendarComponentEvent {
+
+ public static final String EVENT_ID = CalendarEventId.RANGESELECT;
+
+ /** Calendar event's start date. */
+ private Date start;
+
+ /** Calendar event's end date. */
+ private Date end;
+
+ /**
+ * Defines the event's view mode.
+ */
+ private boolean monthlyMode;
+
+ /**
+ * RangeSelectEvent needs a start and end date.
+ *
+ * @param source
+ * Calendar component.
+ * @param start
+ * Start date.
+ * @param end
+ * End date.
+ * @param monthlyMode
+ * Calendar view mode.
+ */
+ public RangeSelectEvent(Calendar source, Date start, Date end,
+ boolean monthlyMode) {
+ super(source);
+ this.start = start;
+ this.end = end;
+ this.monthlyMode = monthlyMode;
+ }
+
+ /**
+ * Get start date.
+ *
+ * @return Start date.
+ */
+ public Date getStart() {
+ return start;
+ }
+
+ /**
+ * Get end date.
+ *
+ * @return End date.
+ */
+ public Date getEnd() {
+ return end;
+ }
+
+ /**
+ * Gets the event's view mode. Calendar can be be either in monthly or
+ * weekly mode, depending on the active date range.
+ *
+ * @deprecated User {@link Calendar#isMonthlyMode()} instead
+ *
+ * @return Returns true when monthly view is active.
+ */
+ @Deprecated
+ public boolean isMonthlyMode() {
+ return monthlyMode;
+ }
+ }
+
+ /** RangeSelectHandler handles RangeSelectEvent. */
+ public interface RangeSelectHandler extends EventListener, Serializable {
+
+ /** Trigger method for the RangeSelectEvent. */
+ public static final Method rangeSelectMethod = ReflectTools.findMethod(
+ RangeSelectHandler.class, "rangeSelect",
+ RangeSelectEvent.class);
+
+ /**
+ * This method will be called when day or time cells are drag-marked
+ * with mouse.
+ *
+ * @param event
+ * RangeSelectEvent that contains range start and end date.
+ */
+ public void rangeSelect(RangeSelectEvent event);
+ }
+
+ /** Notifier interface for navigation listening. */
+ public interface NavigationNotifier extends Serializable {
+ /**
+ * Add a forward navigation listener.
+ *
+ * @param handler
+ * ForwardHandler to be added.
+ */
+ public void setHandler(ForwardHandler handler);
+
+ /**
+ * Add a backward navigation listener.
+ *
+ * @param handler
+ * BackwardHandler to be added.
+ */
+ public void setHandler(BackwardHandler handler);
+
+ /**
+ * Add a date click listener.
+ *
+ * @param handler
+ * DateClickHandler to be added.
+ */
+ public void setHandler(DateClickHandler handler);
+
+ /**
+ * Add a event click listener.
+ *
+ * @param handler
+ * EventClickHandler to be added.
+ */
+ public void setHandler(EventClickHandler handler);
+
+ /**
+ * Add a week click listener.
+ *
+ * @param handler
+ * WeekClickHandler to be added.
+ */
+ public void setHandler(WeekClickHandler handler);
+ }
+
+ /**
+ * ForwardEvent is sent when forward navigation button is clicked.
+ */
+ @SuppressWarnings("serial")
+ public class ForwardEvent extends CalendarComponentEvent {
+
+ public static final String EVENT_ID = CalendarEventId.FORWARD;
+
+ /**
+ * ForwardEvent needs only the source component.
+ *
+ * @param source
+ * Calendar component.
+ */
+ public ForwardEvent(Calendar source) {
+ super(source);
+ }
+ }
+
+ /** ForwardHandler handles ForwardEvent. */
+ public interface ForwardHandler extends EventListener, Serializable {
+
+ /** Trigger method for the ForwardEvent. */
+ public static final Method forwardMethod = ReflectTools.findMethod(
+ ForwardHandler.class, "forward", ForwardEvent.class);
+
+ /**
+ * This method will be called when date range is moved forward.
+ *
+ * @param event
+ * ForwardEvent
+ */
+ public void forward(ForwardEvent event);
+ }
+
+ /**
+ * BackwardEvent is sent when backward navigation button is clicked.
+ */
+ @SuppressWarnings("serial")
+ public class BackwardEvent extends CalendarComponentEvent {
+
+ public static final String EVENT_ID = CalendarEventId.BACKWARD;
+
+ /**
+ * BackwardEvent needs only the source source component.
+ *
+ * @param source
+ * Calendar component.
+ */
+ public BackwardEvent(Calendar source) {
+ super(source);
+ }
+ }
+
+ /** BackwardHandler handles BackwardEvent. */
+ public interface BackwardHandler extends EventListener, Serializable {
+
+ /** Trigger method for the BackwardEvent. */
+ public static final Method backwardMethod = ReflectTools.findMethod(
+ BackwardHandler.class, "backward", BackwardEvent.class);
+
+ /**
+ * This method will be called when date range is moved backwards.
+ *
+ * @param event
+ * BackwardEvent
+ */
+ public void backward(BackwardEvent event);
+ }
+
+ /**
+ * DateClickEvent is sent when a date is clicked.
+ */
+ @SuppressWarnings("serial")
+ public class DateClickEvent extends CalendarComponentEvent {
+
+ public static final String EVENT_ID = CalendarEventId.DATECLICK;
+
+ /** Date that was clicked. */
+ private Date date;
+
+ /** DateClickEvent needs the target date that was clicked. */
+ public DateClickEvent(Calendar source, Date date) {
+ super(source);
+ this.date = date;
+ }
+
+ /**
+ * Get clicked date.
+ *
+ * @return Clicked date.
+ */
+ public Date getDate() {
+ return date;
+ }
+ }
+
+ /** DateClickHandler handles DateClickEvent. */
+ public interface DateClickHandler extends EventListener, Serializable {
+
+ /** Trigger method for the DateClickEvent. */
+ public static final Method dateClickMethod = ReflectTools.findMethod(
+ DateClickHandler.class, "dateClick", DateClickEvent.class);
+
+ /**
+ * This method will be called when a date is clicked.
+ *
+ * @param event
+ * DateClickEvent containing the target date.
+ */
+ public void dateClick(DateClickEvent event);
+ }
+
+ /**
+ * EventClick is sent when an event is clicked.
+ */
+ @SuppressWarnings("serial")
+ public class EventClick extends CalendarComponentEvent {
+
+ public static final String EVENT_ID = CalendarEventId.EVENTCLICK;
+
+ /** Clicked source event. */
+ private CalendarEvent calendarEvent;
+
+ /** Target source event is needed for the EventClick. */
+ public EventClick(Calendar source, CalendarEvent calendarEvent) {
+ super(source);
+ this.calendarEvent = calendarEvent;
+ }
+
+ /**
+ * Get the clicked event.
+ *
+ * @return Clicked event.
+ */
+ public CalendarEvent getCalendarEvent() {
+ return calendarEvent;
+ }
+ }
+
+ /** EventClickHandler handles EventClick. */
+ public interface EventClickHandler extends EventListener, Serializable {
+
+ /** Trigger method for the EventClick. */
+ public static final Method eventClickMethod = ReflectTools.findMethod(
+ EventClickHandler.class, "eventClick", EventClick.class);
+
+ /**
+ * This method will be called when an event is clicked.
+ *
+ * @param event
+ * EventClick containing the target event.
+ */
+ public void eventClick(EventClick event);
+ }
+
+ /**
+ * WeekClick is sent when week is clicked.
+ */
+ @SuppressWarnings("serial")
+ public class WeekClick extends CalendarComponentEvent {
+
+ public static final String EVENT_ID = CalendarEventId.WEEKCLICK;
+
+ /** Target week. */
+ private int week;
+
+ /** Target year. */
+ private int year;
+
+ /**
+ * WeekClick needs a target year and week.
+ *
+ * @param source
+ * Target source.
+ * @param week
+ * Target week.
+ * @param year
+ * Target year.
+ */
+ public WeekClick(Calendar source, int week, int year) {
+ super(source);
+ this.week = week;
+ this.year = year;
+ }
+
+ /**
+ * Get week as a integer. See {@link java.util.Calendar} for the allowed
+ * values.
+ *
+ * @return Week as a integer.
+ */
+ public int getWeek() {
+ return week;
+ }
+
+ /**
+ * Get year as a integer. See {@link java.util.Calendar} for the allowed
+ * values.
+ *
+ * @return Year as a integer
+ */
+ public int getYear() {
+ return year;
+ }
+ }
+
+ /** WeekClickHandler handles WeekClicks. */
+ public interface WeekClickHandler extends EventListener, Serializable {
+
+ /** Trigger method for the WeekClick. */
+ public static final Method weekClickMethod = ReflectTools.findMethod(
+ WeekClickHandler.class, "weekClick", WeekClick.class);
+
+ /**
+ * This method will be called when a week is clicked.
+ *
+ * @param event
+ * WeekClick containing the target week and year.
+ */
+ public void weekClick(WeekClick event);
+ }
+
+ /**
+ * EventResize is sent when an event is resized
+ */
+ @SuppressWarnings("serial")
+ public class EventResize extends CalendarComponentEvent {
+
+ public static final String EVENT_ID = CalendarEventId.EVENTRESIZE;
+
+ private CalendarEvent calendarEvent;
+
+ private Date startTime;
+
+ private Date endTime;
+
+ public EventResize(Calendar source, CalendarEvent calendarEvent,
+ Date startTime, Date endTime) {
+ super(source);
+ this.calendarEvent = calendarEvent;
+ this.startTime = startTime;
+ this.endTime = endTime;
+ }
+
+ /**
+ * Get target event.
+ *
+ * @return Target event.
+ */
+ public CalendarEvent getCalendarEvent() {
+ return calendarEvent;
+ }
+
+ /**
+ * @deprecated Use {@link #getNewStart()} instead
+ *
+ * @return the new start time
+ */
+ @Deprecated
+ public Date getNewStartTime() {
+ return startTime;
+ }
+
+ /**
+ * Returns the updated start date/time of the event
+ *
+ * @return The new date for the event
+ */
+ public Date getNewStart() {
+ return startTime;
+ }
+
+ /**
+ * @deprecated Use {@link #getNewEnd()} instead
+ *
+ * @return the new end time
+ */
+ @Deprecated
+ public Date getNewEndTime() {
+ return endTime;
+ }
+
+ /**
+ * Returns the updates end date/time of the event
+ *
+ * @return The new date for the event
+ */
+ public Date getNewEnd() {
+ return endTime;
+ }
+ }
+
+ /**
+ * Notifier interface for event resizing.
+ */
+ public interface EventResizeNotifier extends Serializable {
+
+ /**
+ * Set a EventResizeHandler.
+ *
+ * @param handler
+ * EventResizeHandler to be set
+ */
+ public void setHandler(EventResizeHandler handler);
+ }
+
+ /**
+ * Handler for EventResize event.
+ */
+ public interface EventResizeHandler extends EventListener, Serializable {
+
+ /** Trigger method for the EventResize. */
+ public static final Method eventResizeMethod = ReflectTools.findMethod(
+ EventResizeHandler.class, "eventResize", EventResize.class);
+
+ void eventResize(EventResize event);
+ }
+
+}
diff --git a/compatibility-server/src/main/java/com/vaadin/v7/ui/components/calendar/CalendarDateRange.java b/compatibility-server/src/main/java/com/vaadin/v7/ui/components/calendar/CalendarDateRange.java
new file mode 100644
index 0000000000..69a4123c1b
--- /dev/null
+++ b/compatibility-server/src/main/java/com/vaadin/v7/ui/components/calendar/CalendarDateRange.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.v7.ui.components.calendar;
+
+import java.io.Serializable;
+import java.util.Date;
+import java.util.TimeZone;
+
+/**
+ * Class for representing a date range.
+ *
+ * @since 7.1.0
+ * @author Vaadin Ltd.
+ *
+ */
+@SuppressWarnings("serial")
+public class CalendarDateRange implements Serializable {
+
+ private Date start;
+
+ private Date end;
+
+ private final transient TimeZone tz;
+
+ /**
+ * Constructor
+ *
+ * @param start
+ * The start date and time of the date range
+ * @param end
+ * The end date and time of the date range
+ */
+ public CalendarDateRange(Date start, Date end, TimeZone tz) {
+ super();
+ this.start = start;
+ this.end = end;
+ this.tz = tz;
+ }
+
+ /**
+ * Get the start date of the date range
+ *
+ * @return the start Date of the range
+ */
+ public Date getStart() {
+ return start;
+ }
+
+ /**
+ * Get the end date of the date range
+ *
+ * @return the end Date of the range
+ */
+ public Date getEnd() {
+ return end;
+ }
+
+ /**
+ * Is a date in the date range
+ *
+ * @param date
+ * The date to check
+ * @return true if the date range contains a date start and end of range
+ * inclusive; false otherwise
+ */
+ public boolean inRange(Date date) {
+ if (date == null) {
+ return false;
+ }
+
+ return date.compareTo(start) >= 0 && date.compareTo(end) <= 0;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see java.lang.Object#toString()
+ */
+ @Override
+ public String toString() {
+ return "CalendarDateRange [start=" + start + ", end=" + end + "]";
+ }
+
+}
diff --git a/compatibility-server/src/main/java/com/vaadin/v7/ui/components/calendar/CalendarTargetDetails.java b/compatibility-server/src/main/java/com/vaadin/v7/ui/components/calendar/CalendarTargetDetails.java
new file mode 100644
index 0000000000..f9f4100e53
--- /dev/null
+++ b/compatibility-server/src/main/java/com/vaadin/v7/ui/components/calendar/CalendarTargetDetails.java
@@ -0,0 +1,80 @@
+/*
+ * 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.v7.ui.components.calendar;
+
+import java.util.Date;
+import java.util.Map;
+
+import com.vaadin.event.dd.DropTarget;
+import com.vaadin.event.dd.TargetDetailsImpl;
+import com.vaadin.v7.ui.Calendar;
+
+/**
+ * Drop details for {@link com.vaadin.v7.ui.addon.calendar.ui.Calendar Calendar}.
+ * When something is dropped on the Calendar, this class contains the specific
+ * details of the drop point. Specifically, this class gives access to the date
+ * where the drop happened. If the Calendar was in weekly mode, the date also
+ * includes the start time of the slot.
+ *
+ * @since 7.1
+ * @author Vaadin Ltd.
+ */
+@SuppressWarnings("serial")
+public class CalendarTargetDetails extends TargetDetailsImpl {
+
+ private boolean hasDropTime;
+
+ public CalendarTargetDetails(Map<String, Object> rawDropData,
+ DropTarget dropTarget) {
+ super(rawDropData, dropTarget);
+ }
+
+ /**
+ * @return true if {@link #getDropTime()} will return a date object with the
+ * time set to the start of the time slot where the drop happened
+ */
+ public boolean hasDropTime() {
+ return hasDropTime;
+ }
+
+ /**
+ * Does the dropped item have a time associated with it
+ *
+ * @param hasDropTime
+ */
+ public void setHasDropTime(boolean hasDropTime) {
+ this.hasDropTime = hasDropTime;
+ }
+
+ /**
+ * @return the date where the drop happened
+ */
+ public Date getDropTime() {
+ if (hasDropTime) {
+ return (Date) getData("dropTime");
+ } else {
+ return (Date) getData("dropDay");
+ }
+ }
+
+ /**
+ * @return the {@link com.vaadin.v7.ui.addon.calendar.ui.Calendar Calendar}
+ * instance which was the target of the drop
+ */
+ public Calendar getTargetCalendar() {
+ return (Calendar) getTarget();
+ }
+}
diff --git a/compatibility-server/src/main/java/com/vaadin/v7/ui/components/calendar/ContainerEventProvider.java b/compatibility-server/src/main/java/com/vaadin/v7/ui/components/calendar/ContainerEventProvider.java
new file mode 100644
index 0000000000..961b2d8fec
--- /dev/null
+++ b/compatibility-server/src/main/java/com/vaadin/v7/ui/components/calendar/ContainerEventProvider.java
@@ -0,0 +1,566 @@
+/*
+ * 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.v7.ui.components.calendar;
+
+import java.util.Collections;
+import java.util.Date;
+import java.util.LinkedList;
+import java.util.List;
+
+import com.vaadin.v7.data.Container;
+import com.vaadin.v7.data.Container.Indexed;
+import com.vaadin.v7.data.Container.ItemSetChangeEvent;
+import com.vaadin.v7.data.Container.ItemSetChangeNotifier;
+import com.vaadin.v7.data.Item;
+import com.vaadin.v7.data.Property;
+import com.vaadin.v7.data.Property.ValueChangeEvent;
+import com.vaadin.v7.data.Property.ValueChangeNotifier;
+import com.vaadin.v7.ui.components.calendar.CalendarComponentEvents.EventMoveHandler;
+import com.vaadin.v7.ui.components.calendar.CalendarComponentEvents.EventResize;
+import com.vaadin.v7.ui.components.calendar.CalendarComponentEvents.EventResizeHandler;
+import com.vaadin.v7.ui.components.calendar.CalendarComponentEvents.MoveEvent;
+import com.vaadin.v7.ui.components.calendar.event.BasicEvent;
+import com.vaadin.v7.ui.components.calendar.event.CalendarEditableEventProvider;
+import com.vaadin.v7.ui.components.calendar.event.CalendarEvent;
+import com.vaadin.v7.ui.components.calendar.event.CalendarEvent.EventChangeListener;
+import com.vaadin.v7.ui.components.calendar.event.CalendarEvent.EventChangeNotifier;
+import com.vaadin.v7.ui.components.calendar.event.CalendarEventProvider;
+import com.vaadin.v7.ui.components.calendar.event.CalendarEventProvider.EventSetChangeNotifier;
+
+/**
+ * A event provider which uses a {@link Container} as a datasource. Container
+ * used as data source.
+ *
+ * NOTE: The data source must be sorted by date!
+ *
+ * @since 7.1.0
+ * @author Vaadin Ltd.
+ */
+@SuppressWarnings("serial")
+public class ContainerEventProvider
+ implements CalendarEditableEventProvider, EventSetChangeNotifier,
+ EventChangeNotifier, EventMoveHandler, EventResizeHandler,
+ Container.ItemSetChangeListener, Property.ValueChangeListener {
+
+ // Default property ids
+ public static final String CAPTION_PROPERTY = "caption";
+ public static final String DESCRIPTION_PROPERTY = "description";
+ public static final String STARTDATE_PROPERTY = "start";
+ public static final String ENDDATE_PROPERTY = "end";
+ public static final String STYLENAME_PROPERTY = "styleName";
+ public static final String ALL_DAY_PROPERTY = "allDay";
+
+ /**
+ * Internal class to keep the container index which item this event
+ * represents
+ *
+ */
+ private class ContainerCalendarEvent extends BasicEvent {
+ private final int index;
+
+ public ContainerCalendarEvent(int containerIndex) {
+ super();
+ index = containerIndex;
+ }
+
+ public int getContainerIndex() {
+ return index;
+ }
+ }
+
+ /**
+ * Listeners attached to the container
+ */
+ private final List<EventSetChangeListener> eventSetChangeListeners = new LinkedList<CalendarEventProvider.EventSetChangeListener>();
+ private final List<EventChangeListener> eventChangeListeners = new LinkedList<CalendarEvent.EventChangeListener>();
+
+ /**
+ * The event cache contains the events previously created by
+ * {@link #getEvents(Date, Date)}
+ */
+ private final List<CalendarEvent> eventCache = new LinkedList<CalendarEvent>();
+
+ /**
+ * The container used as datasource
+ */
+ private Indexed container;
+
+ /**
+ * Container properties. Defaults based on using the {@link BasicEvent}
+ * helper class.
+ */
+ private Object captionProperty = CAPTION_PROPERTY;
+ private Object descriptionProperty = DESCRIPTION_PROPERTY;
+ private Object startDateProperty = STARTDATE_PROPERTY;
+ private Object endDateProperty = ENDDATE_PROPERTY;
+ private Object styleNameProperty = STYLENAME_PROPERTY;
+ private Object allDayProperty = ALL_DAY_PROPERTY;
+
+ /**
+ * Constructor
+ *
+ * @param container
+ * Container to use as a data source.
+ */
+ public ContainerEventProvider(Container.Indexed container) {
+ this.container = container;
+ listenToContainerEvents();
+ }
+
+ /**
+ * Set the container data source
+ *
+ * @param container
+ * The container to use as datasource
+ *
+ */
+ public void setContainerDataSource(Container.Indexed container) {
+ // Detach the previous container
+ detachContainerDataSource();
+
+ this.container = container;
+ listenToContainerEvents();
+ }
+
+ /**
+ * Returns the container used as data source
+ *
+ */
+ public Container.Indexed getContainerDataSource() {
+ return container;
+ }
+
+ /**
+ * Attaches listeners to the container so container events can be processed
+ */
+ private void listenToContainerEvents() {
+ if (container instanceof ItemSetChangeNotifier) {
+ ((ItemSetChangeNotifier) container).addItemSetChangeListener(this);
+ }
+ if (container instanceof ValueChangeNotifier) {
+ ((ValueChangeNotifier) container).addValueChangeListener(this);
+ }
+ }
+
+ /**
+ * Removes listeners from the container so no events are processed
+ */
+ private void ignoreContainerEvents() {
+ if (container instanceof ItemSetChangeNotifier) {
+ ((ItemSetChangeNotifier) container)
+ .removeItemSetChangeListener(this);
+ }
+ if (container instanceof ValueChangeNotifier) {
+ ((ValueChangeNotifier) container).removeValueChangeListener(this);
+ }
+ }
+
+ /**
+ * Converts an event in the container to an {@link CalendarEvent}
+ *
+ * @param index
+ * The index of the item in the container to get the event for
+ * @return
+ */
+ private CalendarEvent getEvent(int index) {
+
+ // Check the event cache first
+ for (CalendarEvent e : eventCache) {
+ if (e instanceof ContainerCalendarEvent
+ && ((ContainerCalendarEvent) e)
+ .getContainerIndex() == index) {
+ return e;
+ } else if (container.getIdByIndex(index) == e) {
+ return e;
+ }
+ }
+
+ final Object id = container.getIdByIndex(index);
+ Item item = container.getItem(id);
+ CalendarEvent event;
+ if (id instanceof CalendarEvent) {
+ /*
+ * If we are using the BeanItemContainer or another container which
+ * stores the objects as ids then just return the instances
+ */
+ event = (CalendarEvent) id;
+
+ } else {
+ /*
+ * Else we use the properties to create the event
+ */
+ BasicEvent basicEvent = new ContainerCalendarEvent(index);
+
+ // Set values from property values
+ if (captionProperty != null
+ && item.getItemPropertyIds().contains(captionProperty)) {
+ basicEvent.setCaption(String.valueOf(
+ item.getItemProperty(captionProperty).getValue()));
+ }
+ if (descriptionProperty != null && item.getItemPropertyIds()
+ .contains(descriptionProperty)) {
+ basicEvent.setDescription(String.valueOf(
+ item.getItemProperty(descriptionProperty).getValue()));
+ }
+ if (startDateProperty != null
+ && item.getItemPropertyIds().contains(startDateProperty)) {
+ basicEvent.setStart((Date) item
+ .getItemProperty(startDateProperty).getValue());
+ }
+ if (endDateProperty != null
+ && item.getItemPropertyIds().contains(endDateProperty)) {
+ basicEvent.setEnd((Date) item.getItemProperty(endDateProperty)
+ .getValue());
+ }
+ if (styleNameProperty != null
+ && item.getItemPropertyIds().contains(styleNameProperty)) {
+ basicEvent.setStyleName(String.valueOf(
+ item.getItemProperty(styleNameProperty).getValue()));
+ }
+ if (allDayProperty != null
+ && item.getItemPropertyIds().contains(allDayProperty)) {
+ basicEvent.setAllDay((Boolean) item
+ .getItemProperty(allDayProperty).getValue());
+ }
+ event = basicEvent;
+ }
+ return event;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.addon.calendar.event.CalendarEventProvider#getEvents(java.
+ * util.Date, java.util.Date)
+ */
+ @Override
+ public List<CalendarEvent> getEvents(Date startDate, Date endDate) {
+ eventCache.clear();
+ int size = container.size();
+ assert size >= 0;
+
+ for (int i = 0; i < size; i++) {
+ Object id = container.getIdByIndex(i);
+ Item item = container.getItem(id);
+ boolean add = true;
+ if (startDate != null) {
+ Date eventEnd = (Date) item.getItemProperty(endDateProperty)
+ .getValue();
+ if (eventEnd.compareTo(startDate) < 0) {
+ add = false;
+ }
+ }
+ if (add && endDate != null) {
+ Date eventStart = (Date) item.getItemProperty(startDateProperty)
+ .getValue();
+ if (eventStart.compareTo(endDate) >= 0) {
+ break; // because container is sorted, all further events
+ // will be even later
+ }
+ }
+ if (add) {
+ eventCache.add(getEvent(i));
+ }
+ }
+ return Collections.unmodifiableList(eventCache);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.addon.calendar.event.CalendarEventProvider.
+ * EventSetChangeNotifier
+ * #addListener(com.vaadin.addon.calendar.event.CalendarEventProvider.
+ * EventSetChangeListener)
+ */
+ @Override
+ public void addEventSetChangeListener(EventSetChangeListener listener) {
+ if (!eventSetChangeListeners.contains(listener)) {
+ eventSetChangeListeners.add(listener);
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.addon.calendar.event.CalendarEventProvider.
+ * EventSetChangeNotifier
+ * #removeListener(com.vaadin.addon.calendar.event.CalendarEventProvider.
+ * EventSetChangeListener)
+ */
+ @Override
+ public void removeEventSetChangeListener(EventSetChangeListener listener) {
+ eventSetChangeListeners.remove(listener);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.addon.calendar.event.CalendarEvent.EventChangeNotifier#
+ * addListener
+ * (com.vaadin.addon.calendar.event.CalendarEvent.EventChangeListener)
+ */
+ @Override
+ public void addEventChangeListener(EventChangeListener listener) {
+ if (eventChangeListeners.contains(listener)) {
+ eventChangeListeners.add(listener);
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.addon.calendar.event.CalendarEvent.EventChangeNotifier#
+ * removeListener
+ * (com.vaadin.addon.calendar.event.CalendarEvent.EventChangeListener)
+ */
+ @Override
+ public void removeEventChangeListener(EventChangeListener listener) {
+ eventChangeListeners.remove(listener);
+ }
+
+ /**
+ * Get the property which provides the caption of the event
+ */
+ public Object getCaptionProperty() {
+ return captionProperty;
+ }
+
+ /**
+ * Set the property which provides the caption of the event
+ */
+ public void setCaptionProperty(Object captionProperty) {
+ this.captionProperty = captionProperty;
+ }
+
+ /**
+ * Get the property which provides the description of the event
+ */
+ public Object getDescriptionProperty() {
+ return descriptionProperty;
+ }
+
+ /**
+ * Set the property which provides the description of the event
+ */
+ public void setDescriptionProperty(Object descriptionProperty) {
+ this.descriptionProperty = descriptionProperty;
+ }
+
+ /**
+ * Get the property which provides the starting date and time of the event
+ */
+ public Object getStartDateProperty() {
+ return startDateProperty;
+ }
+
+ /**
+ * Set the property which provides the starting date and time of the event
+ */
+ public void setStartDateProperty(Object startDateProperty) {
+ this.startDateProperty = startDateProperty;
+ }
+
+ /**
+ * Get the property which provides the ending date and time of the event
+ */
+ public Object getEndDateProperty() {
+ return endDateProperty;
+ }
+
+ /**
+ * Set the property which provides the ending date and time of the event
+ */
+ public void setEndDateProperty(Object endDateProperty) {
+ this.endDateProperty = endDateProperty;
+ }
+
+ /**
+ * Get the property which provides the style name for the event
+ */
+ public Object getStyleNameProperty() {
+ return styleNameProperty;
+ }
+
+ /**
+ * Set the property which provides the style name for the event
+ */
+ public void setStyleNameProperty(Object styleNameProperty) {
+ this.styleNameProperty = styleNameProperty;
+ }
+
+ /**
+ * Set the all day property for the event
+ *
+ * @since 7.3.4
+ */
+ public void setAllDayProperty(Object allDayProperty) {
+ this.allDayProperty = allDayProperty;
+ }
+
+ /**
+ * Get the all day property for the event
+ *
+ * @since 7.3.4
+ */
+ public Object getAllDayProperty() {
+ return allDayProperty;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.data.Container.ItemSetChangeListener#containerItemSetChange
+ * (com.vaadin.data.Container.ItemSetChangeEvent)
+ */
+ @Override
+ public void containerItemSetChange(ItemSetChangeEvent event) {
+ if (event.getContainer() == container) {
+ // Trigger an eventset change event when the itemset changes
+ for (EventSetChangeListener listener : eventSetChangeListeners) {
+ listener.eventSetChange(new EventSetChangeEvent(this));
+ }
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.data.Property.ValueChangeListener#valueChange(com.vaadin.data
+ * .Property.ValueChangeEvent)
+ */
+ @Override
+ public void valueChange(ValueChangeEvent event) {
+ /*
+ * TODO Need to figure out how to get the item which triggered the the
+ * valuechange event and then trigger a EventChange event to the
+ * listeners
+ */
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.addon.calendar.ui.CalendarComponentEvents.EventMoveHandler
+ * #eventMove
+ * (com.vaadin.addon.calendar.ui.CalendarComponentEvents.MoveEvent)
+ */
+ @Override
+ public void eventMove(MoveEvent event) {
+ CalendarEvent ce = event.getCalendarEvent();
+ if (eventCache.contains(ce)) {
+ int index;
+ if (ce instanceof ContainerCalendarEvent) {
+ index = ((ContainerCalendarEvent) ce).getContainerIndex();
+ } else {
+ index = container.indexOfId(ce);
+ }
+
+ long eventLength = ce.getEnd().getTime() - ce.getStart().getTime();
+ Date newEnd = new Date(event.getNewStart().getTime() + eventLength);
+
+ ignoreContainerEvents();
+ Item item = container.getItem(container.getIdByIndex(index));
+ item.getItemProperty(startDateProperty)
+ .setValue(event.getNewStart());
+ item.getItemProperty(endDateProperty).setValue(newEnd);
+ listenToContainerEvents();
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.addon.calendar.ui.CalendarComponentEvents.EventResizeHandler
+ * #eventResize
+ * (com.vaadin.addon.calendar.ui.CalendarComponentEvents.EventResize)
+ */
+ @Override
+ public void eventResize(EventResize event) {
+ CalendarEvent ce = event.getCalendarEvent();
+ if (eventCache.contains(ce)) {
+ int index;
+ if (ce instanceof ContainerCalendarEvent) {
+ index = ((ContainerCalendarEvent) ce).getContainerIndex();
+ } else {
+ index = container.indexOfId(ce);
+ }
+ ignoreContainerEvents();
+ Item item = container.getItem(container.getIdByIndex(index));
+ item.getItemProperty(startDateProperty)
+ .setValue(event.getNewStart());
+ item.getItemProperty(endDateProperty).setValue(event.getNewEnd());
+ listenToContainerEvents();
+ }
+ }
+
+ /**
+ * If you are reusing the container which previously have been attached to
+ * this ContainerEventProvider call this method to remove this event
+ * providers container listeners before attaching it to an other
+ * ContainerEventProvider
+ */
+ public void detachContainerDataSource() {
+ ignoreContainerEvents();
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.addon.calendar.event.CalendarEditableEventProvider#addEvent
+ * (com.vaadin.addon.calendar.event.CalendarEvent)
+ */
+ @Override
+ public void addEvent(CalendarEvent event) {
+ Item item;
+ try {
+ item = container.addItem(event);
+ } catch (UnsupportedOperationException uop) {
+ // Thrown if container does not support adding items with custom
+ // ids. JPAContainer for example.
+ item = container.getItem(container.addItem());
+ }
+ if (item != null) {
+ item.getItemProperty(getCaptionProperty())
+ .setValue(event.getCaption());
+ item.getItemProperty(getStartDateProperty())
+ .setValue(event.getStart());
+ item.getItemProperty(getEndDateProperty()).setValue(event.getEnd());
+ item.getItemProperty(getStyleNameProperty())
+ .setValue(event.getStyleName());
+ item.getItemProperty(getDescriptionProperty())
+ .setValue(event.getDescription());
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.addon.calendar.event.CalendarEditableEventProvider#removeEvent
+ * (com.vaadin.addon.calendar.event.CalendarEvent)
+ */
+ @Override
+ public void removeEvent(CalendarEvent event) {
+ container.removeItem(event);
+ }
+}
diff --git a/compatibility-server/src/main/java/com/vaadin/v7/ui/components/calendar/event/BasicEvent.java b/compatibility-server/src/main/java/com/vaadin/v7/ui/components/calendar/event/BasicEvent.java
new file mode 100644
index 0000000000..f1b6524d2c
--- /dev/null
+++ b/compatibility-server/src/main/java/com/vaadin/v7/ui/components/calendar/event/BasicEvent.java
@@ -0,0 +1,265 @@
+/*
+ * 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.v7.ui.components.calendar.event;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+import com.vaadin.v7.ui.components.calendar.event.CalendarEvent.EventChangeNotifier;
+
+/**
+ * Simple implementation of {@link com.vaadin.addon.calendar.event.CalendarEvent
+ * CalendarEvent}. Has setters for all required fields and fires events when
+ * this event is changed.
+ *
+ * @since 7.1.0
+ * @author Vaadin Ltd.
+ */
+@SuppressWarnings("serial")
+public class BasicEvent implements EditableCalendarEvent, EventChangeNotifier {
+
+ private String caption;
+ private String description;
+ private Date end;
+ private Date start;
+ private String styleName;
+ private transient List<EventChangeListener> listeners = new ArrayList<EventChangeListener>();
+
+ private boolean isAllDay;
+
+ /**
+ * Default constructor
+ */
+ public BasicEvent() {
+
+ }
+
+ /**
+ * Constructor for creating an event with the same start and end date
+ *
+ * @param caption
+ * The caption for the event
+ * @param description
+ * The description for the event
+ * @param date
+ * The date the event occurred
+ */
+ public BasicEvent(String caption, String description, Date date) {
+ this.caption = caption;
+ this.description = description;
+ start = date;
+ end = date;
+ }
+
+ /**
+ * Constructor for creating an event with a start date and an end date.
+ * Start date should be before the end date
+ *
+ * @param caption
+ * The caption for the event
+ * @param description
+ * The description for the event
+ * @param startDate
+ * The start date of the event
+ * @param endDate
+ * The end date of the event
+ */
+ public BasicEvent(String caption, String description, Date startDate,
+ Date endDate) {
+ this.caption = caption;
+ this.description = description;
+ start = startDate;
+ end = endDate;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.addon.calendar.event.CalendarEvent#getCaption()
+ */
+ @Override
+ public String getCaption() {
+ return caption;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.addon.calendar.event.CalendarEvent#getDescription()
+ */
+ @Override
+ public String getDescription() {
+ return description;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.addon.calendar.event.CalendarEvent#getEnd()
+ */
+ @Override
+ public Date getEnd() {
+ return end;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.addon.calendar.event.CalendarEvent#getStart()
+ */
+ @Override
+ public Date getStart() {
+ return start;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.addon.calendar.event.CalendarEvent#getStyleName()
+ */
+ @Override
+ public String getStyleName() {
+ return styleName;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.addon.calendar.event.CalendarEvent#isAllDay()
+ */
+ @Override
+ public boolean isAllDay() {
+ return isAllDay;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.addon.calendar.event.CalendarEventEditor#setCaption(java.lang
+ * .String)
+ */
+ @Override
+ public void setCaption(String caption) {
+ this.caption = caption;
+ fireEventChange();
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.addon.calendar.event.CalendarEventEditor#setDescription(java
+ * .lang.String)
+ */
+ @Override
+ public void setDescription(String description) {
+ this.description = description;
+ fireEventChange();
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.addon.calendar.event.CalendarEventEditor#setEnd(java.util.
+ * Date)
+ */
+ @Override
+ public void setEnd(Date end) {
+ this.end = end;
+ fireEventChange();
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.addon.calendar.event.CalendarEventEditor#setStart(java.util
+ * .Date)
+ */
+ @Override
+ public void setStart(Date start) {
+ this.start = start;
+ fireEventChange();
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.addon.calendar.event.CalendarEventEditor#setStyleName(java
+ * .lang.String)
+ */
+ @Override
+ public void setStyleName(String styleName) {
+ this.styleName = styleName;
+ fireEventChange();
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.addon.calendar.event.CalendarEventEditor#setAllDay(boolean)
+ */
+ @Override
+ public void setAllDay(boolean isAllDay) {
+ this.isAllDay = isAllDay;
+ fireEventChange();
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.addon.calendar.ui.CalendarComponentEvents.EventChangeNotifier
+ * #addListener
+ * (com.vaadin.addon.calendar.ui.CalendarComponentEvents.EventChangeListener
+ * )
+ */
+ @Override
+ public void addEventChangeListener(EventChangeListener listener) {
+ listeners.add(listener);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.addon.calendar.ui.CalendarComponentEvents.EventChangeNotifier
+ * #removeListener
+ * (com.vaadin.addon.calendar.ui.CalendarComponentEvents.EventChangeListener
+ * )
+ */
+ @Override
+ public void removeEventChangeListener(EventChangeListener listener) {
+ listeners.remove(listener);
+ }
+
+ /**
+ * Fires an event change event to the listeners. Should be triggered when
+ * some property of the event changes.
+ */
+ protected void fireEventChange() {
+ EventChangeEvent event = new EventChangeEvent(this);
+
+ for (EventChangeListener listener : listeners) {
+ listener.eventChange(event);
+ }
+ }
+}
diff --git a/compatibility-server/src/main/java/com/vaadin/v7/ui/components/calendar/event/BasicEventProvider.java b/compatibility-server/src/main/java/com/vaadin/v7/ui/components/calendar/event/BasicEventProvider.java
new file mode 100644
index 0000000000..59c8baca9c
--- /dev/null
+++ b/compatibility-server/src/main/java/com/vaadin/v7/ui/components/calendar/event/BasicEventProvider.java
@@ -0,0 +1,177 @@
+/*
+ * 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.v7.ui.components.calendar.event;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+import com.vaadin.v7.ui.components.calendar.event.CalendarEvent.EventChangeEvent;
+import com.vaadin.v7.ui.components.calendar.event.CalendarEventProvider.EventSetChangeNotifier;
+
+/**
+ * <p>
+ * Simple implementation of
+ * {@link com.vaadin.addon.calendar.event.CalendarEventProvider
+ * CalendarEventProvider}. Use {@link #addEvent(CalendarEvent)} and
+ * {@link #removeEvent(CalendarEvent)} to add / remove events.
+ * </p>
+ *
+ * <p>
+ * {@link com.vaadin.addon.calendar.event.CalendarEventProvider.EventSetChangeNotifier
+ * EventSetChangeNotifier} and
+ * {@link com.vaadin.addon.calendar.event.CalendarEvent.EventChangeListener
+ * EventChangeListener} are also implemented, so the Calendar is notified when
+ * an event is added, changed or removed.
+ * </p>
+ *
+ * @since 7.1.0
+ * @author Vaadin Ltd.
+ */
+@SuppressWarnings("serial")
+public class BasicEventProvider implements CalendarEditableEventProvider,
+ EventSetChangeNotifier, CalendarEvent.EventChangeListener {
+
+ protected List<CalendarEvent> eventList = new ArrayList<CalendarEvent>();
+
+ private List<EventSetChangeListener> listeners = new ArrayList<EventSetChangeListener>();
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.addon.calendar.event.CalendarEventProvider#getEvents(java.
+ * util.Date, java.util.Date)
+ */
+ @Override
+ public List<CalendarEvent> getEvents(Date startDate, Date endDate) {
+ ArrayList<CalendarEvent> activeEvents = new ArrayList<CalendarEvent>();
+
+ for (CalendarEvent ev : eventList) {
+ long from = startDate.getTime();
+ long to = endDate.getTime();
+
+ if (ev.getStart() != null && ev.getEnd() != null) {
+ long f = ev.getStart().getTime();
+ long t = ev.getEnd().getTime();
+ // Select only events that overlaps with startDate and
+ // endDate.
+ if ((f <= to && f >= from) || (t >= from && t <= to)
+ || (f <= from && t >= to)) {
+ activeEvents.add(ev);
+ }
+ }
+ }
+
+ return activeEvents;
+ }
+
+ /**
+ * Does this event provider container this event
+ *
+ * @param event
+ * The event to check for
+ * @return If this provider has the event then true is returned, else false
+ */
+ public boolean containsEvent(BasicEvent event) {
+ return eventList.contains(event);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.addon.calendar.ui.CalendarComponentEvents.
+ * EventSetChangeNotifier #addListener
+ * (com.vaadin.addon.calendar.ui.CalendarComponentEvents.
+ * EventSetChangeListener )
+ */
+ @Override
+ public void addEventSetChangeListener(EventSetChangeListener listener) {
+ listeners.add(listener);
+
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.addon.calendar.ui.CalendarComponentEvents.
+ * EventSetChangeNotifier #removeListener
+ * (com.vaadin.addon.calendar.ui.CalendarComponentEvents.
+ * EventSetChangeListener )
+ */
+ @Override
+ public void removeEventSetChangeListener(EventSetChangeListener listener) {
+ listeners.remove(listener);
+ }
+
+ /**
+ * Fires a eventsetchange event. The event is fired when either an event is
+ * added or removed to the event provider
+ */
+ protected void fireEventSetChange() {
+ EventSetChangeEvent event = new EventSetChangeEvent(this);
+
+ for (EventSetChangeListener listener : listeners) {
+ listener.eventSetChange(event);
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.addon.calendar.ui.CalendarComponentEvents.EventChangeListener
+ * #eventChange
+ * (com.vaadin.addon.calendar.ui.CalendarComponentEvents.EventSetChange)
+ */
+ @Override
+ public void eventChange(EventChangeEvent changeEvent) {
+ // naive implementation
+ fireEventSetChange();
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.addon.calendar.event.CalendarEditableEventProvider#addEvent
+ * (com.vaadin.addon.calendar.event.CalendarEvent)
+ */
+ @Override
+ public void addEvent(CalendarEvent event) {
+ eventList.add(event);
+ if (event instanceof BasicEvent) {
+ ((BasicEvent) event).addEventChangeListener(this);
+ }
+ fireEventSetChange();
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.addon.calendar.event.CalendarEditableEventProvider#removeEvent
+ * (com.vaadin.addon.calendar.event.CalendarEvent)
+ */
+ @Override
+ public void removeEvent(CalendarEvent event) {
+ eventList.remove(event);
+ if (event instanceof BasicEvent) {
+ ((BasicEvent) event).removeEventChangeListener(this);
+ }
+ fireEventSetChange();
+ }
+}
diff --git a/compatibility-server/src/main/java/com/vaadin/v7/ui/components/calendar/event/CalendarEditableEventProvider.java b/compatibility-server/src/main/java/com/vaadin/v7/ui/components/calendar/event/CalendarEditableEventProvider.java
new file mode 100644
index 0000000000..90f8992720
--- /dev/null
+++ b/compatibility-server/src/main/java/com/vaadin/v7/ui/components/calendar/event/CalendarEditableEventProvider.java
@@ -0,0 +1,42 @@
+/*
+ * 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.v7.ui.components.calendar.event;
+
+/**
+ * An event provider which allows adding and removing events
+ *
+ * @since 7.1.0
+ * @author Vaadin Ltd.
+ */
+public interface CalendarEditableEventProvider extends CalendarEventProvider {
+
+ /**
+ * Adds an event to the event provider
+ *
+ * @param event
+ * The event to add
+ */
+ void addEvent(CalendarEvent event);
+
+ /**
+ * Removes an event from the event provider
+ *
+ * @param event
+ * The event
+ */
+ void removeEvent(CalendarEvent event);
+}
diff --git a/compatibility-server/src/main/java/com/vaadin/v7/ui/components/calendar/event/CalendarEvent.java b/compatibility-server/src/main/java/com/vaadin/v7/ui/components/calendar/event/CalendarEvent.java
new file mode 100644
index 0000000000..9aaa1cd5ba
--- /dev/null
+++ b/compatibility-server/src/main/java/com/vaadin/v7/ui/components/calendar/event/CalendarEvent.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.v7.ui.components.calendar.event;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * <p>
+ * Event in the calendar. Customize your own event by implementing this
+ * interface.
+ * </p>
+ *
+ * <li>Start and end fields are mandatory.</li>
+ *
+ * <li>In "allDay" events longer than one day, starting and ending clock times
+ * are omitted in UI and only dates are shown.</li>
+ *
+ * @since 7.1.0
+ * @author Vaadin Ltd.
+ *
+ */
+public interface CalendarEvent extends Serializable {
+
+ /**
+ * Gets start date of event.
+ *
+ * @return Start date.
+ */
+ public Date getStart();
+
+ /**
+ * Get end date of event.
+ *
+ * @return End date;
+ */
+ public Date getEnd();
+
+ /**
+ * Gets caption of event.
+ *
+ * @return Caption text
+ */
+ public String getCaption();
+
+ /**
+ * Gets description of event. Shown as a tooltip over the event.
+ *
+ * @return Description text.
+ */
+ public String getDescription();
+
+ /**
+ * <p>
+ * Gets style name of event. In the client, style name will be set to the
+ * event's element class name and can be styled by CSS
+ * </p>
+ * Styling example:</br>
+ * <code>Java code: </br>
+ * event.setStyleName("color1");
+ * </br></br>
+ * CSS:</br>
+ * .v-calendar-event-color1 {</br>
+ * &nbsp;&nbsp;&nbsp;background-color: #9effae;</br>}</code>
+ *
+ * @return Style name.
+ */
+ public String getStyleName();
+
+ /**
+ * An all-day event typically does not occur at a specific time but targets
+ * a whole day or days. The rendering of all-day events differs from normal
+ * events.
+ *
+ * @return true if this event is an all-day event, false otherwise
+ */
+ public boolean isAllDay();
+
+ /**
+ * Event to signal that an event has changed.
+ */
+ @SuppressWarnings("serial")
+ public class EventChangeEvent implements Serializable {
+
+ private CalendarEvent source;
+
+ public EventChangeEvent(CalendarEvent source) {
+ this.source = source;
+ }
+
+ /**
+ * @return the {@link com.vaadin.addon.calendar.event.CalendarEvent
+ * CalendarEvent} that has changed
+ */
+ public CalendarEvent getCalendarEvent() {
+ return source;
+ }
+ }
+
+ /**
+ * Listener for EventSetChange events.
+ */
+ public interface EventChangeListener extends Serializable {
+
+ /**
+ * Called when an Event has changed.
+ */
+ public void eventChange(EventChangeEvent eventChangeEvent);
+ }
+
+ /**
+ * Notifier interface for EventChange events.
+ */
+ public interface EventChangeNotifier extends Serializable {
+
+ /**
+ * Add a listener to listen for EventChangeEvents. These events are
+ * fired when a events properties are changed.
+ *
+ * @param listener
+ * The listener to add
+ */
+ void addEventChangeListener(EventChangeListener listener);
+
+ /**
+ * Remove a listener from the event provider.
+ *
+ * @param listener
+ * The listener to remove
+ */
+ void removeEventChangeListener(EventChangeListener listener);
+ }
+
+}
diff --git a/compatibility-server/src/main/java/com/vaadin/v7/ui/components/calendar/event/CalendarEventProvider.java b/compatibility-server/src/main/java/com/vaadin/v7/ui/components/calendar/event/CalendarEventProvider.java
new file mode 100644
index 0000000000..bef6aaea18
--- /dev/null
+++ b/compatibility-server/src/main/java/com/vaadin/v7/ui/components/calendar/event/CalendarEventProvider.java
@@ -0,0 +1,112 @@
+/*
+ * 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.v7.ui.components.calendar.event;
+
+import java.io.Serializable;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * Interface for querying events. The Vaadin Calendar always has a
+ * CalendarEventProvider set.
+ *
+ * @since 7.1.0
+ * @author Vaadin Ltd.
+ */
+public interface CalendarEventProvider extends Serializable {
+ /**
+ * <p>
+ * Gets all available events in the target date range between startDate and
+ * endDate. The Vaadin Calendar queries the events from the range that is
+ * shown, which is not guaranteed to be the same as the date range that is
+ * set.
+ * </p>
+ *
+ * <p>
+ * For example, if you set the date range to be monday 22.2.2010 - wednesday
+ * 24.2.2010, the used Event Provider will be queried for events between
+ * monday 22.2.2010 00:00 and sunday 28.2.2010 23:59. Generally you can
+ * expect the date range to be expanded to whole days and whole weeks.
+ * </p>
+ *
+ * @param startDate
+ * Start date
+ * @param endDate
+ * End date
+ * @return List of events
+ */
+ public List<CalendarEvent> getEvents(Date startDate, Date endDate);
+
+ /**
+ * Event to signal that the set of events has changed and the calendar
+ * should refresh its view from the
+ * {@link com.vaadin.addon.calendar.event.CalendarEventProvider
+ * CalendarEventProvider} .
+ *
+ */
+ @SuppressWarnings("serial")
+ public class EventSetChangeEvent implements Serializable {
+
+ private CalendarEventProvider source;
+
+ public EventSetChangeEvent(CalendarEventProvider source) {
+ this.source = source;
+ }
+
+ /**
+ * @return the
+ * {@link com.vaadin.addon.calendar.event.CalendarEventProvider
+ * CalendarEventProvider} that has changed
+ */
+ public CalendarEventProvider getProvider() {
+ return source;
+ }
+ }
+
+ /**
+ * Listener for EventSetChange events.
+ */
+ public interface EventSetChangeListener extends Serializable {
+
+ /**
+ * Called when the set of Events has changed.
+ */
+ public void eventSetChange(EventSetChangeEvent changeEvent);
+ }
+
+ /**
+ * Notifier interface for EventSetChange events.
+ */
+ public interface EventSetChangeNotifier extends Serializable {
+
+ /**
+ * Add a listener for listening to when new events are adding or removed
+ * from the event provider.
+ *
+ * @param listener
+ * The listener to add
+ */
+ void addEventSetChangeListener(EventSetChangeListener listener);
+
+ /**
+ * Remove a listener which listens to {@link EventSetChangeEvent}-events
+ *
+ * @param listener
+ * The listener to remove
+ */
+ void removeEventSetChangeListener(EventSetChangeListener listener);
+ }
+}
diff --git a/compatibility-server/src/main/java/com/vaadin/v7/ui/components/calendar/event/EditableCalendarEvent.java b/compatibility-server/src/main/java/com/vaadin/v7/ui/components/calendar/event/EditableCalendarEvent.java
new file mode 100644
index 0000000000..cb3553ec87
--- /dev/null
+++ b/compatibility-server/src/main/java/com/vaadin/v7/ui/components/calendar/event/EditableCalendarEvent.java
@@ -0,0 +1,91 @@
+/*
+ * 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.v7.ui.components.calendar.event;
+
+import java.util.Date;
+
+/**
+ * <p>
+ * Extension to the basic {@link com.vaadin.addon.calendar.event.CalendarEvent
+ * CalendarEvent}. This interface provides setters (and thus editing
+ * capabilities) for all {@link com.vaadin.addon.calendar.event.CalendarEvent
+ * CalendarEvent} fields. For descriptions on the fields, refer to the extended
+ * interface.
+ * </p>
+ *
+ * <p>
+ * This interface is used by some of the basic Calendar event handlers in the
+ * <code>com.vaadin.addon.calendar.ui.handler</code> package to determine
+ * whether an event can be edited.
+ * </p>
+ *
+ * @since 7.1
+ * @author Vaadin Ltd.
+ */
+public interface EditableCalendarEvent extends CalendarEvent {
+
+ /**
+ * Set the visible text in the calendar for the event.
+ *
+ * @param caption
+ * The text to show in the calendar
+ */
+ void setCaption(String caption);
+
+ /**
+ * Set the description of the event. This is shown in the calendar when
+ * hoovering over the event.
+ *
+ * @param description
+ * The text which describes the event
+ */
+ void setDescription(String description);
+
+ /**
+ * Set the end date of the event. Must be after the start date.
+ *
+ * @param end
+ * The end date to set
+ */
+ void setEnd(Date end);
+
+ /**
+ * Set the start date for the event. Must be before the end date
+ *
+ * @param start
+ * The start date of the event
+ */
+ void setStart(Date start);
+
+ /**
+ * Set the style name for the event used for styling the event cells
+ *
+ * @param styleName
+ * The stylename to use
+ *
+ */
+ void setStyleName(String styleName);
+
+ /**
+ * Does the event span the whole day. If so then set this to true
+ *
+ * @param isAllDay
+ * True if the event spans the whole day. In this case the start
+ * and end times are ignored.
+ */
+ void setAllDay(boolean isAllDay);
+
+}
diff --git a/compatibility-server/src/main/java/com/vaadin/v7/ui/components/calendar/handler/BasicBackwardHandler.java b/compatibility-server/src/main/java/com/vaadin/v7/ui/components/calendar/handler/BasicBackwardHandler.java
new file mode 100644
index 0000000000..956db6b179
--- /dev/null
+++ b/compatibility-server/src/main/java/com/vaadin/v7/ui/components/calendar/handler/BasicBackwardHandler.java
@@ -0,0 +1,96 @@
+/*
+ * 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.v7.ui.components.calendar.handler;
+
+import java.util.Calendar;
+import java.util.Date;
+
+import com.vaadin.shared.ui.calendar.DateConstants;
+import com.vaadin.v7.ui.components.calendar.CalendarComponentEvents.BackwardEvent;
+import com.vaadin.v7.ui.components.calendar.CalendarComponentEvents.BackwardHandler;
+
+/**
+ * Implements basic functionality needed to enable backwards navigation.
+ *
+ * @since 7.1
+ * @author Vaadin Ltd.
+ */
+@SuppressWarnings("serial")
+public class BasicBackwardHandler implements BackwardHandler {
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.addon.calendar.ui.CalendarComponentEvents.BackwardHandler#
+ * backward
+ * (com.vaadin.addon.calendar.ui.CalendarComponentEvents.BackwardEvent)
+ */
+ @Override
+ public void backward(BackwardEvent event) {
+ Date start = event.getComponent().getStartDate();
+ Date end = event.getComponent().getEndDate();
+
+ // calculate amount to move back
+ int durationInDays = (int) (((end.getTime()) - start.getTime())
+ / DateConstants.DAYINMILLIS);
+ durationInDays++;
+ // for week view durationInDays = -7, for day view durationInDays = -1
+ durationInDays = -durationInDays;
+
+ // set new start and end times
+ Calendar javaCalendar = event.getComponent().getInternalCalendar();
+ javaCalendar.setTime(start);
+ javaCalendar.add(java.util.Calendar.DATE, durationInDays);
+ Date newStart = javaCalendar.getTime();
+
+ javaCalendar.setTime(end);
+ javaCalendar.add(java.util.Calendar.DATE, durationInDays);
+ Date newEnd = javaCalendar.getTime();
+
+ if (start.equals(end)) { // day view
+ int firstDay = event.getComponent().getFirstVisibleDayOfWeek();
+ int lastDay = event.getComponent().getLastVisibleDayOfWeek();
+ int dayOfWeek = javaCalendar.get(Calendar.DAY_OF_WEEK);
+
+ // we suppose that 7 >= lastDay >= firstDay >= 1
+ while (!(firstDay <= dayOfWeek && dayOfWeek <= lastDay)) {
+ javaCalendar.add(java.util.Calendar.DATE, -1);
+ dayOfWeek = javaCalendar.get(Calendar.DAY_OF_WEEK);
+ }
+
+ newStart = javaCalendar.getTime();
+ newEnd = javaCalendar.getTime();
+ }
+
+ setDates(event, newStart, newEnd);
+ }
+
+ /**
+ * Set the start and end dates for the event
+ *
+ * @param event
+ * The event that the start and end dates should be set
+ * @param start
+ * The start date
+ * @param end
+ * The end date
+ */
+ protected void setDates(BackwardEvent event, Date start, Date end) {
+ event.getComponent().setStartDate(start);
+ event.getComponent().setEndDate(end);
+ }
+}
diff --git a/compatibility-server/src/main/java/com/vaadin/v7/ui/components/calendar/handler/BasicDateClickHandler.java b/compatibility-server/src/main/java/com/vaadin/v7/ui/components/calendar/handler/BasicDateClickHandler.java
new file mode 100644
index 0000000000..381acb6b1d
--- /dev/null
+++ b/compatibility-server/src/main/java/com/vaadin/v7/ui/components/calendar/handler/BasicDateClickHandler.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.v7.ui.components.calendar.handler;
+
+import java.util.Calendar;
+import java.util.Date;
+
+import com.vaadin.v7.ui.components.calendar.CalendarComponentEvents.DateClickEvent;
+import com.vaadin.v7.ui.components.calendar.CalendarComponentEvents.DateClickHandler;
+
+/**
+ * Implements basic functionality needed to switch to day view when a single day
+ * is clicked.
+ *
+ * @since 7.1
+ * @author Vaadin Ltd.
+ */
+@SuppressWarnings("serial")
+public class BasicDateClickHandler implements DateClickHandler {
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.addon.calendar.ui.CalendarComponentEvents.DateClickHandler
+ * #dateClick
+ * (com.vaadin.addon.calendar.ui.CalendarComponentEvents.DateClickEvent)
+ */
+ @Override
+ public void dateClick(DateClickEvent event) {
+ Date clickedDate = event.getDate();
+
+ Calendar javaCalendar = event.getComponent().getInternalCalendar();
+ javaCalendar.setTime(clickedDate);
+
+ // as times are expanded, this is all that is needed to show one day
+ Date start = javaCalendar.getTime();
+ Date end = javaCalendar.getTime();
+
+ setDates(event, start, end);
+ }
+
+ /**
+ * Set the start and end dates for the event
+ *
+ * @param event
+ * The event that the start and end dates should be set
+ * @param start
+ * The start date
+ * @param end
+ * The end date
+ */
+ protected void setDates(DateClickEvent event, Date start, Date end) {
+ event.getComponent().setStartDate(start);
+ event.getComponent().setEndDate(end);
+ }
+}
diff --git a/compatibility-server/src/main/java/com/vaadin/v7/ui/components/calendar/handler/BasicEventMoveHandler.java b/compatibility-server/src/main/java/com/vaadin/v7/ui/components/calendar/handler/BasicEventMoveHandler.java
new file mode 100644
index 0000000000..be27b606fe
--- /dev/null
+++ b/compatibility-server/src/main/java/com/vaadin/v7/ui/components/calendar/handler/BasicEventMoveHandler.java
@@ -0,0 +1,74 @@
+/*
+ * 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.v7.ui.components.calendar.handler;
+
+import java.util.Date;
+
+import com.vaadin.v7.ui.components.calendar.CalendarComponentEvents.EventMoveHandler;
+import com.vaadin.v7.ui.components.calendar.CalendarComponentEvents.MoveEvent;
+import com.vaadin.v7.ui.components.calendar.event.CalendarEvent;
+import com.vaadin.v7.ui.components.calendar.event.EditableCalendarEvent;
+
+/**
+ * Implements basic functionality needed to enable moving events.
+ *
+ * @since 7.1
+ * @author Vaadin Ltd.
+ */
+@SuppressWarnings("serial")
+public class BasicEventMoveHandler implements EventMoveHandler {
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.addon.calendar.ui.CalendarComponentEvents.EventMoveHandler
+ * #eventMove
+ * (com.vaadin.addon.calendar.ui.CalendarComponentEvents.MoveEvent)
+ */
+ @Override
+ public void eventMove(MoveEvent event) {
+ CalendarEvent calendarEvent = event.getCalendarEvent();
+
+ if (calendarEvent instanceof EditableCalendarEvent) {
+
+ EditableCalendarEvent editableEvent = (EditableCalendarEvent) calendarEvent;
+
+ Date newFromTime = event.getNewStart();
+
+ // Update event dates
+ long length = editableEvent.getEnd().getTime()
+ - editableEvent.getStart().getTime();
+ setDates(editableEvent, newFromTime,
+ new Date(newFromTime.getTime() + length));
+ }
+ }
+
+ /**
+ * Set the start and end dates for the event
+ *
+ * @param event
+ * The event that the start and end dates should be set
+ * @param start
+ * The start date
+ * @param end
+ * The end date
+ */
+ protected void setDates(EditableCalendarEvent event, Date start, Date end) {
+ event.setStart(start);
+ event.setEnd(end);
+ }
+}
diff --git a/compatibility-server/src/main/java/com/vaadin/v7/ui/components/calendar/handler/BasicEventResizeHandler.java b/compatibility-server/src/main/java/com/vaadin/v7/ui/components/calendar/handler/BasicEventResizeHandler.java
new file mode 100644
index 0000000000..9b6f08ff09
--- /dev/null
+++ b/compatibility-server/src/main/java/com/vaadin/v7/ui/components/calendar/handler/BasicEventResizeHandler.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.v7.ui.components.calendar.handler;
+
+import java.util.Date;
+
+import com.vaadin.v7.ui.components.calendar.CalendarComponentEvents.EventResize;
+import com.vaadin.v7.ui.components.calendar.CalendarComponentEvents.EventResizeHandler;
+import com.vaadin.v7.ui.components.calendar.event.CalendarEvent;
+import com.vaadin.v7.ui.components.calendar.event.EditableCalendarEvent;
+
+/**
+ * Implements basic functionality needed to enable event resizing.
+ *
+ * @since 7.1
+ * @author Vaadin Ltd.
+ */
+@SuppressWarnings("serial")
+public class BasicEventResizeHandler implements EventResizeHandler {
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.addon.calendar.ui.CalendarComponentEvents.EventResizeHandler
+ * #eventResize
+ * (com.vaadin.addon.calendar.ui.CalendarComponentEvents.EventResize)
+ */
+ @Override
+ public void eventResize(EventResize event) {
+ CalendarEvent calendarEvent = event.getCalendarEvent();
+
+ if (calendarEvent instanceof EditableCalendarEvent) {
+ Date newStartTime = event.getNewStart();
+ Date newEndTime = event.getNewEnd();
+
+ EditableCalendarEvent editableEvent = (EditableCalendarEvent) calendarEvent;
+
+ setDates(editableEvent, newStartTime, newEndTime);
+ }
+ }
+
+ /**
+ * Set the start and end dates for the event
+ *
+ * @param event
+ * The event that the start and end dates should be set
+ * @param start
+ * The start date
+ * @param end
+ * The end date
+ */
+ protected void setDates(EditableCalendarEvent event, Date start, Date end) {
+ event.setStart(start);
+ event.setEnd(end);
+ }
+}
diff --git a/compatibility-server/src/main/java/com/vaadin/v7/ui/components/calendar/handler/BasicForwardHandler.java b/compatibility-server/src/main/java/com/vaadin/v7/ui/components/calendar/handler/BasicForwardHandler.java
new file mode 100644
index 0000000000..d71958536e
--- /dev/null
+++ b/compatibility-server/src/main/java/com/vaadin/v7/ui/components/calendar/handler/BasicForwardHandler.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.v7.ui.components.calendar.handler;
+
+import java.util.Calendar;
+import java.util.Date;
+
+import com.vaadin.shared.ui.calendar.DateConstants;
+import com.vaadin.v7.ui.components.calendar.CalendarComponentEvents.ForwardEvent;
+import com.vaadin.v7.ui.components.calendar.CalendarComponentEvents.ForwardHandler;
+
+/**
+ * Implements basic functionality needed to enable forward navigation.
+ *
+ * @since 7.1
+ * @author Vaadin Ltd.
+ */
+@SuppressWarnings("serial")
+public class BasicForwardHandler implements ForwardHandler {
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.addon.calendar.ui.CalendarComponentEvents.ForwardHandler#
+ * forward
+ * (com.vaadin.addon.calendar.ui.CalendarComponentEvents.ForwardEvent)
+ */
+ @Override
+ public void forward(ForwardEvent event) {
+ Date start = event.getComponent().getStartDate();
+ Date end = event.getComponent().getEndDate();
+
+ // calculate amount to move forward
+ int durationInDays = (int) (((end.getTime()) - start.getTime())
+ / DateConstants.DAYINMILLIS);
+ // for week view durationInDays = 7, for day view durationInDays = 1
+ durationInDays++;
+
+ // set new start and end times
+ Calendar javaCalendar = Calendar.getInstance();
+ javaCalendar.setTime(start);
+ javaCalendar.add(java.util.Calendar.DATE, durationInDays);
+ Date newStart = javaCalendar.getTime();
+
+ javaCalendar.setTime(end);
+ javaCalendar.add(java.util.Calendar.DATE, durationInDays);
+ Date newEnd = javaCalendar.getTime();
+
+ if (start.equals(end)) { // day view
+ int firstDay = event.getComponent().getFirstVisibleDayOfWeek();
+ int lastDay = event.getComponent().getLastVisibleDayOfWeek();
+ int dayOfWeek = javaCalendar.get(Calendar.DAY_OF_WEEK);
+
+ // we suppose that 7 >= lastDay >= firstDay >= 1
+ while (!(firstDay <= dayOfWeek && dayOfWeek <= lastDay)) {
+ javaCalendar.add(java.util.Calendar.DATE, 1);
+ dayOfWeek = javaCalendar.get(Calendar.DAY_OF_WEEK);
+ }
+
+ newStart = javaCalendar.getTime();
+ newEnd = javaCalendar.getTime();
+ }
+
+ setDates(event, newStart, newEnd);
+ }
+
+ /**
+ * Set the start and end dates for the event
+ *
+ * @param event
+ * The event that the start and end dates should be set
+ * @param start
+ * The start date
+ * @param end
+ * The end date
+ */
+ protected void setDates(ForwardEvent event, Date start, Date end) {
+ event.getComponent().setStartDate(start);
+ event.getComponent().setEndDate(end);
+ }
+}
diff --git a/compatibility-server/src/main/java/com/vaadin/v7/ui/components/calendar/handler/BasicWeekClickHandler.java b/compatibility-server/src/main/java/com/vaadin/v7/ui/components/calendar/handler/BasicWeekClickHandler.java
new file mode 100644
index 0000000000..420d0a76f6
--- /dev/null
+++ b/compatibility-server/src/main/java/com/vaadin/v7/ui/components/calendar/handler/BasicWeekClickHandler.java
@@ -0,0 +1,82 @@
+/*
+ * 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.v7.ui.components.calendar.handler;
+
+import java.util.Calendar;
+import java.util.Date;
+import java.util.GregorianCalendar;
+
+import com.vaadin.v7.ui.components.calendar.CalendarComponentEvents.WeekClick;
+import com.vaadin.v7.ui.components.calendar.CalendarComponentEvents.WeekClickHandler;
+
+/**
+ * Implements basic functionality needed to change to week view when a week
+ * number is clicked.
+ *
+ * @since 7.1
+ * @author Vaadin Ltd.
+ */
+@SuppressWarnings("serial")
+public class BasicWeekClickHandler implements WeekClickHandler {
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.addon.calendar.ui.CalendarComponentEvents.WeekClickHandler
+ * #weekClick
+ * (com.vaadin.addon.calendar.ui.CalendarComponentEvents.WeekClick)
+ */
+ @Override
+ public void weekClick(WeekClick event) {
+ int week = event.getWeek();
+ int year = event.getYear();
+
+ // set correct year and month
+ Calendar javaCalendar = event.getComponent().getInternalCalendar();
+ javaCalendar.set(GregorianCalendar.YEAR, year);
+ javaCalendar.set(GregorianCalendar.WEEK_OF_YEAR, week);
+
+ // starting at the beginning of the week
+ javaCalendar.set(GregorianCalendar.DAY_OF_WEEK,
+ javaCalendar.getFirstDayOfWeek());
+ Date start = javaCalendar.getTime();
+
+ // ending at the end of the week
+ javaCalendar.add(GregorianCalendar.DATE, 6);
+ Date end = javaCalendar.getTime();
+
+ setDates(event, start, end);
+
+ // times are automatically expanded, no need to worry about them
+ }
+
+ /**
+ * Set the start and end dates for the event
+ *
+ * @param event
+ * The event that the start and end dates should be set
+ * @param start
+ * The start date
+ * @param end
+ * The end date
+ */
+ protected void setDates(WeekClick event, Date start, Date end) {
+ event.getComponent().setStartDate(start);
+ event.getComponent().setEndDate(end);
+ }
+
+}
diff --git a/compatibility-server/src/main/java/com/vaadin/v7/ui/components/colorpicker/ColorChangeEvent.java b/compatibility-server/src/main/java/com/vaadin/v7/ui/components/colorpicker/ColorChangeEvent.java
new file mode 100644
index 0000000000..da0b435ddc
--- /dev/null
+++ b/compatibility-server/src/main/java/com/vaadin/v7/ui/components/colorpicker/ColorChangeEvent.java
@@ -0,0 +1,43 @@
+/*
+ * 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.v7.ui.components.colorpicker;
+
+import com.vaadin.shared.ui.colorpicker.Color;
+import com.vaadin.ui.Component;
+import com.vaadin.ui.Component.Event;
+
+/**
+ * The color changed event which is passed to the listeners when a color change
+ * occurs.
+ *
+ * @since 7.0.0
+ */
+public class ColorChangeEvent extends Event {
+ private final Color color;
+
+ public ColorChangeEvent(Component source, Color color) {
+ super(source);
+
+ this.color = color;
+ }
+
+ /**
+ * Returns the new color.
+ */
+ public Color getColor() {
+ return color;
+ }
+}
diff --git a/compatibility-server/src/main/java/com/vaadin/v7/ui/components/colorpicker/ColorChangeListener.java b/compatibility-server/src/main/java/com/vaadin/v7/ui/components/colorpicker/ColorChangeListener.java
new file mode 100644
index 0000000000..87f0046242
--- /dev/null
+++ b/compatibility-server/src/main/java/com/vaadin/v7/ui/components/colorpicker/ColorChangeListener.java
@@ -0,0 +1,42 @@
+/*
+ * 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.v7.ui.components.colorpicker;
+
+import java.io.Serializable;
+
+/**
+ * The listener interface for receiving colorChange events. The class that is
+ * interested in processing a {@link ColorChangeEvent} implements this
+ * interface, and the object created with that class is registered with a
+ * component using the component's <code>addColorChangeListener</code> method.
+ * When the colorChange event occurs, that object's appropriate method is
+ * invoked.
+ *
+ * @since 7.0.0
+ *
+ * @see ColorChangeEvent
+ */
+public interface ColorChangeListener extends Serializable {
+
+ /**
+ * Called when a new color has been selected.
+ *
+ * @param event
+ * An event containing information about the color change.
+ */
+ void colorChanged(ColorChangeEvent event);
+
+}
diff --git a/compatibility-server/src/main/java/com/vaadin/v7/ui/components/colorpicker/ColorPickerGradient.java b/compatibility-server/src/main/java/com/vaadin/v7/ui/components/colorpicker/ColorPickerGradient.java
new file mode 100644
index 0000000000..cb9dd698d9
--- /dev/null
+++ b/compatibility-server/src/main/java/com/vaadin/v7/ui/components/colorpicker/ColorPickerGradient.java
@@ -0,0 +1,144 @@
+/*
+ * 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.v7.ui.components.colorpicker;
+
+import java.lang.reflect.Method;
+
+import com.vaadin.shared.ui.colorpicker.Color;
+import com.vaadin.shared.ui.colorpicker.ColorPickerGradientServerRpc;
+import com.vaadin.shared.ui.colorpicker.ColorPickerGradientState;
+import com.vaadin.ui.AbstractComponent;
+import com.vaadin.v7.ui.AbstractColorPicker.Coordinates2Color;
+
+/**
+ * A component that represents a color gradient within a color picker.
+ *
+ * @since 7.0.0
+ */
+public class ColorPickerGradient extends AbstractComponent
+ implements ColorSelector {
+
+ private static final Method COLOR_CHANGE_METHOD;
+ static {
+ try {
+ COLOR_CHANGE_METHOD = ColorChangeListener.class.getDeclaredMethod(
+ "colorChanged", new Class[] { ColorChangeEvent.class });
+ } catch (final java.lang.NoSuchMethodException e) {
+ // This should never happen
+ throw new java.lang.RuntimeException(
+ "Internal error finding methods in ColorPicker");
+ }
+ }
+
+ private ColorPickerGradientServerRpc rpc = new ColorPickerGradientServerRpc() {
+
+ @Override
+ public void select(int cursorX, int cursorY) {
+ x = cursorX;
+ y = cursorY;
+ color = converter.calculate(x, y);
+
+ fireColorChanged(color);
+ }
+ };
+
+ /** The converter. */
+ private Coordinates2Color converter;
+
+ /** The foreground color. */
+ private Color color;
+
+ /** The x-coordinate. */
+ private int x = 0;
+
+ /** The y-coordinate. */
+ private int y = 0;
+
+ private ColorPickerGradient() {
+ registerRpc(rpc);
+ // width and height must be set here instead of in theme, otherwise
+ // coordinate calculations fail
+ getState().width = "220px";
+ getState().height = "220px";
+ }
+
+ /**
+ * Instantiates a new color picker gradient.
+ *
+ * @param id
+ * the id
+ * @param converter
+ * the converter
+ */
+ public ColorPickerGradient(String id, Coordinates2Color converter) {
+ this();
+ addStyleName(id);
+ this.converter = converter;
+ }
+
+ @Override
+ public void setColor(Color c) {
+ color = c;
+
+ int[] coords = converter.calculate(c);
+ x = coords[0];
+ y = coords[1];
+
+ getState().cursorX = x;
+ getState().cursorY = y;
+
+ }
+
+ @Override
+ public void addColorChangeListener(ColorChangeListener listener) {
+ addListener(ColorChangeEvent.class, listener, COLOR_CHANGE_METHOD);
+ }
+
+ @Override
+ public void removeColorChangeListener(ColorChangeListener listener) {
+ removeListener(ColorChangeEvent.class, listener);
+ }
+
+ /**
+ * Sets the background color.
+ *
+ * @param color
+ * the new background color
+ */
+ public void setBackgroundColor(Color color) {
+ getState().bgColor = color.getCSS();
+ }
+
+ @Override
+ public Color getColor() {
+ return color;
+ }
+
+ /**
+ * Notifies the listeners that the color has changed
+ *
+ * @param color
+ * The color which it changed to
+ */
+ public void fireColorChanged(Color color) {
+ fireEvent(new ColorChangeEvent(this, color));
+ }
+
+ @Override
+ protected ColorPickerGradientState getState() {
+ return (ColorPickerGradientState) super.getState();
+ }
+}
diff --git a/compatibility-server/src/main/java/com/vaadin/v7/ui/components/colorpicker/ColorPickerGrid.java b/compatibility-server/src/main/java/com/vaadin/v7/ui/components/colorpicker/ColorPickerGrid.java
new file mode 100644
index 0000000000..a1286dbc58
--- /dev/null
+++ b/compatibility-server/src/main/java/com/vaadin/v7/ui/components/colorpicker/ColorPickerGrid.java
@@ -0,0 +1,258 @@
+/*
+ * 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.v7.ui.components.colorpicker;
+
+import java.awt.Point;
+import java.lang.reflect.Method;
+import java.util.HashMap;
+import java.util.Map;
+
+import com.vaadin.shared.ui.colorpicker.Color;
+import com.vaadin.shared.ui.colorpicker.ColorPickerGridServerRpc;
+import com.vaadin.shared.ui.colorpicker.ColorPickerGridState;
+import com.vaadin.ui.AbstractComponent;
+
+/**
+ * A component that represents a color selection grid within a color picker.
+ *
+ * @since 7.0.0
+ */
+public class ColorPickerGrid extends AbstractComponent
+ implements ColorSelector {
+
+ private static final String STYLENAME = "v-colorpicker-grid";
+
+ private static final Method COLOR_CHANGE_METHOD;
+ static {
+ try {
+ COLOR_CHANGE_METHOD = ColorChangeListener.class.getDeclaredMethod(
+ "colorChanged", new Class[] { ColorChangeEvent.class });
+ } catch (final java.lang.NoSuchMethodException e) {
+ // This should never happen
+ throw new java.lang.RuntimeException(
+ "Internal error finding methods in ColorPicker");
+ }
+ }
+
+ private ColorPickerGridServerRpc rpc = new ColorPickerGridServerRpc() {
+
+ @Override
+ public void select(int x, int y) {
+ ColorPickerGrid.this.x = x;
+ ColorPickerGrid.this.y = y;
+
+ fireColorChanged(colorGrid[y][x]);
+ }
+
+ @Override
+ public void refresh() {
+ for (int row = 0; row < rows; row++) {
+ for (int col = 0; col < columns; col++) {
+ changedColors.put(new Point(row, col), colorGrid[row][col]);
+ }
+ }
+ sendChangedColors();
+ markAsDirty();
+ }
+ };
+
+ /** The x-coordinate. */
+ private int x = 0;
+
+ /** The y-coordinate. */
+ private int y = 0;
+
+ /** The rows. */
+ private int rows;
+
+ /** The columns. */
+ private int columns;
+
+ /** The color grid. */
+ private Color[][] colorGrid = new Color[1][1];
+
+ /** The changed colors. */
+ private final Map<Point, Color> changedColors = new HashMap<Point, Color>();
+
+ /**
+ * Instantiates a new color picker grid.
+ */
+ public ColorPickerGrid() {
+ registerRpc(rpc);
+ setPrimaryStyleName(STYLENAME);
+ setColorGrid(new Color[1][1]);
+ setColor(Color.WHITE);
+ }
+
+ /**
+ * Instantiates a new color picker grid.
+ *
+ * @param rows
+ * the rows
+ * @param columns
+ * the columns
+ */
+ public ColorPickerGrid(int rows, int columns) {
+ registerRpc(rpc);
+ setPrimaryStyleName(STYLENAME);
+ setColorGrid(new Color[rows][columns]);
+ setColor(Color.WHITE);
+ }
+
+ /**
+ * Instantiates a new color picker grid.
+ *
+ * @param colors
+ * the colors
+ */
+ public ColorPickerGrid(Color[][] colors) {
+ registerRpc(rpc);
+ setPrimaryStyleName(STYLENAME);
+ setColorGrid(colors);
+ }
+
+ private void setColumnCount(int columns) {
+ this.columns = columns;
+ getState().columnCount = columns;
+ }
+
+ private void setRowCount(int rows) {
+ this.rows = rows;
+ getState().rowCount = rows;
+ }
+
+ private void sendChangedColors() {
+ if (!changedColors.isEmpty()) {
+ String[] colors = new String[changedColors.size()];
+ String[] XCoords = new String[changedColors.size()];
+ String[] YCoords = new String[changedColors.size()];
+ int counter = 0;
+ for (Point p : changedColors.keySet()) {
+ Color c = changedColors.get(p);
+ if (c == null) {
+ continue;
+ }
+
+ String color = c.getCSS();
+
+ colors[counter] = color;
+ XCoords[counter] = String.valueOf((int) p.getX());
+ YCoords[counter] = String.valueOf((int) p.getY());
+ counter++;
+ }
+ getState().changedColor = colors;
+ getState().changedX = XCoords;
+ getState().changedY = YCoords;
+
+ changedColors.clear();
+ }
+ }
+
+ /**
+ * Sets the color grid.
+ *
+ * @param colors
+ * the new color grid
+ */
+ public void setColorGrid(Color[][] colors) {
+ setRowCount(colors.length);
+ setColumnCount(colors[0].length);
+ colorGrid = colors;
+
+ for (int row = 0; row < rows; row++) {
+ for (int col = 0; col < columns; col++) {
+ changedColors.put(new Point(row, col), colorGrid[row][col]);
+ }
+ }
+ sendChangedColors();
+
+ markAsDirty();
+ }
+
+ /**
+ * Adds a color change listener
+ *
+ * @param listener
+ * The color change listener
+ */
+ @Override
+ public void addColorChangeListener(ColorChangeListener listener) {
+ addListener(ColorChangeEvent.class, listener, COLOR_CHANGE_METHOD);
+ }
+
+ @Override
+ public Color getColor() {
+ return colorGrid[x][y];
+ }
+
+ /**
+ * Removes a color change listener
+ *
+ * @param listener
+ * The listener
+ */
+ @Override
+ public void removeColorChangeListener(ColorChangeListener listener) {
+ removeListener(ColorChangeEvent.class, listener);
+ }
+
+ @Override
+ public void setColor(Color color) {
+ colorGrid[x][y] = color;
+ changedColors.put(new Point(x, y), color);
+ sendChangedColors();
+ markAsDirty();
+ }
+
+ /**
+ * Sets the position.
+ *
+ * @param x
+ * the x
+ * @param y
+ * the y
+ */
+ public void setPosition(int x, int y) {
+ if (x >= 0 && x < columns && y >= 0 && y < rows) {
+ this.x = x;
+ this.y = y;
+ }
+ }
+
+ /**
+ * Gets the position.
+ *
+ * @return the position
+ */
+ public int[] getPosition() {
+ return new int[] { x, y };
+ }
+
+ /**
+ * Notifies the listeners that a color change has occurred
+ *
+ * @param color
+ * The color which it changed to
+ */
+ public void fireColorChanged(Color color) {
+ fireEvent(new ColorChangeEvent(this, color));
+ }
+
+ @Override
+ protected ColorPickerGridState getState() {
+ return (ColorPickerGridState) super.getState();
+ }
+}
diff --git a/compatibility-server/src/main/java/com/vaadin/v7/ui/components/colorpicker/ColorPickerHistory.java b/compatibility-server/src/main/java/com/vaadin/v7/ui/components/colorpicker/ColorPickerHistory.java
new file mode 100644
index 0000000000..e26e802a32
--- /dev/null
+++ b/compatibility-server/src/main/java/com/vaadin/v7/ui/components/colorpicker/ColorPickerHistory.java
@@ -0,0 +1,217 @@
+/*
+ * 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.v7.ui.components.colorpicker;
+
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.concurrent.ArrayBlockingQueue;
+
+import com.vaadin.shared.ui.colorpicker.Color;
+import com.vaadin.ui.CustomComponent;
+
+/**
+ * A component that represents color selection history within a color picker.
+ *
+ * @since 7.0.0
+ */
+public class ColorPickerHistory extends CustomComponent
+ implements ColorSelector, ColorChangeListener {
+
+ private static final String STYLENAME = "v-colorpicker-history";
+
+ private static final Method COLOR_CHANGE_METHOD;
+ static {
+ try {
+ COLOR_CHANGE_METHOD = ColorChangeListener.class.getDeclaredMethod(
+ "colorChanged", new Class[] { ColorChangeEvent.class });
+ } catch (final java.lang.NoSuchMethodException e) {
+ // This should never happen
+ throw new java.lang.RuntimeException(
+ "Internal error finding methods in ColorPicker");
+ }
+ }
+
+ /** The rows. */
+ private static final int rows = 4;
+
+ /** The columns. */
+ private static final int columns = 15;
+
+ /** Temporary color history for when the component is detached. */
+ private ArrayBlockingQueue<Color> tempHistory = new ArrayBlockingQueue<Color>(
+ rows * columns);
+
+ /** The grid. */
+ private final ColorPickerGrid grid;
+
+ /**
+ * Instantiates a new color picker history.
+ */
+ public ColorPickerHistory() {
+ setPrimaryStyleName(STYLENAME);
+
+ grid = new ColorPickerGrid(rows, columns);
+ grid.setWidth("100%");
+ grid.setPosition(0, 0);
+ grid.addColorChangeListener(this);
+
+ setCompositionRoot(grid);
+ }
+
+ @Override
+ public void attach() {
+ super.attach();
+ createColorHistoryIfNecessary();
+ }
+
+ private void createColorHistoryIfNecessary() {
+ List<Color> tempColors = new ArrayList<Color>(tempHistory);
+ if (getSession().getAttribute("colorPickerHistory") == null) {
+ getSession().setAttribute("colorPickerHistory",
+ new ArrayBlockingQueue<Color>(rows * columns));
+ }
+ for (Color color : tempColors) {
+ setColor(color);
+ }
+ tempHistory.clear();
+ }
+
+ @SuppressWarnings("unchecked")
+ private ArrayBlockingQueue<Color> getColorHistory() {
+ if (isAttached()) {
+ Object colorHistory = getSession()
+ .getAttribute("colorPickerHistory");
+ if (colorHistory instanceof ArrayBlockingQueue<?>) {
+ return (ArrayBlockingQueue<Color>) colorHistory;
+ }
+ }
+ return tempHistory;
+ }
+
+ @Override
+ public void setHeight(String height) {
+ super.setHeight(height);
+ grid.setHeight(height);
+ }
+
+ @Override
+ public void setColor(Color color) {
+
+ ArrayBlockingQueue<Color> colorHistory = getColorHistory();
+
+ // Check that the color does not already exist
+ boolean exists = false;
+ Iterator<Color> iter = colorHistory.iterator();
+ while (iter.hasNext()) {
+ if (color.equals(iter.next())) {
+ exists = true;
+ break;
+ }
+ }
+
+ // If the color does not exist then add it
+ if (!exists) {
+ if (!colorHistory.offer(color)) {
+ colorHistory.poll();
+ colorHistory.offer(color);
+ }
+ }
+
+ List<Color> colorList = new ArrayList<Color>(colorHistory);
+
+ // Invert order of colors
+ Collections.reverse(colorList);
+
+ // Move the selected color to the front of the list
+ Collections.swap(colorList, colorList.indexOf(color), 0);
+
+ // Create 2d color map
+ Color[][] colors = new Color[rows][columns];
+ iter = colorList.iterator();
+
+ for (int row = 0; row < rows; row++) {
+ for (int col = 0; col < columns; col++) {
+ if (iter.hasNext()) {
+ colors[row][col] = iter.next();
+ } else {
+ colors[row][col] = Color.WHITE;
+ }
+ }
+ }
+
+ grid.setColorGrid(colors);
+ grid.markAsDirty();
+ }
+
+ @Override
+ public Color getColor() {
+ return getColorHistory().peek();
+ }
+
+ /**
+ * Gets the history.
+ *
+ * @return the history
+ */
+ public List<Color> getHistory() {
+ ArrayBlockingQueue<Color> colorHistory = getColorHistory();
+ Color[] array = colorHistory.toArray(new Color[colorHistory.size()]);
+ return Collections.unmodifiableList(Arrays.asList(array));
+ }
+
+ /**
+ * Checks if the history contains given color.
+ *
+ * @param c
+ * the color
+ *
+ * @return true, if successful
+ */
+ public boolean hasColor(Color c) {
+ return getColorHistory().contains(c);
+ }
+
+ /**
+ * Adds a color change listener
+ *
+ * @param listener
+ * The listener
+ */
+ @Override
+ public void addColorChangeListener(ColorChangeListener listener) {
+ addListener(ColorChangeEvent.class, listener, COLOR_CHANGE_METHOD);
+ }
+
+ /**
+ * Removes a color change listener
+ *
+ * @param listener
+ * The listener
+ */
+ @Override
+ public void removeColorChangeListener(ColorChangeListener listener) {
+ removeListener(ColorChangeEvent.class, listener);
+ }
+
+ @Override
+ public void colorChanged(ColorChangeEvent event) {
+ fireEvent(new ColorChangeEvent(this, event.getColor()));
+ }
+}
diff --git a/compatibility-server/src/main/java/com/vaadin/v7/ui/components/colorpicker/ColorPickerPopup.java b/compatibility-server/src/main/java/com/vaadin/v7/ui/components/colorpicker/ColorPickerPopup.java
new file mode 100644
index 0000000000..39931ce570
--- /dev/null
+++ b/compatibility-server/src/main/java/com/vaadin/v7/ui/components/colorpicker/ColorPickerPopup.java
@@ -0,0 +1,759 @@
+/*
+ * 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.v7.ui.components.colorpicker;
+
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import com.vaadin.shared.ui.MarginInfo;
+import com.vaadin.shared.ui.colorpicker.Color;
+import com.vaadin.ui.Alignment;
+import com.vaadin.ui.Button;
+import com.vaadin.ui.Button.ClickEvent;
+import com.vaadin.ui.Button.ClickListener;
+import com.vaadin.ui.Component;
+import com.vaadin.ui.HorizontalLayout;
+import com.vaadin.ui.Layout;
+import com.vaadin.ui.Slider;
+import com.vaadin.ui.Slider.ValueOutOfBoundsException;
+import com.vaadin.ui.TabSheet;
+import com.vaadin.ui.VerticalLayout;
+import com.vaadin.ui.Window;
+import com.vaadin.v7.ui.AbstractColorPicker.Coordinates2Color;
+
+/**
+ * A component that represents color selection popup within a color picker.
+ *
+ * @since 7.0.0
+ */
+public class ColorPickerPopup extends Window
+ implements ClickListener, ColorChangeListener, ColorSelector {
+
+ private static final String STYLENAME = "v-colorpicker-popup";
+
+ private static final Method COLOR_CHANGE_METHOD;
+ static {
+ try {
+ COLOR_CHANGE_METHOD = ColorChangeListener.class.getDeclaredMethod(
+ "colorChanged", new Class[] { ColorChangeEvent.class });
+ } catch (final java.lang.NoSuchMethodException e) {
+ // This should never happen
+ throw new java.lang.RuntimeException(
+ "Internal error finding methods in ColorPicker");
+ }
+ }
+
+ /** The tabs. */
+ private final TabSheet tabs = new TabSheet();
+
+ private Component rgbTab;
+
+ private Component hsvTab;
+
+ private Component swatchesTab;
+
+ /** The layout. */
+ private final VerticalLayout layout;
+
+ /** The ok button. */
+ private final Button ok = new Button("OK");
+
+ /** The cancel button. */
+ private final Button cancel = new Button("Cancel");
+
+ /** The resize button. */
+ private final Button resize = new Button("show/hide history");
+
+ /** The selected color. */
+ private Color selectedColor = Color.WHITE;
+
+ /** The history. */
+ private ColorPickerHistory history;
+
+ /** The history container. */
+ private Layout historyContainer;
+
+ /** The rgb gradient. */
+ private ColorPickerGradient rgbGradient;
+
+ /** The hsv gradient. */
+ private ColorPickerGradient hsvGradient;
+
+ /** The red slider. */
+ private Slider redSlider;
+
+ /** The green slider. */
+ private Slider greenSlider;
+
+ /** The blue slider. */
+ private Slider blueSlider;
+
+ /** The hue slider. */
+ private Slider hueSlider;
+
+ /** The saturation slider. */
+ private Slider saturationSlider;
+
+ /** The value slider. */
+ private Slider valueSlider;
+
+ /** The preview on the rgb tab. */
+ private ColorPickerPreview rgbPreview;
+
+ /** The preview on the hsv tab. */
+ private ColorPickerPreview hsvPreview;
+
+ /** The preview on the swatches tab. */
+ private ColorPickerPreview selPreview;
+
+ /** The color select. */
+ private ColorPickerSelect colorSelect;
+
+ /** The selectors. */
+ private final Set<ColorSelector> selectors = new HashSet<ColorSelector>();
+
+ /**
+ * Set true while the slider values are updated after colorChange. When
+ * true, valueChange reactions from the sliders are disabled, because
+ * otherwise the set color may become corrupted as it is repeatedly re-set
+ * in valueChangeListeners using values from sliders that may not have been
+ * updated yet.
+ */
+ private boolean updatingColors = false;
+
+ private ColorPickerPopup() {
+ // Set the layout
+ layout = new VerticalLayout();
+ layout.setSpacing(false);
+ layout.setMargin(false);
+ layout.setWidth("100%");
+ layout.setHeight(null);
+
+ setContent(layout);
+ setStyleName(STYLENAME);
+ setResizable(false);
+ setImmediate(true);
+ // Create the history
+ history = new ColorPickerHistory();
+ history.addColorChangeListener(this);
+ }
+
+ /**
+ * Instantiates a new color picker popup.
+ */
+ public ColorPickerPopup(Color initialColor) {
+ this();
+ selectedColor = initialColor;
+ initContents();
+ }
+
+ private void initContents() {
+ // Create the preview on the rgb tab
+ rgbPreview = new ColorPickerPreview(selectedColor);
+ rgbPreview.setWidth("240px");
+ rgbPreview.setHeight("20px");
+ rgbPreview.addColorChangeListener(this);
+ selectors.add(rgbPreview);
+
+ // Create the preview on the hsv tab
+ hsvPreview = new ColorPickerPreview(selectedColor);
+ hsvPreview.setWidth("240px");
+ hsvPreview.setHeight("20px");
+ hsvPreview.addColorChangeListener(this);
+ selectors.add(hsvPreview);
+
+ // Create the preview on the swatches tab
+ selPreview = new ColorPickerPreview(selectedColor);
+ selPreview.setWidth("100%");
+ selPreview.setHeight("20px");
+ selPreview.addColorChangeListener(this);
+ selectors.add(selPreview);
+
+ // Create the tabs
+ rgbTab = createRGBTab(selectedColor);
+ tabs.addTab(rgbTab, "RGB", null);
+
+ hsvTab = createHSVTab(selectedColor);
+ tabs.addTab(hsvTab, "HSV", null);
+
+ swatchesTab = createSelectTab();
+ tabs.addTab(swatchesTab, "Swatches", null);
+
+ // Add the tabs
+ tabs.setWidth("100%");
+
+ layout.addComponent(tabs);
+
+ // Add the history
+ history.setWidth("97%");
+ history.setHeight("22px");
+
+ // Create the default colors
+ List<Color> defaultColors = new ArrayList<Color>();
+ defaultColors.add(Color.BLACK);
+ defaultColors.add(Color.WHITE);
+
+ // Create the history
+ VerticalLayout innerContainer = new VerticalLayout();
+ innerContainer.setWidth("100%");
+ innerContainer.setHeight(null);
+ innerContainer.addComponent(history);
+
+ VerticalLayout outerContainer = new VerticalLayout();
+ outerContainer.setWidth("99%");
+ outerContainer.setHeight("27px");
+ outerContainer.addComponent(innerContainer);
+ historyContainer = outerContainer;
+
+ layout.addComponent(historyContainer);
+
+ // Add the resize button for the history
+ resize.addClickListener(this);
+ resize.setData(new Boolean(false));
+ resize.setWidth("100%");
+ resize.setHeight("10px");
+ resize.setPrimaryStyleName("resize-button");
+ layout.addComponent(resize);
+
+ // Add the buttons
+ ok.setWidth("70px");
+ ok.addClickListener(this);
+
+ cancel.setWidth("70px");
+ cancel.addClickListener(this);
+
+ HorizontalLayout buttons = new HorizontalLayout();
+ buttons.addComponent(ok);
+ buttons.addComponent(cancel);
+ buttons.setWidth("100%");
+ buttons.setHeight("30px");
+ buttons.setComponentAlignment(ok, Alignment.MIDDLE_CENTER);
+ buttons.setComponentAlignment(cancel, Alignment.MIDDLE_CENTER);
+ layout.addComponent(buttons);
+ }
+
+ /**
+ * Creates the RGB tab.
+ *
+ * @return the component
+ */
+ private Component createRGBTab(Color color) {
+ VerticalLayout rgbLayout = new VerticalLayout();
+ rgbLayout.setMargin(new MarginInfo(false, false, true, false));
+ rgbLayout.addComponent(rgbPreview);
+ rgbLayout.setStyleName("rgbtab");
+
+ // Add the RGB color gradient
+ rgbGradient = new ColorPickerGradient("rgb-gradient", RGBConverter);
+ rgbGradient.setColor(color);
+ rgbGradient.addColorChangeListener(this);
+ rgbLayout.addComponent(rgbGradient);
+ selectors.add(rgbGradient);
+
+ // Add the RGB sliders
+ VerticalLayout sliders = new VerticalLayout();
+ sliders.setStyleName("rgb-sliders");
+
+ redSlider = createRGBSlider("Red", "red");
+ greenSlider = createRGBSlider("Green", "green");
+ blueSlider = createRGBSlider("Blue", "blue");
+ setRgbSliderValues(color);
+
+ redSlider.addValueChangeListener(e -> {
+ double red = e.getValue();
+ if (!updatingColors) {
+ Color newColor = new Color((int) red, selectedColor.getGreen(),
+ selectedColor.getBlue());
+ setColor(newColor);
+ }
+ });
+
+ sliders.addComponent(redSlider);
+
+ greenSlider.addValueChangeListener(e -> {
+ double green = e.getValue();
+ if (!updatingColors) {
+ Color newColor = new Color(selectedColor.getRed(), (int) green,
+ selectedColor.getBlue());
+ setColor(newColor);
+ }
+ });
+ sliders.addComponent(greenSlider);
+
+ blueSlider.addValueChangeListener(e -> {
+ double blue = e.getValue();
+ if (!updatingColors) {
+ Color newColor = new Color(selectedColor.getRed(),
+ selectedColor.getGreen(), (int) blue);
+ setColor(newColor);
+ }
+ });
+ sliders.addComponent(blueSlider);
+
+ rgbLayout.addComponent(sliders);
+
+ return rgbLayout;
+ }
+
+ private Slider createRGBSlider(String caption, String styleName) {
+ Slider redSlider = new Slider(caption, 0, 255);
+ redSlider.setImmediate(true);
+ redSlider.setStyleName("rgb-slider");
+ redSlider.setWidth("220px");
+ redSlider.addStyleName(styleName);
+ return redSlider;
+ }
+
+ /**
+ * Creates the hsv tab.
+ *
+ * @return the component
+ */
+ private Component createHSVTab(Color color) {
+ VerticalLayout hsvLayout = new VerticalLayout();
+ hsvLayout.setMargin(new MarginInfo(false, false, true, false));
+ hsvLayout.addComponent(hsvPreview);
+ hsvLayout.setStyleName("hsvtab");
+
+ // Add the hsv gradient
+ hsvGradient = new ColorPickerGradient("hsv-gradient", HSVConverter);
+ hsvGradient.setColor(color);
+ hsvGradient.addColorChangeListener(this);
+ hsvLayout.addComponent(hsvGradient);
+ selectors.add(hsvGradient);
+
+ VerticalLayout sliders = new VerticalLayout();
+ sliders.setStyleName("hsv-sliders");
+
+ hueSlider = new Slider("Hue", 0, 360);
+ saturationSlider = new Slider("Saturation", 0, 100);
+ valueSlider = new Slider("Value", 0, 100);
+
+ float[] hsv = color.getHSV();
+ setHsvSliderValues(hsv);
+
+ hueSlider.setStyleName("hsv-slider");
+ hueSlider.addStyleName("hue-slider");
+ hueSlider.setWidth("220px");
+ hueSlider.setImmediate(true);
+ hueSlider.addValueChangeListener(event -> {
+ if (!updatingColors) {
+ float hue = (Float.parseFloat(event.getValue().toString()))
+ / 360f;
+ float saturation = (Float
+ .parseFloat(saturationSlider.getValue().toString()))
+ / 100f;
+ float value = (Float
+ .parseFloat(valueSlider.getValue().toString())) / 100f;
+
+ // Set the color
+ Color newColor = new Color(
+ Color.HSVtoRGB(hue, saturation, value));
+ setColor(newColor);
+
+ /*
+ * Set the background color of the hue gradient. This has to be
+ * done here since in the conversion the base color information
+ * is lost when color is black/white
+ */
+ Color bgColor = new Color(Color.HSVtoRGB(hue, 1f, 1f));
+ hsvGradient.setBackgroundColor(bgColor);
+ }
+ });
+ sliders.addComponent(hueSlider);
+
+ saturationSlider.setStyleName("hsv-slider");
+ saturationSlider.setWidth("220px");
+ saturationSlider.setImmediate(true);
+ saturationSlider.addValueChangeListener(event -> {
+ if (!updatingColors) {
+ float hue = (Float.parseFloat(hueSlider.getValue().toString()))
+ / 360f;
+ float saturation = (Float
+ .parseFloat(event.getValue().toString())) / 100f;
+ float value = (Float
+ .parseFloat(valueSlider.getValue().toString())) / 100f;
+ Color newColor = new Color(
+ Color.HSVtoRGB(hue, saturation, value));
+ setColor(newColor);
+ }
+ });
+ sliders.addComponent(saturationSlider);
+
+ valueSlider.setStyleName("hsv-slider");
+ valueSlider.setWidth("220px");
+ valueSlider.setImmediate(true);
+ valueSlider.addValueChangeListener(event -> {
+ if (!updatingColors) {
+ float hue = (Float.parseFloat(hueSlider.getValue().toString()))
+ / 360f;
+ float saturation = (Float
+ .parseFloat(saturationSlider.getValue().toString()))
+ / 100f;
+ float value = (Float.parseFloat(event.getValue().toString()))
+ / 100f;
+
+ Color newColor = new Color(
+ Color.HSVtoRGB(hue, saturation, value));
+ setColor(newColor);
+ }
+ });
+
+ sliders.addComponent(valueSlider);
+ hsvLayout.addComponent(sliders);
+
+ return hsvLayout;
+ }
+
+ /**
+ * Creates the select tab.
+ *
+ * @return the component
+ */
+ private Component createSelectTab() {
+ VerticalLayout selLayout = new VerticalLayout();
+ selLayout.setMargin(new MarginInfo(false, false, true, false));
+ selLayout.addComponent(selPreview);
+ selLayout.addStyleName("seltab");
+
+ colorSelect = new ColorPickerSelect();
+ colorSelect.addColorChangeListener(this);
+ selLayout.addComponent(colorSelect);
+
+ return selLayout;
+ }
+
+ @Override
+ public void buttonClick(ClickEvent event) {
+ // History resize was clicked
+ if (event.getButton() == resize) {
+ boolean state = (Boolean) resize.getData();
+
+ // minimize
+ if (state) {
+ historyContainer.setHeight("27px");
+ history.setHeight("22px");
+
+ // maximize
+ } else {
+ historyContainer.setHeight("90px");
+ history.setHeight("85px");
+ }
+
+ resize.setData(new Boolean(!state));
+ }
+
+ // Ok button was clicked
+ else if (event.getButton() == ok) {
+ history.setColor(getColor());
+ fireColorChanged();
+ close();
+ }
+
+ // Cancel button was clicked
+ else if (event.getButton() == cancel) {
+ close();
+ }
+
+ }
+
+ /**
+ * Notifies the listeners that the color changed
+ */
+ public void fireColorChanged() {
+ fireEvent(new ColorChangeEvent(this, getColor()));
+ }
+
+ /**
+ * Gets the history.
+ *
+ * @return the history
+ */
+ public ColorPickerHistory getHistory() {
+ return history;
+ }
+
+ @Override
+ public void setColor(Color color) {
+ if (color == null) {
+ return;
+ }
+
+ selectedColor = color;
+
+ hsvGradient.setColor(selectedColor);
+ hsvPreview.setColor(selectedColor);
+
+ rgbGradient.setColor(selectedColor);
+ rgbPreview.setColor(selectedColor);
+
+ selPreview.setColor(selectedColor);
+ }
+
+ @Override
+ public Color getColor() {
+ return selectedColor;
+ }
+
+ /**
+ * Gets the color history.
+ *
+ * @return the color history
+ */
+ public List<Color> getColorHistory() {
+ return Collections.unmodifiableList(history.getHistory());
+ }
+
+ @Override
+ public void colorChanged(ColorChangeEvent event) {
+ setColor(event.getColor());
+
+ updatingColors = true;
+
+ setRgbSliderValues(selectedColor);
+ float[] hsv = selectedColor.getHSV();
+ setHsvSliderValues(hsv);
+
+ updatingColors = false;
+
+ for (ColorSelector s : selectors) {
+ if (event.getSource() != s && s != this
+ && s.getColor() != selectedColor) {
+ s.setColor(selectedColor);
+ }
+ }
+ }
+
+ private void setRgbSliderValues(Color color) {
+ try {
+ redSlider.setValue(((Integer) color.getRed()).doubleValue());
+ blueSlider.setValue(((Integer) color.getBlue()).doubleValue());
+ greenSlider.setValue(((Integer) color.getGreen()).doubleValue());
+ } catch (ValueOutOfBoundsException e) {
+ getLogger().log(Level.WARNING,
+ "Unable to set RGB color value to " + color.getRed() + ","
+ + color.getGreen() + "," + color.getBlue(),
+ e);
+ }
+ }
+
+ private void setHsvSliderValues(float[] hsv) {
+ try {
+ hueSlider.setValue(((Float) (hsv[0] * 360f)).doubleValue());
+ saturationSlider.setValue(((Float) (hsv[1] * 100f)).doubleValue());
+ valueSlider.setValue(((Float) (hsv[2] * 100f)).doubleValue());
+ } catch (ValueOutOfBoundsException e) {
+ getLogger().log(Level.WARNING, "Unable to set HSV color value to "
+ + hsv[0] + "," + hsv[1] + "," + hsv[2], e);
+ }
+ }
+
+ @Override
+ public void addColorChangeListener(ColorChangeListener listener) {
+ addListener(ColorChangeEvent.class, listener, COLOR_CHANGE_METHOD);
+ }
+
+ @Override
+ public void removeColorChangeListener(ColorChangeListener listener) {
+ removeListener(ColorChangeEvent.class, listener);
+ }
+
+ /**
+ * Checks the visibility of the given tab
+ *
+ * @param tab
+ * The tab to check
+ * @return true if tab is visible, false otherwise
+ */
+ private boolean tabIsVisible(Component tab) {
+ Iterator<Component> tabIterator = tabs.getComponentIterator();
+ while (tabIterator.hasNext()) {
+ if (tabIterator.next() == tab) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * How many tabs are visible
+ *
+ * @return The number of tabs visible
+ */
+ private int tabsNumVisible() {
+ Iterator<Component> tabIterator = tabs.getComponentIterator();
+ int tabCounter = 0;
+ while (tabIterator.hasNext()) {
+ tabIterator.next();
+ tabCounter++;
+ }
+ return tabCounter;
+ }
+
+ /**
+ * Checks if tabs are needed and hides them if not
+ */
+ private void checkIfTabsNeeded() {
+ tabs.hideTabs(tabsNumVisible() == 1);
+ }
+
+ /**
+ * Set RGB tab visibility
+ *
+ * @param visible
+ * The visibility of the RGB tab
+ */
+ public void setRGBTabVisible(boolean visible) {
+ if (visible && !tabIsVisible(rgbTab)) {
+ tabs.addTab(rgbTab, "RGB", null);
+ checkIfTabsNeeded();
+ } else if (!visible && tabIsVisible(rgbTab)) {
+ tabs.removeComponent(rgbTab);
+ checkIfTabsNeeded();
+ }
+ }
+
+ /**
+ * Set HSV tab visibility
+ *
+ * @param visible
+ * The visibility of the HSV tab
+ */
+ public void setHSVTabVisible(boolean visible) {
+ if (visible && !tabIsVisible(hsvTab)) {
+ tabs.addTab(hsvTab, "HSV", null);
+ checkIfTabsNeeded();
+ } else if (!visible && tabIsVisible(hsvTab)) {
+ tabs.removeComponent(hsvTab);
+ checkIfTabsNeeded();
+ }
+ }
+
+ /**
+ * Set Swatches tab visibility
+ *
+ * @param visible
+ * The visibility of the Swatches tab
+ */
+ public void setSwatchesTabVisible(boolean visible) {
+ if (visible && !tabIsVisible(swatchesTab)) {
+ tabs.addTab(swatchesTab, "Swatches", null);
+ checkIfTabsNeeded();
+ } else if (!visible && tabIsVisible(swatchesTab)) {
+ tabs.removeComponent(swatchesTab);
+ checkIfTabsNeeded();
+ }
+ }
+
+ /**
+ * Set the History visibility
+ *
+ * @param visible
+ */
+ public void setHistoryVisible(boolean visible) {
+ historyContainer.setVisible(visible);
+ resize.setVisible(visible);
+ }
+
+ /**
+ * Set the preview visibility
+ *
+ * @param visible
+ */
+ public void setPreviewVisible(boolean visible) {
+ hsvPreview.setVisible(visible);
+ rgbPreview.setVisible(visible);
+ selPreview.setVisible(visible);
+ }
+
+ /** RGB color converter */
+ private Coordinates2Color RGBConverter = new Coordinates2Color() {
+
+ @Override
+ public Color calculate(int x, int y) {
+ float h = (x / 220f);
+ float s = 1f;
+ float v = 1f;
+
+ if (y < 110) {
+ s = y / 110f;
+ } else if (y > 110) {
+ v = 1f - (y - 110f) / 110f;
+ }
+
+ return new Color(Color.HSVtoRGB(h, s, v));
+ }
+
+ @Override
+ public int[] calculate(Color color) {
+
+ float[] hsv = color.getHSV();
+
+ int x = Math.round(hsv[0] * 220f);
+ int y = 0;
+
+ // lower half
+ if (hsv[1] == 1f) {
+ y = Math.round(110f - (hsv[1] + hsv[2]) * 110f);
+ } else {
+ y = Math.round(hsv[1] * 110f);
+ }
+
+ return new int[] { x, y };
+ }
+ };
+
+ /** HSV color converter */
+ Coordinates2Color HSVConverter = new Coordinates2Color() {
+ @Override
+ public int[] calculate(Color color) {
+
+ float[] hsv = color.getHSV();
+
+ // Calculate coordinates
+ int x = Math.round(hsv[2] * 220.0f);
+ int y = Math.round(220 - hsv[1] * 220.0f);
+
+ // Create background color of clean color
+ Color bgColor = new Color(Color.HSVtoRGB(hsv[0], 1f, 1f));
+ hsvGradient.setBackgroundColor(bgColor);
+
+ return new int[] { x, y };
+ }
+
+ @Override
+ public Color calculate(int x, int y) {
+ float saturation = 1f - (y / 220.0f);
+ float value = (x / 220.0f);
+ float hue = Float.parseFloat(hueSlider.getValue().toString())
+ / 360f;
+
+ Color color = new Color(Color.HSVtoRGB(hue, saturation, value));
+ return color;
+ }
+ };
+
+ private static Logger getLogger() {
+ return Logger.getLogger(ColorPickerPopup.class.getName());
+ }
+}
diff --git a/compatibility-server/src/main/java/com/vaadin/v7/ui/components/colorpicker/ColorPickerPreview.java b/compatibility-server/src/main/java/com/vaadin/v7/ui/components/colorpicker/ColorPickerPreview.java
new file mode 100644
index 0000000000..2a5b7c456f
--- /dev/null
+++ b/compatibility-server/src/main/java/com/vaadin/v7/ui/components/colorpicker/ColorPickerPreview.java
@@ -0,0 +1,198 @@
+/*
+ * 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.v7.ui.components.colorpicker;
+
+import java.lang.reflect.Method;
+
+import com.vaadin.shared.ui.colorpicker.Color;
+import com.vaadin.ui.Component;
+import com.vaadin.ui.CssLayout;
+import com.vaadin.v7.data.Property.ValueChangeEvent;
+import com.vaadin.v7.data.Property.ValueChangeListener;
+import com.vaadin.v7.ui.TextField;
+
+/**
+ * A component that represents color selection preview within a color picker.
+ *
+ * @since 7.0.0
+ */
+public class ColorPickerPreview extends CssLayout
+ implements ColorSelector, ValueChangeListener {
+
+ private static final String STYLE_DARK_COLOR = "v-textfield-dark";
+ private static final String STYLE_LIGHT_COLOR = "v-textfield-light";
+
+ private static final Method COLOR_CHANGE_METHOD;
+ static {
+ try {
+ COLOR_CHANGE_METHOD = ColorChangeListener.class.getDeclaredMethod(
+ "colorChanged", new Class[] { ColorChangeEvent.class });
+ } catch (final java.lang.NoSuchMethodException e) {
+ // This should never happen
+ throw new java.lang.RuntimeException(
+ "Internal error finding methods in ColorPicker");
+ }
+ }
+
+ /** The color. */
+ private Color color;
+
+ /** The field. */
+ private final TextField field;
+
+ /** The old value. */
+ private String oldValue;
+
+ private ColorPickerPreview() {
+ setStyleName("v-colorpicker-preview");
+ setImmediate(true);
+ field = new TextField();
+ field.setImmediate(true);
+ field.setSizeFull();
+ field.setStyleName("v-colorpicker-preview-textfield");
+ field.setData(this);
+ field.addValueChangeListener(this);
+ addComponent(field);
+ }
+
+ /**
+ * Instantiates a new color picker preview.
+ */
+ public ColorPickerPreview(Color color) {
+ this();
+ setColor(color);
+ }
+
+ @Override
+ public void setColor(Color color) {
+ this.color = color;
+
+ // Unregister listener
+ field.removeValueChangeListener(this);
+
+ String colorCSS = color.getCSS();
+ field.setValue(colorCSS);
+
+ if (field.isValid()) {
+ oldValue = colorCSS;
+ } else {
+ field.setValue(oldValue);
+ }
+
+ // Re-register listener
+ field.addValueChangeListener(this);
+
+ // Set the text color
+ field.removeStyleName(STYLE_DARK_COLOR);
+ field.removeStyleName(STYLE_LIGHT_COLOR);
+ if (this.color.getRed() + this.color.getGreen()
+ + this.color.getBlue() < 3 * 128) {
+ field.addStyleName(STYLE_DARK_COLOR);
+ } else {
+ field.addStyleName(STYLE_LIGHT_COLOR);
+ }
+
+ markAsDirty();
+ }
+
+ @Override
+ public Color getColor() {
+ return color;
+ }
+
+ @Override
+ public void addColorChangeListener(ColorChangeListener listener) {
+ addListener(ColorChangeEvent.class, listener, COLOR_CHANGE_METHOD);
+ }
+
+ @Override
+ public void removeColorChangeListener(ColorChangeListener listener) {
+ removeListener(ColorChangeEvent.class, listener);
+ }
+
+ @Override
+ public void valueChange(ValueChangeEvent event) {
+ String value = (String) event.getProperty().getValue();
+ try {
+ if (value != null) {
+ /*
+ * Description of supported formats see
+ * http://www.w3schools.com/cssref/css_colors_legal.asp
+ */
+ if (value.length() == 7 && value.startsWith("#")) {
+ // CSS color format (e.g. #000000)
+ int red = Integer.parseInt(value.substring(1, 3), 16);
+ int green = Integer.parseInt(value.substring(3, 5), 16);
+ int blue = Integer.parseInt(value.substring(5, 7), 16);
+ color = new Color(red, green, blue);
+
+ } else if (value.startsWith("rgb")) {
+ // RGB color format rgb/rgba(255,255,255,0.1)
+ String[] colors = value.substring(value.indexOf("(") + 1,
+ value.length() - 1).split(",");
+
+ int red = Integer.parseInt(colors[0]);
+ int green = Integer.parseInt(colors[1]);
+ int blue = Integer.parseInt(colors[2]);
+ if (colors.length > 3) {
+ int alpha = (int) (Double.parseDouble(colors[3])
+ * 255d);
+ color = new Color(red, green, blue, alpha);
+ } else {
+ color = new Color(red, green, blue);
+ }
+
+ } else if (value.startsWith("hsl")) {
+ // HSL color format hsl/hsla(100,50%,50%,1.0)
+ String[] colors = value.substring(value.indexOf("(") + 1,
+ value.length() - 1).split(",");
+
+ int hue = Integer.parseInt(colors[0]);
+ int saturation = Integer
+ .parseInt(colors[1].replace("%", ""));
+ int lightness = Integer
+ .parseInt(colors[2].replace("%", ""));
+ int rgb = Color.HSLtoRGB(hue, saturation, lightness);
+
+ if (colors.length > 3) {
+ int alpha = (int) (Double.parseDouble(colors[3])
+ * 255d);
+ color = new Color(rgb);
+ color.setAlpha(alpha);
+ } else {
+ color = new Color(rgb);
+ }
+ }
+
+ oldValue = value;
+ fireEvent(new ColorChangeEvent((Component) field.getData(),
+ color));
+ }
+
+ } catch (NumberFormatException nfe) {
+ // Revert value
+ field.setValue(oldValue);
+ }
+ }
+
+ /**
+ * Called when the component is refreshing
+ */
+ @Override
+ protected String getCss(Component c) {
+ return "background: " + color.getCSS();
+ }
+}
diff --git a/compatibility-server/src/main/java/com/vaadin/v7/ui/components/colorpicker/ColorPickerSelect.java b/compatibility-server/src/main/java/com/vaadin/v7/ui/components/colorpicker/ColorPickerSelect.java
new file mode 100644
index 0000000000..87156b34db
--- /dev/null
+++ b/compatibility-server/src/main/java/com/vaadin/v7/ui/components/colorpicker/ColorPickerSelect.java
@@ -0,0 +1,235 @@
+/*
+ * 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.v7.ui.components.colorpicker;
+
+import com.vaadin.shared.ui.colorpicker.Color;
+import com.vaadin.ui.CustomComponent;
+import com.vaadin.ui.VerticalLayout;
+import com.vaadin.v7.data.Property.ValueChangeEvent;
+import com.vaadin.v7.data.Property.ValueChangeListener;
+import com.vaadin.v7.ui.ComboBox;
+
+/**
+ * A component that represents color selection swatches within a color picker.
+ *
+ * @since 7.0.0
+ */
+public class ColorPickerSelect extends CustomComponent
+ implements ColorSelector, ValueChangeListener {
+
+ /** The range. */
+ private final ComboBox range;
+
+ /** The grid. */
+ private final ColorPickerGrid grid;
+
+ /**
+ * The Enum ColorRangePropertyId.
+ */
+ private enum ColorRangePropertyId {
+ ALL("All colors"), RED("Red colors"), GREEN("Green colors"), BLUE(
+ "Blue colors");
+
+ /** The caption. */
+ private String caption;
+
+ /**
+ * Instantiates a new color range property id.
+ *
+ * @param caption
+ * the caption
+ */
+ ColorRangePropertyId(String caption) {
+ this.caption = caption;
+ }
+
+ @Override
+ public String toString() {
+ return caption;
+ }
+ }
+
+ /**
+ * Instantiates a new color picker select.
+ *
+ * @param rows
+ * the rows
+ * @param columns
+ * the columns
+ */
+ public ColorPickerSelect() {
+
+ VerticalLayout layout = new VerticalLayout();
+ setCompositionRoot(layout);
+
+ setStyleName("colorselect");
+ setWidth("100%");
+
+ range = new ComboBox();
+ range.setImmediate(true);
+ range.setImmediate(true);
+ range.setNullSelectionAllowed(false);
+ range.setNewItemsAllowed(false);
+ range.setWidth("100%");
+ range.addValueChangeListener(this);
+
+ for (ColorRangePropertyId id : ColorRangePropertyId.values()) {
+ range.addItem(id);
+ }
+ range.select(ColorRangePropertyId.ALL);
+
+ layout.addComponent(range);
+
+ grid = new ColorPickerGrid(createAllColors(14, 10));
+ grid.setWidth("100%");
+
+ layout.addComponent(grid);
+ }
+
+ /**
+ * Creates the all colors.
+ *
+ * @param rows
+ * the rows
+ * @param columns
+ * the columns
+ *
+ * @return the color[][]
+ */
+ private Color[][] createAllColors(int rows, int columns) {
+ Color[][] colors = new Color[rows][columns];
+
+ for (int row = 0; row < rows; row++) {
+ for (int col = 0; col < columns; col++) {
+
+ // Create the color grid by varying the saturation and value
+ if (row < (rows - 1)) {
+ // Calculate new hue value
+ float hue = ((float) col / (float) columns);
+ float saturation = 1f;
+ float value = 1f;
+
+ // For the upper half use value=1 and variable
+ // saturation
+ if (row < (rows / 2)) {
+ saturation = ((row + 1f) / (rows / 2f));
+ } else {
+ value = 1f - ((row - (rows / 2f)) / (rows / 2f));
+ }
+
+ colors[row][col] = new Color(
+ Color.HSVtoRGB(hue, saturation, value));
+ }
+
+ // The last row should have the black&white gradient
+ else {
+ float hue = 0f;
+ float saturation = 0f;
+ float value = 1f - ((float) col / (float) columns);
+
+ colors[row][col] = new Color(
+ Color.HSVtoRGB(hue, saturation, value));
+ }
+ }
+ }
+
+ return colors;
+ }
+
+ /**
+ * Creates the color.
+ *
+ * @param color
+ * the color
+ * @param rows
+ * the rows
+ * @param columns
+ * the columns
+ *
+ * @return the color[][]
+ */
+ private Color[][] createColors(Color color, int rows, int columns) {
+ Color[][] colors = new Color[rows][columns];
+
+ float[] hsv = color.getHSV();
+
+ float hue = hsv[0];
+ float saturation = 1f;
+ float value = 1f;
+
+ for (int row = 0; row < rows; row++) {
+ for (int col = 0; col < columns; col++) {
+
+ int index = row * columns + col;
+ saturation = 1f;
+ value = 1f;
+
+ if (index <= ((rows * columns) / 2)) {
+ saturation = index
+ / (((float) rows * (float) columns) / 2f);
+ } else {
+ index -= ((rows * columns) / 2);
+ value = 1f
+ - index / (((float) rows * (float) columns) / 2f);
+ }
+
+ colors[row][col] = new Color(
+ Color.HSVtoRGB(hue, saturation, value));
+ }
+ }
+
+ return colors;
+ }
+
+ @Override
+ public Color getColor() {
+ return grid.getColor();
+ }
+
+ @Override
+ public void setColor(Color color) {
+ grid.getColor();
+ }
+
+ @Override
+ public void addColorChangeListener(ColorChangeListener listener) {
+ grid.addColorChangeListener(listener);
+ }
+
+ @Override
+ public void removeColorChangeListener(ColorChangeListener listener) {
+ grid.removeColorChangeListener(listener);
+ }
+
+ @Override
+ public void valueChange(ValueChangeEvent event) {
+ if (grid == null) {
+ return;
+ }
+
+ if (event.getProperty().getValue() == ColorRangePropertyId.ALL) {
+ grid.setColorGrid(createAllColors(14, 10));
+ } else if (event.getProperty().getValue() == ColorRangePropertyId.RED) {
+ grid.setColorGrid(createColors(new Color(0xFF, 0, 0), 14, 10));
+ } else if (event.getProperty()
+ .getValue() == ColorRangePropertyId.GREEN) {
+ grid.setColorGrid(createColors(new Color(0, 0xFF, 0), 14, 10));
+ } else if (event.getProperty()
+ .getValue() == ColorRangePropertyId.BLUE) {
+ grid.setColorGrid(createColors(new Color(0, 0, 0xFF), 14, 10));
+ }
+ }
+}
diff --git a/compatibility-server/src/main/java/com/vaadin/v7/ui/components/colorpicker/ColorSelector.java b/compatibility-server/src/main/java/com/vaadin/v7/ui/components/colorpicker/ColorSelector.java
new file mode 100644
index 0000000000..a4da97c46b
--- /dev/null
+++ b/compatibility-server/src/main/java/com/vaadin/v7/ui/components/colorpicker/ColorSelector.java
@@ -0,0 +1,43 @@
+/*
+ * 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.v7.ui.components.colorpicker;
+
+import java.io.Serializable;
+
+import com.vaadin.shared.ui.colorpicker.Color;
+
+/**
+ * An interface for a color selector.
+ *
+ * @since 7.0.0
+ */
+public interface ColorSelector extends Serializable, HasColorChangeListener {
+
+ /**
+ * Sets the color.
+ *
+ * @param color
+ * the new color
+ */
+ public void setColor(Color color);
+
+ /**
+ * Gets the color.
+ *
+ * @return the color
+ */
+ public Color getColor();
+}
diff --git a/compatibility-server/src/main/java/com/vaadin/v7/ui/components/colorpicker/HasColorChangeListener.java b/compatibility-server/src/main/java/com/vaadin/v7/ui/components/colorpicker/HasColorChangeListener.java
new file mode 100644
index 0000000000..ed416c1ebe
--- /dev/null
+++ b/compatibility-server/src/main/java/com/vaadin/v7/ui/components/colorpicker/HasColorChangeListener.java
@@ -0,0 +1,36 @@
+/*
+ * 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.v7.ui.components.colorpicker;
+
+import java.io.Serializable;
+
+public interface HasColorChangeListener extends Serializable {
+
+ /**
+ * Adds a {@link ColorChangeListener} to the component.
+ *
+ * @param listener
+ */
+ void addColorChangeListener(ColorChangeListener listener);
+
+ /**
+ * Removes a {@link ColorChangeListener} from the component.
+ *
+ * @param listener
+ */
+ void removeColorChangeListener(ColorChangeListener listener);
+
+}
diff --git a/compatibility-server/src/main/java/com/vaadin/v7/ui/renderers/AbstractJavaScriptRenderer.java b/compatibility-server/src/main/java/com/vaadin/v7/ui/renderers/AbstractJavaScriptRenderer.java
new file mode 100644
index 0000000000..2d0ac7f62e
--- /dev/null
+++ b/compatibility-server/src/main/java/com/vaadin/v7/ui/renderers/AbstractJavaScriptRenderer.java
@@ -0,0 +1,175 @@
+/*
+ * 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.v7.ui.renderers;
+
+import com.vaadin.server.AbstractJavaScriptExtension;
+import com.vaadin.server.JavaScriptCallbackHelper;
+import com.vaadin.server.JsonCodec;
+import com.vaadin.shared.JavaScriptExtensionState;
+import com.vaadin.shared.communication.ServerRpc;
+import com.vaadin.ui.JavaScriptFunction;
+import com.vaadin.v7.ui.Grid.AbstractRenderer;
+
+import elemental.json.Json;
+import elemental.json.JsonValue;
+
+/**
+ * Base class for Renderers with all client-side logic implemented using
+ * JavaScript.
+ * <p>
+ * When a new JavaScript renderer is initialized in the browser, the framework
+ * will look for a globally defined JavaScript function that will initialize the
+ * renderer. The name of the initialization function is formed by replacing .
+ * with _ in the name of the server-side class. If no such function is defined,
+ * each super class is used in turn until a match is found. The framework will
+ * thus first attempt with <code>com_example_MyRenderer</code> for the
+ * server-side
+ * <code>com.example.MyRenderer extends AbstractJavaScriptRenderer</code> class.
+ * If MyRenderer instead extends <code>com.example.SuperRenderer</code> , then
+ * <code>com_example_SuperRenderer</code> will also be attempted if
+ * <code>com_example_MyRenderer</code> has not been defined.
+ * <p>
+ *
+ * In addition to the general JavaScript extension functionality explained in
+ * {@link AbstractJavaScriptExtension}, this class also provides some
+ * functionality specific for renderers.
+ * <p>
+ * The initialization function will be called with <code>this</code> pointing to
+ * a connector wrapper object providing integration to Vaadin. Please note that
+ * in JavaScript, <code>this</code> is not necessarily defined inside callback
+ * functions and it might therefore be necessary to assign the reference to a
+ * separate variable, e.g. <code>var self = this;</code>. In addition to the
+ * extension functions described for {@link AbstractJavaScriptExtension}, the
+ * connector wrapper object also provides this function:
+ * <ul>
+ * <li><code>getRowKey(rowIndex)</code> - Gets a unique identifier for the row
+ * at the given index. This identifier can be used on the server to retrieve the
+ * corresponding ItemId using {@link #getItemId(String)}.</li>
+ * </ul>
+ * The connector wrapper also supports these special functions that can be
+ * implemented by the connector:
+ * <ul>
+ * <li><code>render(cell, data)</code> - Callback for rendering the given data
+ * into the given cell. The structure of cell and data are described in separate
+ * sections below. The renderer is required to implement this function.
+ * Corresponds to
+ * {@link com.vaadin.client.renderers.Renderer#render(com.vaadin.client.widget.grid.RendererCellReference, Object)}
+ * .</li>
+ * <li><code>init(cell)</code> - Prepares a cell for rendering. Corresponds to
+ * {@link com.vaadin.client.renderers.ComplexRenderer#init(com.vaadin.client.widget.grid.RendererCellReference)}
+ * .</li>
+ * <li><code>destory(cell)</code> - Allows the renderer to release resources
+ * allocate for a cell that will no longer be used. Corresponds to
+ * {@link com.vaadin.client.renderers.ComplexRenderer#destroy(com.vaadin.client.widget.grid.RendererCellReference)}
+ * .</li>
+ * <li><code>onActivate(cell)</code> - Called when the cell is activated by the
+ * user e.g. by double clicking on the cell or pressing enter with the cell
+ * focused. Corresponds to
+ * {@link com.vaadin.client.renderers.ComplexRenderer#onActivate(com.vaadin.client.widget.grid.CellReference)}
+ * .</li>
+ * <li><code>getConsumedEvents()</code> - Returns a JavaScript array of event
+ * names that should cause onBrowserEvent to be invoked whenever an event is
+ * fired for a cell managed by this renderer. Corresponds to
+ * {@link com.vaadin.client.renderers.ComplexRenderer#getConsumedEvents()}.</li>
+ * <li><code>onBrowserEvent(cell, event)</code> - Called by Grid when an event
+ * of a type returned by getConsumedEvents is fired for a cell managed by this
+ * renderer. Corresponds to
+ * {@link com.vaadin.client.renderers.ComplexRenderer#onBrowserEvent(com.vaadin.client.widget.grid.CellReference, com.google.gwt.dom.client.NativeEvent)}
+ * .</li>
+ * </ul>
+ *
+ * <p>
+ * The cell object passed to functions defined by the renderer has these
+ * properties:
+ * <ul>
+ * <li><code>element</code> - The DOM element corresponding to this cell.
+ * Readonly.</li>
+ * <li><code>rowIndex</code> - The current index of the row of this cell.
+ * Readonly.</li>
+ * <li><code>columnIndex</code> - The current index of the column of this cell.
+ * Readonly.</li>
+ * <li><code>colSpan</code> - The number of columns spanned by this cell. Only
+ * supported in the object passed to the <code>render</code> function - other
+ * functions should not use the property. Readable and writable.
+ * </ul>
+ *
+ * @author Vaadin Ltd
+ * @since 7.4
+ */
+public abstract class AbstractJavaScriptRenderer<T>
+ extends AbstractRenderer<T> {
+ private JavaScriptCallbackHelper callbackHelper = new JavaScriptCallbackHelper(
+ this);
+
+ protected AbstractJavaScriptRenderer(Class<T> presentationType,
+ String nullRepresentation) {
+ super(presentationType, nullRepresentation);
+ }
+
+ protected AbstractJavaScriptRenderer(Class<T> presentationType) {
+ super(presentationType, null);
+ }
+
+ @Override
+ protected <R extends ServerRpc> void registerRpc(R implementation,
+ Class<R> rpcInterfaceType) {
+ super.registerRpc(implementation, rpcInterfaceType);
+ callbackHelper.registerRpc(rpcInterfaceType);
+ }
+
+ /**
+ * Register a {@link JavaScriptFunction} that can be called from the
+ * JavaScript using the provided name. A JavaScript function with the
+ * provided name will be added to the connector wrapper object (initially
+ * available as <code>this</code>). Calling that JavaScript function will
+ * cause the call method in the registered {@link JavaScriptFunction} to be
+ * invoked with the same arguments.
+ *
+ * @param functionName
+ * the name that should be used for client-side callback
+ * @param function
+ * the {@link JavaScriptFunction} object that will be invoked
+ * when the JavaScript function is called
+ */
+ protected void addFunction(String functionName,
+ JavaScriptFunction function) {
+ callbackHelper.registerCallback(functionName, function);
+ }
+
+ /**
+ * Invoke a named function that the connector JavaScript has added to the
+ * JavaScript connector wrapper object. The arguments can be any boxed
+ * primitive type, String, {@link JsonValue} or arrays of any other
+ * supported type. Complex types (e.g. List, Set, Map, Connector or any
+ * JavaBean type) must be explicitly serialized to a {@link JsonValue}
+ * before sending. This can be done either with
+ * {@link JsonCodec#encode(Object, JsonValue, java.lang.reflect.Type, com.vaadin.ui.ConnectorTracker)}
+ * or using the factory methods in {@link Json}.
+ *
+ * @param name
+ * the name of the function
+ * @param arguments
+ * function arguments
+ */
+ protected void callFunction(String name, Object... arguments) {
+ callbackHelper.invokeCallback(name, arguments);
+ }
+
+ @Override
+ protected JavaScriptExtensionState getState() {
+ return (JavaScriptExtensionState) super.getState();
+ }
+}
diff --git a/compatibility-server/src/main/java/com/vaadin/v7/ui/renderers/ButtonRenderer.java b/compatibility-server/src/main/java/com/vaadin/v7/ui/renderers/ButtonRenderer.java
new file mode 100644
index 0000000000..906fc025bb
--- /dev/null
+++ b/compatibility-server/src/main/java/com/vaadin/v7/ui/renderers/ButtonRenderer.java
@@ -0,0 +1,74 @@
+/*
+ * 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.v7.ui.renderers;
+
+/**
+ * A Renderer that displays a button with a textual caption. The value of the
+ * corresponding property is used as the caption. Click listeners can be added
+ * to the renderer, invoked when any of the rendered buttons is clicked.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public class ButtonRenderer extends ClickableRenderer<String> {
+
+ /**
+ * Creates a new button renderer.
+ *
+ * @param nullRepresentation
+ * the textual representation of {@code null} value
+ */
+ public ButtonRenderer(String nullRepresentation) {
+ super(String.class, nullRepresentation);
+ }
+
+ /**
+ * Creates a new button renderer and adds the given click listener to it.
+ *
+ * @param listener
+ * the click listener to register
+ * @param nullRepresentation
+ * the textual representation of {@code null} value
+ */
+ public ButtonRenderer(RendererClickListener listener,
+ String nullRepresentation) {
+ this(nullRepresentation);
+ addClickListener(listener);
+ }
+
+ /**
+ * Creates a new button renderer.
+ */
+ public ButtonRenderer() {
+ this("");
+ }
+
+ /**
+ * Creates a new button renderer and adds the given click listener to it.
+ *
+ * @param listener
+ * the click listener to register
+ */
+ public ButtonRenderer(RendererClickListener listener) {
+ this(listener, "");
+ }
+
+ @Override
+ public String getNullRepresentation() {
+ return super.getNullRepresentation();
+ }
+
+}
diff --git a/compatibility-server/src/main/java/com/vaadin/v7/ui/renderers/ClickableRenderer.java b/compatibility-server/src/main/java/com/vaadin/v7/ui/renderers/ClickableRenderer.java
new file mode 100644
index 0000000000..a1f48a99c4
--- /dev/null
+++ b/compatibility-server/src/main/java/com/vaadin/v7/ui/renderers/ClickableRenderer.java
@@ -0,0 +1,143 @@
+/*
+ * 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.v7.ui.renderers;
+
+import java.lang.reflect.Method;
+
+import com.vaadin.event.ConnectorEventListener;
+import com.vaadin.event.MouseEvents.ClickEvent;
+import com.vaadin.shared.MouseEventDetails;
+import com.vaadin.shared.ui.grid.renderers.RendererClickRpc;
+import com.vaadin.util.ReflectTools;
+import com.vaadin.v7.ui.Grid;
+import com.vaadin.v7.ui.Grid.AbstractRenderer;
+import com.vaadin.v7.ui.Grid.Column;
+
+/**
+ * An abstract superclass for Renderers that render clickable items. Click
+ * listeners can be added to a renderer to be notified when any of the rendered
+ * items is clicked.
+ *
+ * @param <T>
+ * the type presented by the renderer
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public class ClickableRenderer<T> extends AbstractRenderer<T> {
+
+ /**
+ * An interface for listening to {@link RendererClickEvent renderer click
+ * events}.
+ *
+ * @see {@link ButtonRenderer#addClickListener(RendererClickListener)}
+ */
+ public interface RendererClickListener extends ConnectorEventListener {
+
+ static final Method CLICK_METHOD = ReflectTools.findMethod(
+ RendererClickListener.class, "click", RendererClickEvent.class);
+
+ /**
+ * Called when a rendered button is clicked.
+ *
+ * @param event
+ * the event representing the click
+ */
+ void click(RendererClickEvent event);
+ }
+
+ /**
+ * An event fired when a button rendered by a ButtonRenderer is clicked.
+ */
+ public static class RendererClickEvent extends ClickEvent {
+
+ private Object itemId;
+ private Column column;
+
+ protected RendererClickEvent(Grid source, Object itemId, Column column,
+ MouseEventDetails mouseEventDetails) {
+ super(source, mouseEventDetails);
+ this.itemId = itemId;
+ this.column = column;
+ }
+
+ /**
+ * Returns the item ID of the row where the click event originated.
+ *
+ * @return the item ID of the clicked row
+ */
+ public Object getItemId() {
+ return itemId;
+ }
+
+ /**
+ * Returns the {@link Column} where the click event originated.
+ *
+ * @return the column of the click event
+ */
+ public Column getColumn() {
+ return column;
+ }
+
+ /**
+ * Returns the property ID where the click event originated.
+ *
+ * @return the property ID of the clicked cell
+ */
+ public Object getPropertyId() {
+ return column.getPropertyId();
+ }
+ }
+
+ protected ClickableRenderer(Class<T> presentationType) {
+ this(presentationType, null);
+ }
+
+ protected ClickableRenderer(Class<T> presentationType,
+ String nullRepresentation) {
+ super(presentationType, nullRepresentation);
+ registerRpc(new RendererClickRpc() {
+ @Override
+ public void click(String rowKey, String columnId,
+ MouseEventDetails mouseDetails) {
+ fireEvent(new RendererClickEvent(getParentGrid(),
+ getItemId(rowKey), getColumn(columnId), mouseDetails));
+ }
+ });
+ }
+
+ /**
+ * Adds a click listener to this button renderer. The listener is invoked
+ * every time one of the buttons rendered by this renderer is clicked.
+ *
+ * @param listener
+ * the click listener to be added
+ */
+ public void addClickListener(RendererClickListener listener) {
+ addListener(RendererClickEvent.class, listener,
+ RendererClickListener.CLICK_METHOD);
+ }
+
+ /**
+ * Removes the given click listener from this renderer.
+ *
+ * @param listener
+ * the click listener to be removed
+ */
+ public void removeClickListener(RendererClickListener listener) {
+ removeListener(RendererClickEvent.class, listener);
+ }
+}
diff --git a/compatibility-server/src/main/java/com/vaadin/v7/ui/renderers/DateRenderer.java b/compatibility-server/src/main/java/com/vaadin/v7/ui/renderers/DateRenderer.java
new file mode 100644
index 0000000000..ac3b831acc
--- /dev/null
+++ b/compatibility-server/src/main/java/com/vaadin/v7/ui/renderers/DateRenderer.java
@@ -0,0 +1,240 @@
+/*
+ * 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.v7.ui.renderers;
+
+import java.text.DateFormat;
+import java.util.Date;
+import java.util.Locale;
+
+import com.vaadin.v7.ui.Grid.AbstractRenderer;
+
+import elemental.json.JsonValue;
+
+/**
+ * A renderer for presenting date values.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public class DateRenderer extends AbstractRenderer<Date> {
+ private final Locale locale;
+ private final String formatString;
+ private final DateFormat dateFormat;
+
+ /**
+ * Creates a new date renderer.
+ * <p>
+ * The renderer is configured to render with the {@link Date#toString()}
+ * representation for the default locale.
+ */
+ public DateRenderer() {
+ this(Locale.getDefault(), "");
+ }
+
+ /**
+ * Creates a new date renderer.
+ * <p>
+ * The renderer is configured to render with the {@link Date#toString()}
+ * representation for the given locale.
+ *
+ * @param locale
+ * the locale in which to present dates
+ * @throws IllegalArgumentException
+ * if {@code locale} is {@code null}
+ */
+ public DateRenderer(Locale locale) throws IllegalArgumentException {
+ this("%s", locale, "");
+ }
+
+ /**
+ * Creates a new date renderer.
+ * <p>
+ * The renderer is configured to render with the {@link Date#toString()}
+ * representation for the given locale.
+ *
+ * @param locale
+ * the locale in which to present dates
+ * @param nullRepresentation
+ * the textual representation of {@code null} value
+ * @throws IllegalArgumentException
+ * if {@code locale} is {@code null}
+ */
+ public DateRenderer(Locale locale, String nullRepresentation)
+ throws IllegalArgumentException {
+ this("%s", locale, nullRepresentation);
+ }
+
+ /**
+ * Creates a new date renderer.
+ * <p>
+ * The renderer is configured to render with the given string format, as
+ * displayed in the default locale.
+ *
+ * @param formatString
+ * the format string with which to format the date
+ * @throws IllegalArgumentException
+ * if {@code formatString} is {@code null}
+ * @see <a href=
+ * "http://docs.oracle.com/javase/7/docs/api/java/util/Formatter.html#syntax">Format
+ * String Syntax</a>
+ */
+ public DateRenderer(String formatString) throws IllegalArgumentException {
+ this(formatString, "");
+ }
+
+ /**
+ * Creates a new date renderer.
+ * <p>
+ * The renderer is configured to render with the given string format, as
+ * displayed in the default locale.
+ *
+ * @param formatString
+ * the format string with which to format the date
+ * @param nullRepresentation
+ * the textual representation of {@code null} value
+ * @throws IllegalArgumentException
+ * if {@code formatString} is {@code null}
+ * @see <a href=
+ * "http://docs.oracle.com/javase/7/docs/api/java/util/Formatter.html#syntax">Format
+ * String Syntax</a>
+ */
+ public DateRenderer(String formatString, String nullRepresentation)
+ throws IllegalArgumentException {
+ this(formatString, Locale.getDefault(), nullRepresentation);
+ }
+
+ /**
+ * Creates a new date renderer.
+ * <p>
+ * The renderer is configured to render with the given string format, as
+ * displayed in the given locale.
+ *
+ * @param formatString
+ * the format string to format the date with
+ * @param locale
+ * the locale to use
+ * @throws IllegalArgumentException
+ * if either argument is {@code null}
+ * @see <a href=
+ * "http://docs.oracle.com/javase/7/docs/api/java/util/Formatter.html#syntax">Format
+ * String Syntax</a>
+ */
+ public DateRenderer(String formatString, Locale locale)
+ throws IllegalArgumentException {
+ this(formatString, locale, "");
+ }
+
+ /**
+ * Creates a new date renderer.
+ * <p>
+ * The renderer is configured to render with the given string format, as
+ * displayed in the given locale.
+ *
+ * @param formatString
+ * the format string to format the date with
+ * @param locale
+ * the locale to use
+ * @param nullRepresentation
+ * the textual representation of {@code null} value
+ * @throws IllegalArgumentException
+ * if either argument is {@code null}
+ * @see <a href=
+ * "http://docs.oracle.com/javase/7/docs/api/java/util/Formatter.html#syntax">Format
+ * String Syntax</a>
+ */
+ public DateRenderer(String formatString, Locale locale,
+ String nullRepresentation) throws IllegalArgumentException {
+ super(Date.class, nullRepresentation);
+
+ if (formatString == null) {
+ throw new IllegalArgumentException("format string may not be null");
+ }
+
+ if (locale == null) {
+ throw new IllegalArgumentException("locale may not be null");
+ }
+
+ this.locale = locale;
+ this.formatString = formatString;
+ dateFormat = null;
+ }
+
+ /**
+ * Creates a new date renderer.
+ * <p>
+ * The renderer is configured to render with he given date format.
+ *
+ * @param dateFormat
+ * the date format to use when rendering dates
+ * @throws IllegalArgumentException
+ * if {@code dateFormat} is {@code null}
+ */
+ public DateRenderer(DateFormat dateFormat) throws IllegalArgumentException {
+ this(dateFormat, "");
+ }
+
+ /**
+ * Creates a new date renderer.
+ * <p>
+ * The renderer is configured to render with he given date format.
+ *
+ * @param dateFormat
+ * the date format to use when rendering dates
+ * @throws IllegalArgumentException
+ * if {@code dateFormat} is {@code null}
+ */
+ public DateRenderer(DateFormat dateFormat, String nullRepresentation)
+ throws IllegalArgumentException {
+ super(Date.class, nullRepresentation);
+ if (dateFormat == null) {
+ throw new IllegalArgumentException("date format may not be null");
+ }
+
+ locale = null;
+ formatString = null;
+ this.dateFormat = dateFormat;
+ }
+
+ @Override
+ public String getNullRepresentation() {
+ return super.getNullRepresentation();
+ }
+
+ @Override
+ public JsonValue encode(Date value) {
+ String dateString;
+ if (value == null) {
+ dateString = getNullRepresentation();
+ } else if (dateFormat != null) {
+ dateString = dateFormat.format(value);
+ } else {
+ dateString = String.format(locale, formatString, value);
+ }
+ return encode(dateString, String.class);
+ }
+
+ @Override
+ public String toString() {
+ final String fieldInfo;
+ if (dateFormat != null) {
+ fieldInfo = "dateFormat: " + dateFormat.toString();
+ } else {
+ fieldInfo = "locale: " + locale + ", formatString: " + formatString;
+ }
+
+ return String.format("%s [%s]", getClass().getSimpleName(), fieldInfo);
+ }
+}
diff --git a/compatibility-server/src/main/java/com/vaadin/v7/ui/renderers/HtmlRenderer.java b/compatibility-server/src/main/java/com/vaadin/v7/ui/renderers/HtmlRenderer.java
new file mode 100644
index 0000000000..5264f19e0b
--- /dev/null
+++ b/compatibility-server/src/main/java/com/vaadin/v7/ui/renderers/HtmlRenderer.java
@@ -0,0 +1,48 @@
+/*
+ * 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.v7.ui.renderers;
+
+import com.vaadin.v7.ui.Grid.AbstractRenderer;
+
+/**
+ * A renderer for presenting HTML content.
+ *
+ * @author Vaadin Ltd
+ * @since 7.4
+ */
+public class HtmlRenderer extends AbstractRenderer<String> {
+ /**
+ * Creates a new HTML renderer.
+ *
+ * @param nullRepresentation
+ * the html representation of {@code null} value
+ */
+ public HtmlRenderer(String nullRepresentation) {
+ super(String.class, nullRepresentation);
+ }
+
+ /**
+ * Creates a new HTML renderer.
+ */
+ public HtmlRenderer() {
+ this("");
+ }
+
+ @Override
+ public String getNullRepresentation() {
+ return super.getNullRepresentation();
+ }
+}
diff --git a/compatibility-server/src/main/java/com/vaadin/v7/ui/renderers/ImageRenderer.java b/compatibility-server/src/main/java/com/vaadin/v7/ui/renderers/ImageRenderer.java
new file mode 100644
index 0000000000..19c7a77b01
--- /dev/null
+++ b/compatibility-server/src/main/java/com/vaadin/v7/ui/renderers/ImageRenderer.java
@@ -0,0 +1,68 @@
+/*
+ * 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.v7.ui.renderers;
+
+import com.vaadin.server.ExternalResource;
+import com.vaadin.server.Resource;
+import com.vaadin.server.ResourceReference;
+import com.vaadin.server.ThemeResource;
+import com.vaadin.shared.communication.URLReference;
+
+import elemental.json.JsonValue;
+
+/**
+ * A renderer for presenting images.
+ * <p>
+ * The image for each rendered cell is read from a Resource-typed property in
+ * the data source. Only {@link ExternalResource}s and {@link ThemeResource}s
+ * are currently supported.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public class ImageRenderer extends ClickableRenderer<Resource> {
+
+ /**
+ * Creates a new image renderer.
+ */
+ public ImageRenderer() {
+ super(Resource.class, null);
+ }
+
+ /**
+ * Creates a new image renderer and adds the given click listener to it.
+ *
+ * @param listener
+ * the click listener to register
+ */
+ public ImageRenderer(RendererClickListener listener) {
+ this();
+ addClickListener(listener);
+ }
+
+ @Override
+ public JsonValue encode(Resource resource) {
+ if (!(resource == null || resource instanceof ExternalResource
+ || resource instanceof ThemeResource)) {
+ throw new IllegalArgumentException(
+ "ImageRenderer only supports ExternalResource and ThemeResource ("
+ + resource.getClass().getSimpleName() + " given)");
+ }
+
+ return encode(ResourceReference.create(resource, this, null),
+ URLReference.class);
+ }
+}
diff --git a/compatibility-server/src/main/java/com/vaadin/v7/ui/renderers/NumberRenderer.java b/compatibility-server/src/main/java/com/vaadin/v7/ui/renderers/NumberRenderer.java
new file mode 100644
index 0000000000..061f6d5790
--- /dev/null
+++ b/compatibility-server/src/main/java/com/vaadin/v7/ui/renderers/NumberRenderer.java
@@ -0,0 +1,207 @@
+/*
+ * 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.v7.ui.renderers;
+
+import java.text.NumberFormat;
+import java.util.Locale;
+
+import com.vaadin.v7.ui.Grid.AbstractRenderer;
+
+import elemental.json.JsonValue;
+
+/**
+ * A renderer for presenting number values.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public class NumberRenderer extends AbstractRenderer<Number> {
+ private final Locale locale;
+ private final NumberFormat numberFormat;
+ private final String formatString;
+
+ /**
+ * Creates a new number renderer.
+ * <p/>
+ * The renderer is configured to render with the number's natural string
+ * representation in the default locale.
+ */
+ public NumberRenderer() {
+ this(Locale.getDefault());
+ }
+
+ /**
+ * Creates a new number renderer.
+ * <p/>
+ * The renderer is configured to render the number as defined with the given
+ * number format.
+ *
+ * @param numberFormat
+ * the number format with which to display numbers
+ * @throws IllegalArgumentException
+ * if {@code numberFormat} is {@code null}
+ */
+ public NumberRenderer(NumberFormat numberFormat) {
+ this(numberFormat, "");
+ }
+
+ /**
+ * Creates a new number renderer.
+ * <p/>
+ * The renderer is configured to render the number as defined with the given
+ * number format.
+ *
+ * @param numberFormat
+ * the number format with which to display numbers
+ * @param nullRepresentation
+ * the textual representation of {@code null} value
+ * @throws IllegalArgumentException
+ * if {@code numberFormat} is {@code null}
+ */
+ public NumberRenderer(NumberFormat numberFormat, String nullRepresentation)
+ throws IllegalArgumentException {
+ super(Number.class, nullRepresentation);
+
+ if (numberFormat == null) {
+ throw new IllegalArgumentException("Number format may not be null");
+ }
+
+ locale = null;
+ this.numberFormat = numberFormat;
+ formatString = null;
+ }
+
+ /**
+ * Creates a new number renderer.
+ * <p/>
+ * The renderer is configured to render with the number's natural string
+ * representation in the given locale.
+ *
+ * @param locale
+ * the locale in which to display numbers
+ * @throws IllegalArgumentException
+ * if {@code locale} is {@code null}
+ */
+ public NumberRenderer(Locale locale) throws IllegalArgumentException {
+ this("%s", locale);
+ }
+
+ /**
+ * Creates a new number renderer.
+ * <p/>
+ * The renderer is configured to render with the number's natural string
+ * representation in the given locale.
+ *
+ * @param formatString
+ * the format string with which to format the number
+ * @param locale
+ * the locale in which to display numbers
+ * @throws IllegalArgumentException
+ * if {@code locale} is {@code null}
+ */
+ public NumberRenderer(String formatString, Locale locale)
+ throws IllegalArgumentException {
+ this(formatString, locale, ""); // This will call #toString() during
+ // formatting
+ }
+
+ /**
+ * Creates a new number renderer.
+ * <p/>
+ * The renderer is configured to render with the given format string in the
+ * default locale.
+ *
+ * @param formatString
+ * the format string with which to format the number
+ * @throws IllegalArgumentException
+ * if {@code formatString} is {@code null}
+ * @see <a href=
+ * "http://docs.oracle.com/javase/7/docs/api/java/util/Formatter.html#syntax">Format
+ * String Syntax</a>
+ */
+ public NumberRenderer(String formatString) throws IllegalArgumentException {
+ this(formatString, Locale.getDefault(), "");
+ }
+
+ /**
+ * Creates a new number renderer.
+ * <p/>
+ * The renderer is configured to render with the given format string in the
+ * given locale.
+ *
+ * @param formatString
+ * the format string with which to format the number
+ * @param locale
+ * the locale in which to present numbers
+ * @throws IllegalArgumentException
+ * if either argument is {@code null}
+ * @see <a href=
+ * "http://docs.oracle.com/javase/7/docs/api/java/util/Formatter.html#syntax">Format
+ * String Syntax</a>
+ */
+ public NumberRenderer(String formatString, Locale locale,
+ String nullRepresentation) {
+ super(Number.class, nullRepresentation);
+
+ if (formatString == null) {
+ throw new IllegalArgumentException("Format string may not be null");
+ }
+
+ if (locale == null) {
+ throw new IllegalArgumentException("Locale may not be null");
+ }
+
+ this.locale = locale;
+ numberFormat = null;
+ this.formatString = formatString;
+ }
+
+ @Override
+ public JsonValue encode(Number value) {
+ String stringValue;
+ if (value == null) {
+ stringValue = getNullRepresentation();
+ } else if (formatString != null && locale != null) {
+ stringValue = String.format(locale, formatString, value);
+ } else if (numberFormat != null) {
+ stringValue = numberFormat.format(value);
+ } else {
+ throw new IllegalStateException(String.format(
+ "Internal bug: " + "%s is in an illegal state: "
+ + "[locale: %s, numberFormat: %s, formatString: %s]",
+ getClass().getSimpleName(), locale, numberFormat,
+ formatString));
+ }
+ return encode(stringValue, String.class);
+ }
+
+ @Override
+ public String toString() {
+ final String fieldInfo;
+ if (numberFormat != null) {
+ fieldInfo = "numberFormat: " + numberFormat.toString();
+ } else {
+ fieldInfo = "locale: " + locale + ", formatString: " + formatString;
+ }
+
+ return String.format("%s [%s]", getClass().getSimpleName(), fieldInfo);
+ }
+
+ @Override
+ public String getNullRepresentation() {
+ return super.getNullRepresentation();
+ }
+}
diff --git a/compatibility-server/src/main/java/com/vaadin/v7/ui/renderers/ProgressBarRenderer.java b/compatibility-server/src/main/java/com/vaadin/v7/ui/renderers/ProgressBarRenderer.java
new file mode 100644
index 0000000000..d19c09ec59
--- /dev/null
+++ b/compatibility-server/src/main/java/com/vaadin/v7/ui/renderers/ProgressBarRenderer.java
@@ -0,0 +1,46 @@
+/*
+ * 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.v7.ui.renderers;
+
+import com.vaadin.v7.ui.Grid.AbstractRenderer;
+
+import elemental.json.JsonValue;
+
+/**
+ * A renderer that represents a double values as a graphical progress bar.
+ *
+ * @author Vaadin Ltd
+ * @since 7.4
+ */
+public class ProgressBarRenderer extends AbstractRenderer<Double> {
+
+ /**
+ * Creates a new text renderer
+ */
+ public ProgressBarRenderer() {
+ super(Double.class, null);
+ }
+
+ @Override
+ public JsonValue encode(Double value) {
+ if (value != null) {
+ value = Math.max(Math.min(value, 1), 0);
+ } else {
+ value = 0d;
+ }
+ return super.encode(value);
+ }
+}
diff --git a/compatibility-server/src/main/java/com/vaadin/v7/ui/renderers/Renderer.java b/compatibility-server/src/main/java/com/vaadin/v7/ui/renderers/Renderer.java
new file mode 100644
index 0000000000..ce221760cc
--- /dev/null
+++ b/compatibility-server/src/main/java/com/vaadin/v7/ui/renderers/Renderer.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.v7.ui.renderers;
+
+import com.vaadin.server.ClientConnector;
+import com.vaadin.server.Extension;
+
+import elemental.json.JsonValue;
+
+/**
+ * A ClientConnector for controlling client-side
+ * {@link com.vaadin.client.widget.grid.Renderer Grid renderers}. Renderers
+ * currently extend the Extension interface, but this fact should be regarded as
+ * an implementation detail and subject to change in a future major or minor
+ * Vaadin revision.
+ *
+ * @param <T>
+ * the type this renderer knows how to present
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public interface Renderer<T> extends Extension {
+
+ /**
+ * Returns the class literal corresponding to the presentation type T.
+ *
+ * @return the class literal of T
+ */
+ Class<T> getPresentationType();
+
+ /**
+ * Encodes the given value into a {@link JsonValue}.
+ *
+ * @param value
+ * the value to encode
+ * @return a JSON representation of the given value
+ */
+ JsonValue encode(T value);
+
+ /**
+ * This method is inherited from Extension but should never be called
+ * directly with a Renderer.
+ */
+ @Override
+ @Deprecated
+ void remove();
+
+ /**
+ * This method is inherited from Extension but should never be called
+ * directly with a Renderer.
+ */
+ @Override
+ @Deprecated
+ void setParent(ClientConnector parent);
+}
diff --git a/compatibility-server/src/main/java/com/vaadin/v7/ui/renderers/TextRenderer.java b/compatibility-server/src/main/java/com/vaadin/v7/ui/renderers/TextRenderer.java
new file mode 100644
index 0000000000..23306baf75
--- /dev/null
+++ b/compatibility-server/src/main/java/com/vaadin/v7/ui/renderers/TextRenderer.java
@@ -0,0 +1,49 @@
+/*
+ * 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.v7.ui.renderers;
+
+import com.vaadin.v7.ui.Grid.AbstractRenderer;
+
+/**
+ * A renderer for presenting simple plain-text string values.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public class TextRenderer extends AbstractRenderer<String> {
+
+ /**
+ * Creates a new text renderer
+ */
+ public TextRenderer() {
+ this("");
+ }
+
+ /**
+ * Creates a new text renderer
+ *
+ * @param nullRepresentation
+ * the textual representation of {@code null} value
+ */
+ public TextRenderer(String nullRepresentation) {
+ super(String.class, nullRepresentation);
+ }
+
+ @Override
+ public String getNullRepresentation() {
+ return super.getNullRepresentation();
+ }
+}