diff options
author | Artur Signell <artur@vaadin.com> | 2016-08-18 23:19:41 +0300 |
---|---|---|
committer | Artur Signell <artur@vaadin.com> | 2016-08-22 15:59:51 +0300 |
commit | c6b44ac8adc9b2ffd6290c98643a633f405dd6c6 (patch) | |
tree | 634982e9a789452ab8046e660a9170cc8b25623f /compatibility-server/src/main/java/com/vaadin/v7/ui | |
parent | ec8904f6b0ab77231d567daa35c9cc7138b6fe59 (diff) | |
download | vaadin-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')
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> + * – 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> + * 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(); + } +} |