diff options
author | Artur Signell <artur@vaadin.com> | 2012-08-13 18:34:33 +0300 |
---|---|---|
committer | Artur Signell <artur@vaadin.com> | 2012-08-13 19:18:33 +0300 |
commit | e85d933b25cc3c5cc85eb7eb4b13b950fd8e1569 (patch) | |
tree | 9ab6f13f7188cab44bbd979b1cf620f15328a03f /server/src/com/vaadin/ui | |
parent | 14dd4d0b28c76eb994b181a4570f3adec53342e6 (diff) | |
download | vaadin-framework-e85d933b25cc3c5cc85eb7eb4b13b950fd8e1569.tar.gz vaadin-framework-e85d933b25cc3c5cc85eb7eb4b13b950fd8e1569.zip |
Moved server files to a server src folder (#9299)
Diffstat (limited to 'server/src/com/vaadin/ui')
81 files changed, 36843 insertions, 0 deletions
diff --git a/server/src/com/vaadin/ui/AbsoluteLayout.java b/server/src/com/vaadin/ui/AbsoluteLayout.java new file mode 100644 index 0000000000..1c84ca2865 --- /dev/null +++ b/server/src/com/vaadin/ui/AbsoluteLayout.java @@ -0,0 +1,632 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.ui; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; + +import com.vaadin.event.LayoutEvents.LayoutClickEvent; +import com.vaadin.event.LayoutEvents.LayoutClickListener; +import com.vaadin.event.LayoutEvents.LayoutClickNotifier; +import com.vaadin.shared.Connector; +import com.vaadin.shared.MouseEventDetails; +import com.vaadin.shared.ui.absolutelayout.AbsoluteLayoutServerRpc; +import com.vaadin.shared.ui.absolutelayout.AbsoluteLayoutState; +import com.vaadin.terminal.Sizeable; +import com.vaadin.terminal.gwt.client.ui.LayoutClickEventHandler; + +/** + * AbsoluteLayout is a layout implementation that mimics html absolute + * positioning. + * + */ +@SuppressWarnings("serial") +public class AbsoluteLayout extends AbstractLayout implements + LayoutClickNotifier { + + private AbsoluteLayoutServerRpc rpc = new AbsoluteLayoutServerRpc() { + + @Override + public void layoutClick(MouseEventDetails mouseDetails, + Connector clickedConnector) { + fireEvent(LayoutClickEvent.createEvent(AbsoluteLayout.this, + mouseDetails, clickedConnector)); + } + }; + // Maps each component to a position + private LinkedHashMap<Component, ComponentPosition> componentToCoordinates = new LinkedHashMap<Component, ComponentPosition>(); + + /** + * Creates an AbsoluteLayout with full size. + */ + public AbsoluteLayout() { + registerRpc(rpc); + setSizeFull(); + } + + @Override + public AbsoluteLayoutState getState() { + return (AbsoluteLayoutState) super.getState(); + } + + /** + * Gets an iterator for going through all components enclosed in the + * absolute layout. + */ + @Override + public Iterator<Component> getComponentIterator() { + return componentToCoordinates.keySet().iterator(); + } + + /** + * Gets the number of contained components. Consistent with the iterator + * returned by {@link #getComponentIterator()}. + * + * @return the number of contained components + */ + @Override + public int getComponentCount() { + return componentToCoordinates.size(); + } + + /** + * Replaces one component with another one. The new component inherits the + * old components position. + */ + @Override + public void replaceComponent(Component oldComponent, Component newComponent) { + ComponentPosition position = getPosition(oldComponent); + removeComponent(oldComponent); + addComponent(newComponent, position); + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.ui.AbstractComponentContainer#addComponent(com.vaadin.ui.Component + * ) + */ + @Override + public void addComponent(Component c) { + addComponent(c, new ComponentPosition()); + } + + /** + * Adds a component to the layout. The component can be positioned by + * providing a string formatted in CSS-format. + * <p> + * For example the string "top:10px;left:10px" will position the component + * 10 pixels from the left and 10 pixels from the top. The identifiers: + * "top","left","right" and "bottom" can be used to specify the position. + * </p> + * + * @param c + * The component to add to the layout + * @param cssPosition + * The css position string + */ + public void addComponent(Component c, String cssPosition) { + ComponentPosition position = new ComponentPosition(); + position.setCSSString(cssPosition); + addComponent(c, position); + } + + /** + * Adds the component using the given position. Ensures the position is only + * set if the component is added correctly. + * + * @param c + * The component to add + * @param position + * The position info for the component. Must not be null. + * @throws IllegalArgumentException + * If adding the component failed + */ + private void addComponent(Component c, ComponentPosition position) + throws IllegalArgumentException { + /* + * Create position instance and add it to componentToCoordinates map. We + * need to do this before we call addComponent so the attachListeners + * can access this position. #6368 + */ + internalSetPosition(c, position); + try { + super.addComponent(c); + } catch (IllegalArgumentException e) { + internalRemoveComponent(c); + throw e; + } + requestRepaint(); + } + + /** + * Removes the component from all internal data structures. Does not + * actually remove the component from the layout (this is assumed to have + * been done by the caller). + * + * @param c + * The component to remove + */ + private void internalRemoveComponent(Component c) { + componentToCoordinates.remove(c); + } + + @Override + public void updateState() { + super.updateState(); + + // This could be in internalRemoveComponent and internalSetComponent if + // Map<Connector,String> was supported. We cannot get the child + // connectorId unless the component is attached to the application so + // the String->String map cannot be populated in internal* either. + Map<String, String> connectorToPosition = new HashMap<String, String>(); + for (Iterator<Component> ci = getComponentIterator(); ci.hasNext();) { + Component c = ci.next(); + connectorToPosition.put(c.getConnectorId(), getPosition(c) + .getCSSString()); + } + getState().setConnectorToCssPosition(connectorToPosition); + + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.ui.AbstractComponentContainer#removeComponent(com.vaadin.ui + * .Component) + */ + @Override + public void removeComponent(Component c) { + internalRemoveComponent(c); + super.removeComponent(c); + requestRepaint(); + } + + /** + * Gets the position of a component in the layout. Returns null if component + * is not attached to the layout. + * <p> + * Note that you cannot update the position by updating this object. Call + * {@link #setPosition(Component, ComponentPosition)} with the updated + * {@link ComponentPosition} object. + * </p> + * + * @param component + * The component which position is needed + * @return An instance of ComponentPosition containing the position of the + * component, or null if the component is not enclosed in the + * layout. + */ + public ComponentPosition getPosition(Component component) { + return componentToCoordinates.get(component); + } + + /** + * Sets the position of a component in the layout. + * + * @param component + * @param position + */ + public void setPosition(Component component, ComponentPosition position) { + if (!componentToCoordinates.containsKey(component)) { + throw new IllegalArgumentException( + "Component must be a child of this layout"); + } + internalSetPosition(component, position); + } + + /** + * Updates the position for a component. Caller must ensure component is a + * child of this layout. + * + * @param component + * The component. Must be a child for this layout. Not enforced. + * @param position + * New position. Must not be null. + */ + private void internalSetPosition(Component component, + ComponentPosition position) { + componentToCoordinates.put(component, position); + requestRepaint(); + } + + /** + * The CompontPosition class represents a components position within the + * absolute layout. It contains the attributes for left, right, top and + * bottom and the units used to specify them. + */ + public class ComponentPosition implements Serializable { + + private int zIndex = -1; + private Float topValue = null; + private Float rightValue = null; + private Float bottomValue = null; + private Float leftValue = null; + + private Unit topUnits = Unit.PIXELS; + private Unit rightUnits = Unit.PIXELS; + private Unit bottomUnits = Unit.PIXELS; + private Unit leftUnits = Unit.PIXELS; + + /** + * Sets the position attributes using CSS syntax. Attributes not + * included in the string are reset to their unset states. + * + * <code><pre> + * setCSSString("top:10px;left:20%;z-index:16;"); + * </pre></code> + * + * @param css + */ + public void setCSSString(String css) { + topValue = rightValue = bottomValue = leftValue = null; + topUnits = rightUnits = bottomUnits = leftUnits = Unit.PIXELS; + zIndex = -1; + if (css == null) { + return; + } + + String[] cssProperties = css.split(";"); + for (int i = 0; i < cssProperties.length; i++) { + String[] keyValuePair = cssProperties[i].split(":"); + String key = keyValuePair[0].trim(); + if (key.equals("")) { + continue; + } + if (key.equals("z-index")) { + zIndex = Integer.parseInt(keyValuePair[1].trim()); + } else { + String value; + if (keyValuePair.length > 1) { + value = keyValuePair[1].trim(); + } else { + value = ""; + } + String symbol = value.replaceAll("[0-9\\.\\-]+", ""); + if (!symbol.equals("")) { + value = value.substring(0, value.indexOf(symbol)) + .trim(); + } + float v = Float.parseFloat(value); + Unit unit = Unit.getUnitFromSymbol(symbol); + if (key.equals("top")) { + topValue = v; + topUnits = unit; + } else if (key.equals("right")) { + rightValue = v; + rightUnits = unit; + } else if (key.equals("bottom")) { + bottomValue = v; + bottomUnits = unit; + } else if (key.equals("left")) { + leftValue = v; + leftUnits = unit; + } + } + } + requestRepaint(); + } + + /** + * Converts the internal values into a valid CSS string. + * + * @return A valid CSS string + */ + public String getCSSString() { + String s = ""; + if (topValue != null) { + s += "top:" + topValue + topUnits.getSymbol() + ";"; + } + if (rightValue != null) { + s += "right:" + rightValue + rightUnits.getSymbol() + ";"; + } + if (bottomValue != null) { + s += "bottom:" + bottomValue + bottomUnits.getSymbol() + ";"; + } + if (leftValue != null) { + s += "left:" + leftValue + leftUnits.getSymbol() + ";"; + } + if (zIndex >= 0) { + s += "z-index:" + zIndex + ";"; + } + return s; + } + + /** + * Sets the 'top' attribute; distance from the top of the component to + * the top edge of the layout. + * + * @param topValue + * The value of the 'top' attribute + * @param topUnits + * The unit of the 'top' attribute. See UNIT_SYMBOLS for a + * description of the available units. + */ + public void setTop(Float topValue, Unit topUnits) { + this.topValue = topValue; + this.topUnits = topUnits; + requestRepaint(); + } + + /** + * Sets the 'right' attribute; distance from the right of the component + * to the right edge of the layout. + * + * @param rightValue + * The value of the 'right' attribute + * @param rightUnits + * The unit of the 'right' attribute. See UNIT_SYMBOLS for a + * description of the available units. + */ + public void setRight(Float rightValue, Unit rightUnits) { + this.rightValue = rightValue; + this.rightUnits = rightUnits; + requestRepaint(); + } + + /** + * Sets the 'bottom' attribute; distance from the bottom of the + * component to the bottom edge of the layout. + * + * @param bottomValue + * The value of the 'bottom' attribute + * @param units + * The unit of the 'bottom' attribute. See UNIT_SYMBOLS for a + * description of the available units. + */ + public void setBottom(Float bottomValue, Unit bottomUnits) { + this.bottomValue = bottomValue; + this.bottomUnits = bottomUnits; + requestRepaint(); + } + + /** + * Sets the 'left' attribute; distance from the left of the component to + * the left edge of the layout. + * + * @param leftValue + * The value of the 'left' attribute + * @param units + * The unit of the 'left' attribute. See UNIT_SYMBOLS for a + * description of the available units. + */ + public void setLeft(Float leftValue, Unit leftUnits) { + this.leftValue = leftValue; + this.leftUnits = leftUnits; + requestRepaint(); + } + + /** + * Sets the 'z-index' attribute; the visual stacking order + * + * @param zIndex + * The z-index for the component. + */ + public void setZIndex(int zIndex) { + this.zIndex = zIndex; + requestRepaint(); + } + + /** + * Sets the value of the 'top' attribute; distance from the top of the + * component to the top edge of the layout. + * + * @param topValue + * The value of the 'left' attribute + */ + public void setTopValue(Float topValue) { + this.topValue = topValue; + requestRepaint(); + } + + /** + * Gets the 'top' attributes value in current units. + * + * @see #getTopUnits() + * @return The value of the 'top' attribute, null if not set + */ + public Float getTopValue() { + return topValue; + } + + /** + * Gets the 'right' attributes value in current units. + * + * @return The value of the 'right' attribute, null if not set + * @see #getRightUnits() + */ + public Float getRightValue() { + return rightValue; + } + + /** + * Sets the 'right' attribute value (distance from the right of the + * component to the right edge of the layout). Currently active units + * are maintained. + * + * @param rightValue + * The value of the 'right' attribute + * @see #setRightUnits(int) + */ + public void setRightValue(Float rightValue) { + this.rightValue = rightValue; + requestRepaint(); + } + + /** + * Gets the 'bottom' attributes value using current units. + * + * @return The value of the 'bottom' attribute, null if not set + * @see #getBottomUnits() + */ + public Float getBottomValue() { + return bottomValue; + } + + /** + * Sets the 'bottom' attribute value (distance from the bottom of the + * component to the bottom edge of the layout). Currently active units + * are maintained. + * + * @param bottomValue + * The value of the 'bottom' attribute + * @see #setBottomUnits(int) + */ + public void setBottomValue(Float bottomValue) { + this.bottomValue = bottomValue; + requestRepaint(); + } + + /** + * Gets the 'left' attributes value using current units. + * + * @return The value of the 'left' attribute, null if not set + * @see #getLeftUnits() + */ + public Float getLeftValue() { + return leftValue; + } + + /** + * Sets the 'left' attribute value (distance from the left of the + * component to the left edge of the layout). Currently active units are + * maintained. + * + * @param leftValue + * The value of the 'left' CSS-attribute + * @see #setLeftUnits(int) + */ + public void setLeftValue(Float leftValue) { + this.leftValue = leftValue; + requestRepaint(); + } + + /** + * Gets the unit for the 'top' attribute + * + * @return See {@link Sizeable} UNIT_SYMBOLS for a description of the + * available units. + */ + public Unit getTopUnits() { + return topUnits; + } + + /** + * Sets the unit for the 'top' attribute + * + * @param topUnits + * See {@link Sizeable} UNIT_SYMBOLS for a description of the + * available units. + */ + public void setTopUnits(Unit topUnits) { + this.topUnits = topUnits; + requestRepaint(); + } + + /** + * Gets the unit for the 'right' attribute + * + * @return See {@link Sizeable} UNIT_SYMBOLS for a description of the + * available units. + */ + public Unit getRightUnits() { + return rightUnits; + } + + /** + * Sets the unit for the 'right' attribute + * + * @param rightUnits + * See {@link Sizeable} UNIT_SYMBOLS for a description of the + * available units. + */ + public void setRightUnits(Unit rightUnits) { + this.rightUnits = rightUnits; + requestRepaint(); + } + + /** + * Gets the unit for the 'bottom' attribute + * + * @return See {@link Sizeable} UNIT_SYMBOLS for a description of the + * available units. + */ + public Unit getBottomUnits() { + return bottomUnits; + } + + /** + * Sets the unit for the 'bottom' attribute + * + * @param bottomUnits + * See {@link Sizeable} UNIT_SYMBOLS for a description of the + * available units. + */ + public void setBottomUnits(Unit bottomUnits) { + this.bottomUnits = bottomUnits; + requestRepaint(); + } + + /** + * Gets the unit for the 'left' attribute + * + * @return See {@link Sizeable} UNIT_SYMBOLS for a description of the + * available units. + */ + public Unit getLeftUnits() { + return leftUnits; + } + + /** + * Sets the unit for the 'left' attribute + * + * @param leftUnits + * See {@link Sizeable} UNIT_SYMBOLS for a description of the + * available units. + */ + public void setLeftUnits(Unit leftUnits) { + this.leftUnits = leftUnits; + requestRepaint(); + } + + /** + * Gets the 'z-index' attribute. + * + * @return the zIndex The z-index attribute + */ + public int getZIndex() { + return zIndex; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return getCSSString(); + } + + } + + @Override + public void addListener(LayoutClickListener listener) { + addListener(LayoutClickEventHandler.LAYOUT_CLICK_EVENT_IDENTIFIER, + LayoutClickEvent.class, listener, + LayoutClickListener.clickMethod); + } + + @Override + public void removeListener(LayoutClickListener listener) { + removeListener(LayoutClickEventHandler.LAYOUT_CLICK_EVENT_IDENTIFIER, + LayoutClickEvent.class, listener); + } + +} diff --git a/server/src/com/vaadin/ui/AbstractComponent.java b/server/src/com/vaadin/ui/AbstractComponent.java new file mode 100644 index 0000000000..e7cb38256c --- /dev/null +++ b/server/src/com/vaadin/ui/AbstractComponent.java @@ -0,0 +1,1382 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.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.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import com.vaadin.Application; +import com.vaadin.event.ActionManager; +import com.vaadin.event.EventRouter; +import com.vaadin.event.MethodEventSource; +import com.vaadin.event.ShortcutListener; +import com.vaadin.shared.ComponentState; +import com.vaadin.terminal.AbstractClientConnector; +import com.vaadin.terminal.ErrorMessage; +import com.vaadin.terminal.Resource; +import com.vaadin.terminal.Terminal; +import com.vaadin.terminal.gwt.server.ClientConnector; +import com.vaadin.terminal.gwt.server.ComponentSizeValidator; +import com.vaadin.terminal.gwt.server.ResourceReference; +import com.vaadin.tools.ReflectTools; + +/** + * An abstract class that defines default implementation for the + * {@link Component} interface. Basic UI components that are not derived from an + * external component can inherit this class to easily qualify as Vaadin + * components. Most components in Vaadin do just that. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +@SuppressWarnings("serial") +public abstract class AbstractComponent extends AbstractClientConnector + implements Component, MethodEventSource { + + /* Private members */ + + /** + * Application specific data object. The component does not use or modify + * this. + */ + private Object applicationData; + + /** + * The EventRouter used for the event model. + */ + private EventRouter eventRouter = null; + + /** + * The internal error message of the component. + */ + private ErrorMessage componentError = null; + + /** + * Locale of this component. + */ + private Locale locale; + + /** + * The component should receive focus (if {@link Focusable}) when attached. + */ + private boolean delayedFocus; + + /* Sizeable fields */ + + private float width = SIZE_UNDEFINED; + private float height = SIZE_UNDEFINED; + private Unit widthUnit = Unit.PIXELS; + private Unit heightUnit = Unit.PIXELS; + private static final Pattern sizePattern = Pattern + .compile("^(-?\\d+(\\.\\d+)?)(%|px|em|ex|in|cm|mm|pt|pc)?$"); + + private ComponentErrorHandler errorHandler = null; + + /** + * Keeps track of the Actions added to this component; the actual + * handling/notifying is delegated, usually to the containing window. + */ + private ActionManager actionManager; + + /* Constructor */ + + /** + * Constructs a new Component. + */ + public AbstractComponent() { + // ComponentSizeValidator.setCreationLocation(this); + } + + /* Get/Set component properties */ + + @Override + public void setDebugId(String id) { + getState().setDebugId(id); + } + + @Override + public String getDebugId() { + return getState().getDebugId(); + } + + /** + * Gets style for component. Multiple styles are joined with spaces. + * + * @return the component's styleValue of property style. + * @deprecated Use getStyleName() instead; renamed for consistency and to + * indicate that "style" should not be used to switch client + * side implementation, only to style the component. + */ + @Deprecated + public String getStyle() { + return getStyleName(); + } + + /** + * Sets and replaces all previous style names of the component. This method + * will trigger a {@link RepaintRequestEvent}. + * + * @param style + * the new style of the component. + * @deprecated Use setStyleName() instead; renamed for consistency and to + * indicate that "style" should not be used to switch client + * side implementation, only to style the component. + */ + @Deprecated + public void setStyle(String style) { + setStyleName(style); + } + + /* + * Gets the component's style. Don't add a JavaDoc comment here, we use the + * default documentation from implemented interface. + */ + @Override + public String getStyleName() { + String s = ""; + if (getState().getStyles() != null) { + for (final Iterator<String> it = getState().getStyles().iterator(); it + .hasNext();) { + s += it.next(); + if (it.hasNext()) { + s += " "; + } + } + } + return s; + } + + /* + * Sets the component's style. Don't add a JavaDoc comment here, we use the + * default documentation from implemented interface. + */ + @Override + public void setStyleName(String style) { + if (style == null || "".equals(style)) { + getState().setStyles(null); + requestRepaint(); + return; + } + if (getState().getStyles() == null) { + getState().setStyles(new ArrayList<String>()); + } + List<String> styles = getState().getStyles(); + styles.clear(); + String[] styleParts = style.split(" +"); + for (String part : styleParts) { + if (part.length() > 0) { + styles.add(part); + } + } + requestRepaint(); + } + + @Override + public void addStyleName(String style) { + if (style == null || "".equals(style)) { + return; + } + if (style.contains(" ")) { + // Split space separated style names and add them one by one. + for (String realStyle : style.split(" ")) { + addStyleName(realStyle); + } + return; + } + + if (getState().getStyles() == null) { + getState().setStyles(new ArrayList<String>()); + } + List<String> styles = getState().getStyles(); + if (!styles.contains(style)) { + styles.add(style); + requestRepaint(); + } + } + + @Override + public void removeStyleName(String style) { + if (getState().getStyles() != null) { + String[] styleParts = style.split(" +"); + for (String part : styleParts) { + if (part.length() > 0) { + getState().getStyles().remove(part); + } + } + requestRepaint(); + } + } + + /* + * Get's the component's caption. Don't add a JavaDoc comment here, we use + * the default documentation from implemented interface. + */ + @Override + public String getCaption() { + return getState().getCaption(); + } + + /** + * Sets the component's caption <code>String</code>. Caption is the visible + * name of the component. This method will trigger a + * {@link RepaintRequestEvent}. + * + * @param caption + * the new caption <code>String</code> for the component. + */ + @Override + public void setCaption(String caption) { + getState().setCaption(caption); + requestRepaint(); + } + + /* + * Don't add a JavaDoc comment here, we use the default documentation from + * implemented interface. + */ + @Override + public Locale getLocale() { + if (locale != null) { + return locale; + } + HasComponents parent = getParent(); + if (parent != null) { + return parent.getLocale(); + } + final Application app = getApplication(); + if (app != null) { + return app.getLocale(); + } + return null; + } + + /** + * Sets the locale of this component. + * + * <pre> + * // Component for which the locale is meaningful + * InlineDateField date = new InlineDateField("Datum"); + * + * // German language specified with ISO 639-1 language + * // code and ISO 3166-1 alpha-2 country code. + * date.setLocale(new Locale("de", "DE")); + * + * date.setResolution(DateField.RESOLUTION_DAY); + * layout.addComponent(date); + * </pre> + * + * + * @param locale + * the locale to become this component's locale. + */ + public void setLocale(Locale locale) { + this.locale = locale; + + // FIXME: Reload value if there is a converter + requestRepaint(); + } + + /* + * Gets the component's icon resource. Don't add a JavaDoc comment here, we + * use the default documentation from implemented interface. + */ + @Override + public Resource getIcon() { + return ResourceReference.getResource(getState().getIcon()); + } + + /** + * Sets the component's icon. This method will trigger a + * {@link RepaintRequestEvent}. + * + * @param icon + * the icon to be shown with the component's caption. + */ + @Override + public void setIcon(Resource icon) { + getState().setIcon(ResourceReference.create(icon)); + requestRepaint(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.Component#isEnabled() + */ + @Override + public boolean isEnabled() { + return getState().isEnabled(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.Component#setEnabled(boolean) + */ + @Override + public void setEnabled(boolean enabled) { + if (getState().isEnabled() != enabled) { + getState().setEnabled(enabled); + requestRepaint(); + } + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.terminal.gwt.client.Connector#isConnectorEnabled() + */ + @Override + public boolean isConnectorEnabled() { + if (!isVisible()) { + return false; + } else if (!isEnabled()) { + return false; + } else if (!super.isConnectorEnabled()) { + return false; + } else if (!getParent().isComponentVisible(this)) { + return false; + } else { + return true; + } + } + + /* + * Tests if the component is in the immediate mode. Don't add a JavaDoc + * comment here, we use the default documentation from implemented + * interface. + */ + public boolean isImmediate() { + return getState().isImmediate(); + } + + /** + * Sets the component's immediate mode to the specified status. This method + * will trigger a {@link RepaintRequestEvent}. + * + * @param immediate + * the boolean value specifying if the component should be in the + * immediate mode after the call. + * @see Component#isImmediate() + */ + public void setImmediate(boolean immediate) { + getState().setImmediate(immediate); + requestRepaint(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.Component#isVisible() + */ + @Override + public boolean isVisible() { + return getState().isVisible(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.Component#setVisible(boolean) + */ + @Override + public void setVisible(boolean visible) { + if (getState().isVisible() == visible) { + return; + } + + getState().setVisible(visible); + requestRepaint(); + if (getParent() != null) { + // Must always repaint the parent (at least the hierarchy) when + // visibility of a child component changes. + getParent().requestRepaint(); + } + } + + /** + * <p> + * Gets the component's description, used in tooltips and can be displayed + * directly in certain other components such as forms. The description can + * be used to briefly describe the state of the component to the user. The + * description string may contain certain XML tags: + * </p> + * + * <p> + * <table border=1> + * <tr> + * <td width=120><b>Tag</b></td> + * <td width=120><b>Description</b></td> + * <td width=120><b>Example</b></td> + * </tr> + * <tr> + * <td><b></td> + * <td>bold</td> + * <td><b>bold text</b></td> + * </tr> + * <tr> + * <td><i></td> + * <td>italic</td> + * <td><i>italic text</i></td> + * </tr> + * <tr> + * <td><u></td> + * <td>underlined</td> + * <td><u>underlined text</u></td> + * </tr> + * <tr> + * <td><br></td> + * <td>linebreak</td> + * <td>N/A</td> + * </tr> + * <tr> + * <td><ul><br> + * <li>item1<br> + * <li>item1<br> + * </ul></td> + * <td>item list</td> + * <td> + * <ul> + * <li>item1 + * <li>item2 + * </ul> + * </td> + * </tr> + * </table> + * </p> + * + * <p> + * These tags may be nested. + * </p> + * + * @return component's description <code>String</code> + */ + public String getDescription() { + return getState().getDescription(); + } + + /** + * Sets the component's description. See {@link #getDescription()} for more + * information on what the description is. This method will trigger a + * {@link RepaintRequestEvent}. + * + * The description is displayed as HTML/XHTML in tooltips or directly in + * certain components so care should be taken to avoid creating the + * possibility for HTML injection and possibly XSS vulnerabilities. + * + * @param description + * the new description string for the component. + */ + public void setDescription(String description) { + getState().setDescription(description); + requestRepaint(); + } + + /* + * Gets the component's parent component. Don't add a JavaDoc comment here, + * we use the default documentation from implemented interface. + */ + @Override + public HasComponents getParent() { + return (HasComponents) super.getParent(); + } + + @Override + public void setParent(ClientConnector parent) { + if (parent == null || parent instanceof HasComponents) { + super.setParent(parent); + } else { + throw new IllegalArgumentException( + "The parent of a Component must implement HasComponents, which " + + parent.getClass() + " doesn't do."); + } + } + + /** + * Returns the closest ancestor with the given type. + * <p> + * To find the Window that contains the component, use {@code Window w = + * getParent(Window.class);} + * </p> + * + * @param <T> + * The type of the ancestor + * @param parentType + * The ancestor class we are looking for + * @return The first ancestor that can be assigned to the given class. Null + * if no ancestor with the correct type could be found. + */ + public <T extends HasComponents> T findAncestor(Class<T> parentType) { + HasComponents p = getParent(); + while (p != null) { + if (parentType.isAssignableFrom(p.getClass())) { + return parentType.cast(p); + } + p = p.getParent(); + } + return null; + } + + /** + * Gets the error message for this component. + * + * @return ErrorMessage containing the description of the error state of the + * component or null, if the component contains no errors. Extending + * classes should override this method if they support other error + * message types such as validation errors or buffering errors. The + * returned error message contains information about all the errors. + */ + public ErrorMessage getErrorMessage() { + return componentError; + } + + /** + * Gets the component's error message. + * + * @link Terminal.ErrorMessage#ErrorMessage(String, int) + * + * @return the component's error message. + */ + public ErrorMessage getComponentError() { + return componentError; + } + + /** + * Sets the component's error message. The message may contain certain XML + * tags, for more information see + * + * @link Component.ErrorMessage#ErrorMessage(String, int) + * + * @param componentError + * the new <code>ErrorMessage</code> of the component. + */ + public void setComponentError(ErrorMessage componentError) { + this.componentError = componentError; + fireComponentErrorEvent(); + requestRepaint(); + } + + /* + * Tests if the component is in read-only mode. Don't add a JavaDoc comment + * here, we use the default documentation from implemented interface. + */ + @Override + public boolean isReadOnly() { + return getState().isReadOnly(); + } + + /* + * Sets the component's read-only mode. Don't add a JavaDoc comment here, we + * use the default documentation from implemented interface. + */ + @Override + public void setReadOnly(boolean readOnly) { + getState().setReadOnly(readOnly); + requestRepaint(); + } + + /* + * Gets the parent window of the component. Don't add a JavaDoc comment + * here, we use the default documentation from implemented interface. + */ + @Override + public Root getRoot() { + // Just make method from implemented Component interface public + return super.getRoot(); + } + + /* + * Notify the component that it's attached to a window. Don't add a JavaDoc + * comment here, we use the default documentation from implemented + * interface. + */ + @Override + public void attach() { + super.attach(); + if (delayedFocus) { + focus(); + } + setActionManagerViewer(); + } + + /* + * Detach the component from application. Don't add a JavaDoc comment here, + * we use the default documentation from implemented interface. + */ + @Override + public void detach() { + super.detach(); + if (actionManager != null) { + // Remove any existing viewer. Root cast is just to make the + // compiler happy + actionManager.setViewer((Root) null); + } + } + + /** + * Sets the focus for this component if the component is {@link Focusable}. + */ + protected void focus() { + if (this instanceof Focusable) { + final Application app = getApplication(); + if (app != null) { + getRoot().setFocusedComponent((Focusable) this); + delayedFocus = false; + } else { + delayedFocus = true; + } + } + } + + /** + * Gets the application object to which the component is attached. + * + * <p> + * The method will return {@code null} if the component is not currently + * attached to an application. This is often a problem in constructors of + * regular components and in the initializers of custom composite + * components. A standard workaround is to move the problematic + * initialization to {@link #attach()}, as described in the documentation of + * the method. + * </p> + * <p> + * <b>This method is not meant to be overridden. Due to CDI requirements we + * cannot declare it as final even though it should be final.</b> + * </p> + * + * @return the parent application of the component or <code>null</code>. + * @see #attach() + */ + @Override + public Application getApplication() { + // Just make method inherited from Component interface public + return super.getApplication(); + } + + /** + * Build CSS compatible string representation of height. + * + * @return CSS height + */ + private String getCSSHeight() { + if (getHeightUnits() == Unit.PIXELS) { + return ((int) getHeight()) + getHeightUnits().getSymbol(); + } else { + return getHeight() + getHeightUnits().getSymbol(); + } + } + + /** + * Build CSS compatible string representation of width. + * + * @return CSS width + */ + private String getCSSWidth() { + if (getWidthUnits() == Unit.PIXELS) { + return ((int) getWidth()) + getWidthUnits().getSymbol(); + } else { + return getWidth() + getWidthUnits().getSymbol(); + } + } + + /** + * Returns the shared state bean with information to be sent from the server + * to the client. + * + * Subclasses should override this method and set any relevant fields of the + * state returned by super.getState(). + * + * @since 7.0 + * + * @return updated component shared state + */ + @Override + public ComponentState getState() { + return (ComponentState) super.getState(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.Component#updateState() + */ + @Override + public void updateState() { + // TODO This logic should be on the client side and the state should + // simply be a data object with "width" and "height". + if (getHeight() >= 0 + && (getHeightUnits() != Unit.PERCENTAGE || ComponentSizeValidator + .parentCanDefineHeight(this))) { + getState().setHeight("" + getCSSHeight()); + } else { + getState().setHeight(""); + } + + if (getWidth() >= 0 + && (getWidthUnits() != Unit.PERCENTAGE || ComponentSizeValidator + .parentCanDefineWidth(this))) { + getState().setWidth("" + getCSSWidth()); + } else { + getState().setWidth(""); + } + + ErrorMessage error = getErrorMessage(); + if (null != error) { + getState().setErrorMessage(error.getFormattedHtmlMessage()); + } else { + getState().setErrorMessage(null); + } + } + + /* Documentation copied from interface */ + @Override + public void requestRepaint() { + // Invisible components (by flag in this particular component) do not + // need repaints + if (!getState().isVisible()) { + return; + } + super.requestRepaint(); + } + + /* General event framework */ + + private static final Method COMPONENT_EVENT_METHOD = ReflectTools + .findMethod(Component.Listener.class, "componentEvent", + Component.Event.class); + + /** + * <p> + * Registers a new listener with the specified activation method to listen + * events generated by this component. If the activation method does not + * have any arguments the event object will not be passed to it when it's + * called. + * </p> + * + * <p> + * This method additionally informs the event-api to route events with the + * given eventIdentifier to the components handleEvent function call. + * </p> + * + * <p> + * For more information on the inheritable event mechanism see the + * {@link com.vaadin.event com.vaadin.event package documentation}. + * </p> + * + * @param eventIdentifier + * the identifier of the event to listen for + * @param eventType + * the type of the listened event. Events of this type or its + * subclasses activate the listener. + * @param target + * the object instance who owns the activation method. + * @param method + * the activation method. + * + * @since 6.2 + */ + protected void addListener(String eventIdentifier, Class<?> eventType, + Object target, Method method) { + if (eventRouter == null) { + eventRouter = new EventRouter(); + } + boolean needRepaint = !eventRouter.hasListeners(eventType); + eventRouter.addListener(eventType, target, method); + + if (needRepaint) { + getState().addRegisteredEventListener(eventIdentifier); + requestRepaint(); + } + } + + /** + * Checks if the given {@link Event} type is listened for this component. + * + * @param eventType + * the event type to be checked + * @return true if a listener is registered for the given event type + */ + protected boolean hasListeners(Class<?> eventType) { + return eventRouter != null && eventRouter.hasListeners(eventType); + } + + /** + * Removes all registered listeners matching the given parameters. Since + * this method receives the event type and the listener object as + * parameters, it will unregister all <code>object</code>'s methods that are + * registered to listen to events of type <code>eventType</code> generated + * by this component. + * + * <p> + * This method additionally informs the event-api to stop routing events + * with the given eventIdentifier to the components handleEvent function + * call. + * </p> + * + * <p> + * For more information on the inheritable event mechanism see the + * {@link com.vaadin.event com.vaadin.event package documentation}. + * </p> + * + * @param eventIdentifier + * the identifier of the event to stop listening for + * @param eventType + * the exact event type the <code>object</code> listens to. + * @param target + * the target object that has registered to listen to events of + * type <code>eventType</code> with one or more methods. + * + * @since 6.2 + */ + protected void removeListener(String eventIdentifier, Class<?> eventType, + Object target) { + if (eventRouter != null) { + eventRouter.removeListener(eventType, target); + if (!eventRouter.hasListeners(eventType)) { + getState().removeRegisteredEventListener(eventIdentifier); + requestRepaint(); + } + } + } + + /** + * <p> + * Registers a new listener with the specified activation method to listen + * events generated by this component. If the activation method does not + * have any arguments the event object will not be passed to it when it's + * called. + * </p> + * + * <p> + * For more information on the inheritable event mechanism see the + * {@link com.vaadin.event com.vaadin.event package documentation}. + * </p> + * + * @param eventType + * the type of the listened event. Events of this type or its + * subclasses activate the listener. + * @param target + * the object instance who owns the activation method. + * @param method + * the activation method. + */ + @Override + public void addListener(Class<?> eventType, Object target, Method method) { + if (eventRouter == null) { + eventRouter = new EventRouter(); + } + eventRouter.addListener(eventType, target, method); + } + + /** + * <p> + * Convenience method for registering a new listener with the specified + * activation method to listen events generated by this component. If the + * activation method does not have any arguments the event object will not + * be passed to it when it's called. + * </p> + * + * <p> + * This version of <code>addListener</code> gets the name of the activation + * method as a parameter. The actual method is reflected from + * <code>object</code>, and unless exactly one match is found, + * <code>java.lang.IllegalArgumentException</code> is thrown. + * </p> + * + * <p> + * For more information on the inheritable event mechanism see the + * {@link com.vaadin.event com.vaadin.event package documentation}. + * </p> + * + * <p> + * Note: Using this method is discouraged because it cannot be checked + * during compilation. Use {@link #addListener(Class, Object, Method)} or + * {@link #addListener(com.vaadin.ui.Component.Listener)} instead. + * </p> + * + * @param eventType + * the type of the listened event. Events of this type or its + * subclasses activate the listener. + * @param target + * the object instance who owns the activation method. + * @param methodName + * the name of the activation method. + */ + @Override + public void addListener(Class<?> eventType, Object target, String methodName) { + if (eventRouter == null) { + eventRouter = new EventRouter(); + } + eventRouter.addListener(eventType, target, methodName); + } + + /** + * Removes all registered listeners matching the given parameters. Since + * this method receives the event type and the listener object as + * parameters, it will unregister all <code>object</code>'s methods that are + * registered to listen to events of type <code>eventType</code> generated + * by this component. + * + * <p> + * For more information on the inheritable event mechanism see the + * {@link com.vaadin.event com.vaadin.event package documentation}. + * </p> + * + * @param eventType + * the exact event type the <code>object</code> listens to. + * @param target + * the target object that has registered to listen to events of + * type <code>eventType</code> with one or more methods. + */ + @Override + public void removeListener(Class<?> eventType, Object target) { + if (eventRouter != null) { + eventRouter.removeListener(eventType, target); + } + } + + /** + * Removes one registered listener method. The given method owned by the + * given object will no longer be called when the specified events are + * generated by this component. + * + * <p> + * For more information on the inheritable event mechanism see the + * {@link com.vaadin.event com.vaadin.event package documentation}. + * </p> + * + * @param eventType + * the exact event type the <code>object</code> listens to. + * @param target + * target object that has registered to listen to events of type + * <code>eventType</code> with one or more methods. + * @param method + * the method owned by <code>target</code> that's registered to + * listen to events of type <code>eventType</code>. + */ + @Override + public void removeListener(Class<?> eventType, Object target, Method method) { + if (eventRouter != null) { + eventRouter.removeListener(eventType, target, method); + } + } + + /** + * <p> + * Removes one registered listener method. The given method owned by the + * given object will no longer be called when the specified events are + * generated by this component. + * </p> + * + * <p> + * This version of <code>removeListener</code> gets the name of the + * activation method as a parameter. The actual method is reflected from + * <code>target</code>, and unless exactly one match is found, + * <code>java.lang.IllegalArgumentException</code> is thrown. + * </p> + * + * <p> + * For more information on the inheritable event mechanism see the + * {@link com.vaadin.event com.vaadin.event package documentation}. + * </p> + * + * @param eventType + * the exact event type the <code>object</code> listens to. + * @param target + * the target object that has registered to listen to events of + * type <code>eventType</code> with one or more methods. + * @param methodName + * the name of the method owned by <code>target</code> that's + * registered to listen to events of type <code>eventType</code>. + */ + @Override + public void removeListener(Class<?> eventType, Object target, + String methodName) { + if (eventRouter != null) { + eventRouter.removeListener(eventType, target, methodName); + } + } + + /** + * Returns all listeners that are registered for the given event type or one + * of its subclasses. + * + * @param eventType + * The type of event to return listeners for. + * @return A collection with all registered listeners. Empty if no listeners + * are found. + */ + public Collection<?> getListeners(Class<?> eventType) { + if (eventRouter == null) { + return Collections.EMPTY_LIST; + } + + return eventRouter.getListeners(eventType); + } + + /** + * Sends the event to all listeners. + * + * @param event + * the Event to be sent to all listeners. + */ + protected void fireEvent(Component.Event event) { + if (eventRouter != null) { + eventRouter.fireEvent(event); + } + + } + + /* Component event framework */ + + /* + * Registers a new listener to listen events generated by this component. + * Don't add a JavaDoc comment here, we use the default documentation from + * implemented interface. + */ + @Override + public void addListener(Component.Listener listener) { + addListener(Component.Event.class, listener, COMPONENT_EVENT_METHOD); + } + + /* + * Removes a previously registered listener from this component. Don't add a + * JavaDoc comment here, we use the default documentation from implemented + * interface. + */ + @Override + public void removeListener(Component.Listener listener) { + removeListener(Component.Event.class, listener, COMPONENT_EVENT_METHOD); + } + + /** + * Emits the component event. It is transmitted to all registered listeners + * interested in such events. + */ + protected void fireComponentEvent() { + fireEvent(new Component.Event(this)); + } + + /** + * Emits the component error event. It is transmitted to all registered + * listeners interested in such events. + */ + protected void fireComponentErrorEvent() { + fireEvent(new Component.ErrorEvent(getComponentError(), this)); + } + + /** + * Sets the data object, that can be used for any application specific data. + * The component does not use or modify this data. + * + * @param data + * the Application specific data. + * @since 3.1 + */ + public void setData(Object data) { + applicationData = data; + } + + /** + * Gets the application specific data. See {@link #setData(Object)}. + * + * @return the Application specific data set with setData function. + * @since 3.1 + */ + public Object getData() { + return applicationData; + } + + /* Sizeable and other size related methods */ + + /* + * (non-Javadoc) + * + * @see com.vaadin.terminal.Sizeable#getHeight() + */ + @Override + public float getHeight() { + return height; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.terminal.Sizeable#getHeightUnits() + */ + @Override + public Unit getHeightUnits() { + return heightUnit; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.terminal.Sizeable#getWidth() + */ + @Override + public float getWidth() { + return width; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.terminal.Sizeable#getWidthUnits() + */ + @Override + public Unit getWidthUnits() { + return widthUnit; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.terminal.Sizeable#setHeight(float, Unit) + */ + @Override + public void setHeight(float height, Unit unit) { + if (unit == null) { + throw new IllegalArgumentException("Unit can not be null"); + } + this.height = height; + heightUnit = unit; + requestRepaint(); + // ComponentSizeValidator.setHeightLocation(this); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.terminal.Sizeable#setSizeFull() + */ + @Override + public void setSizeFull() { + setWidth(100, Unit.PERCENTAGE); + setHeight(100, Unit.PERCENTAGE); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.terminal.Sizeable#setSizeUndefined() + */ + @Override + public void setSizeUndefined() { + setWidth(-1, Unit.PIXELS); + setHeight(-1, Unit.PIXELS); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.terminal.Sizeable#setWidth(float, Unit) + */ + @Override + public void setWidth(float width, Unit unit) { + if (unit == null) { + throw new IllegalArgumentException("Unit can not be null"); + } + this.width = width; + widthUnit = unit; + requestRepaint(); + // ComponentSizeValidator.setWidthLocation(this); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.terminal.Sizeable#setWidth(java.lang.String) + */ + @Override + public void setWidth(String width) { + Size size = parseStringSize(width); + if (size != null) { + setWidth(size.getSize(), size.getUnit()); + } else { + setWidth(-1, Unit.PIXELS); + } + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.terminal.Sizeable#setHeight(java.lang.String) + */ + @Override + public void setHeight(String height) { + Size size = parseStringSize(height); + if (size != null) { + setHeight(size.getSize(), size.getUnit()); + } else { + setHeight(-1, Unit.PIXELS); + } + } + + /* + * Returns array with size in index 0 unit in index 1. Null or empty string + * will produce {-1,Unit#PIXELS} + */ + private static Size parseStringSize(String s) { + if (s == null) { + return null; + } + s = s.trim(); + if ("".equals(s)) { + return null; + } + float size = 0; + Unit unit = null; + Matcher matcher = sizePattern.matcher(s); + if (matcher.find()) { + size = Float.parseFloat(matcher.group(1)); + if (size < 0) { + size = -1; + unit = Unit.PIXELS; + } else { + String symbol = matcher.group(3); + unit = Unit.getUnitFromSymbol(symbol); + } + } else { + throw new IllegalArgumentException("Invalid size argument: \"" + s + + "\" (should match " + sizePattern.pattern() + ")"); + } + return new Size(size, unit); + } + + private static class Size implements Serializable { + float size; + Unit unit; + + public Size(float size, Unit unit) { + this.size = size; + this.unit = unit; + } + + public float getSize() { + return size; + } + + public Unit getUnit() { + return unit; + } + } + + public interface ComponentErrorEvent extends Terminal.ErrorEvent { + } + + public interface ComponentErrorHandler extends Serializable { + /** + * Handle the component error + * + * @param event + * @return True if the error has been handled False, otherwise + */ + public boolean handleComponentError(ComponentErrorEvent event); + } + + /** + * Gets the error handler for the component. + * + * The error handler is dispatched whenever there is an error processing the + * data coming from the client. + * + * @return + */ + public ComponentErrorHandler getErrorHandler() { + return errorHandler; + } + + /** + * Sets the error handler for the component. + * + * The error handler is dispatched whenever there is an error processing the + * data coming from the client. + * + * If the error handler is not set, the application error handler is used to + * handle the exception. + * + * @param errorHandler + * AbstractField specific error handler + */ + public void setErrorHandler(ComponentErrorHandler errorHandler) { + this.errorHandler = errorHandler; + } + + /** + * Handle the component error event. + * + * @param error + * Error event to handle + * @return True if the error has been handled False, otherwise. If the error + * haven't been handled by this component, it will be handled in the + * application error handler. + */ + public boolean handleError(ComponentErrorEvent error) { + if (errorHandler != null) { + return errorHandler.handleComponentError(error); + } + return false; + + } + + /* + * Actions + */ + + /** + * Gets the {@link ActionManager} used to manage the + * {@link ShortcutListener}s added to this {@link Field}. + * + * @return the ActionManager in use + */ + protected ActionManager getActionManager() { + if (actionManager == null) { + actionManager = new ActionManager(); + setActionManagerViewer(); + } + return actionManager; + } + + /** + * Set a viewer for the action manager to be the parent sub window (if the + * component is in a window) or the root (otherwise). This is still a + * simplification of the real case as this should be handled by the parent + * VOverlay (on the client side) if the component is inside an VOverlay + * component. + */ + private void setActionManagerViewer() { + if (actionManager != null && getRoot() != null) { + // Attached and has action manager + Window w = findAncestor(Window.class); + if (w != null) { + actionManager.setViewer(w); + } else { + actionManager.setViewer(getRoot()); + } + } + + } + + public void addShortcutListener(ShortcutListener shortcut) { + getActionManager().addAction(shortcut); + } + + public void removeShortcutListener(ShortcutListener shortcut) { + if (actionManager != null) { + actionManager.removeAction(shortcut); + } + } +} diff --git a/server/src/com/vaadin/ui/AbstractComponentContainer.java b/server/src/com/vaadin/ui/AbstractComponentContainer.java new file mode 100644 index 0000000000..bc27242bb8 --- /dev/null +++ b/server/src/com/vaadin/ui/AbstractComponentContainer.java @@ -0,0 +1,351 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.lang.reflect.Method; +import java.util.Collection; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; + +import com.vaadin.terminal.gwt.server.ComponentSizeValidator; + +/** + * Extension to {@link AbstractComponent} that defines the default + * implementation for the methods in {@link ComponentContainer}. Basic UI + * components that need to contain other components inherit this class to easily + * qualify as a component container. + * + * @author Vaadin Ltd + * @version + * @VERSION@ + * @since 3.0 + */ +@SuppressWarnings("serial") +public abstract class AbstractComponentContainer extends AbstractComponent + implements ComponentContainer { + + /** + * Constructs a new component container. + */ + public AbstractComponentContainer() { + super(); + } + + /** + * Removes all components from the container. This should probably be + * re-implemented in extending classes for a more powerful implementation. + */ + @Override + public void removeAllComponents() { + final LinkedList<Component> l = new LinkedList<Component>(); + + // Adds all components + for (final Iterator<Component> i = getComponentIterator(); i.hasNext();) { + l.add(i.next()); + } + + // Removes all component + for (final Iterator<Component> i = l.iterator(); i.hasNext();) { + removeComponent(i.next()); + } + } + + /* + * Moves all components from an another container into this container. Don't + * add a JavaDoc comment here, we use the default documentation from + * implemented interface. + */ + @Override + public void moveComponentsFrom(ComponentContainer source) { + final LinkedList<Component> components = new LinkedList<Component>(); + for (final Iterator<Component> i = source.getComponentIterator(); i + .hasNext();) { + components.add(i.next()); + } + + for (final Iterator<Component> i = components.iterator(); i.hasNext();) { + final Component c = i.next(); + source.removeComponent(c); + addComponent(c); + } + } + + /* Events */ + + private static final Method COMPONENT_ATTACHED_METHOD; + + private static final Method COMPONENT_DETACHED_METHOD; + + static { + try { + COMPONENT_ATTACHED_METHOD = ComponentAttachListener.class + .getDeclaredMethod("componentAttachedToContainer", + new Class[] { ComponentAttachEvent.class }); + COMPONENT_DETACHED_METHOD = ComponentDetachListener.class + .getDeclaredMethod("componentDetachedFromContainer", + new Class[] { ComponentDetachEvent.class }); + } catch (final java.lang.NoSuchMethodException e) { + // This should never happen + throw new java.lang.RuntimeException( + "Internal error finding methods in AbstractComponentContainer"); + } + } + + /* documented in interface */ + @Override + public void addListener(ComponentAttachListener listener) { + addListener(ComponentContainer.ComponentAttachEvent.class, listener, + COMPONENT_ATTACHED_METHOD); + } + + /* documented in interface */ + @Override + public void addListener(ComponentDetachListener listener) { + addListener(ComponentContainer.ComponentDetachEvent.class, listener, + COMPONENT_DETACHED_METHOD); + } + + /* documented in interface */ + @Override + public void removeListener(ComponentAttachListener listener) { + removeListener(ComponentContainer.ComponentAttachEvent.class, listener, + COMPONENT_ATTACHED_METHOD); + } + + /* documented in interface */ + @Override + public void removeListener(ComponentDetachListener listener) { + removeListener(ComponentContainer.ComponentDetachEvent.class, listener, + COMPONENT_DETACHED_METHOD); + } + + /** + * Fires the component attached event. This should be called by the + * addComponent methods after the component have been added to this + * container. + * + * @param component + * the component that has been added to this container. + */ + protected void fireComponentAttachEvent(Component component) { + fireEvent(new ComponentAttachEvent(this, component)); + } + + /** + * Fires the component detached event. This should be called by the + * removeComponent methods after the component have been removed from this + * container. + * + * @param component + * the component that has been removed from this container. + */ + protected void fireComponentDetachEvent(Component component) { + fireEvent(new ComponentDetachEvent(this, component)); + } + + /** + * This only implements the events and component parent calls. The extending + * classes must implement component list maintenance and call this method + * after component list maintenance. + * + * @see com.vaadin.ui.ComponentContainer#addComponent(Component) + */ + @Override + public void addComponent(Component c) { + if (c instanceof ComponentContainer) { + // Make sure we're not adding the component inside it's own content + for (Component parent = this; parent != null; parent = parent + .getParent()) { + if (parent == c) { + throw new IllegalArgumentException( + "Component cannot be added inside it's own content"); + } + } + } + + if (c.getParent() != null) { + // If the component already has a parent, try to remove it + ComponentContainer oldParent = (ComponentContainer) c.getParent(); + oldParent.removeComponent(c); + + } + + c.setParent(this); + fireComponentAttachEvent(c); + } + + /** + * This only implements the events and component parent calls. The extending + * classes must implement component list maintenance and call this method + * before component list maintenance. + * + * @see com.vaadin.ui.ComponentContainer#removeComponent(Component) + */ + @Override + public void removeComponent(Component c) { + if (c.getParent() == this) { + c.setParent(null); + fireComponentDetachEvent(c); + } + } + + @Override + public void setVisible(boolean visible) { + if (getState().isVisible() == visible) { + return; + } + + super.setVisible(visible); + // If the visibility state is toggled it might affect all children + // aswell, e.g. make container visible should make children visible if + // they were only hidden because the container was hidden. + requestRepaintAll(); + } + + @Override + public void setWidth(float width, Unit unit) { + /* + * child tree repaints may be needed, due to our fall back support for + * invalid relative sizes + */ + Collection<Component> dirtyChildren = null; + boolean childrenMayBecomeUndefined = false; + if (getWidth() == SIZE_UNDEFINED && width != SIZE_UNDEFINED) { + // children currently in invalid state may need repaint + dirtyChildren = getInvalidSizedChildren(false); + } else if ((width == SIZE_UNDEFINED && getWidth() != SIZE_UNDEFINED) + || (unit == Unit.PERCENTAGE + && getWidthUnits() != Unit.PERCENTAGE && !ComponentSizeValidator + .parentCanDefineWidth(this))) { + /* + * relative width children may get to invalid state if width becomes + * invalid. Width may also become invalid if units become percentage + * due to the fallback support + */ + childrenMayBecomeUndefined = true; + dirtyChildren = getInvalidSizedChildren(false); + } + super.setWidth(width, unit); + repaintChangedChildTrees(dirtyChildren, childrenMayBecomeUndefined, + false); + } + + private void repaintChangedChildTrees( + Collection<Component> invalidChildren, + boolean childrenMayBecomeUndefined, boolean vertical) { + if (childrenMayBecomeUndefined) { + Collection<Component> previouslyInvalidComponents = invalidChildren; + invalidChildren = getInvalidSizedChildren(vertical); + if (previouslyInvalidComponents != null && invalidChildren != null) { + for (Iterator<Component> iterator = invalidChildren.iterator(); iterator + .hasNext();) { + Component component = iterator.next(); + if (previouslyInvalidComponents.contains(component)) { + // still invalid don't repaint + iterator.remove(); + } + } + } + } else if (invalidChildren != null) { + Collection<Component> stillInvalidChildren = getInvalidSizedChildren(vertical); + if (stillInvalidChildren != null) { + for (Component component : stillInvalidChildren) { + // didn't become valid + invalidChildren.remove(component); + } + } + } + if (invalidChildren != null) { + repaintChildTrees(invalidChildren); + } + } + + private Collection<Component> getInvalidSizedChildren(final boolean vertical) { + HashSet<Component> components = null; + if (this instanceof Panel) { + Panel p = (Panel) this; + ComponentContainer content = p.getContent(); + boolean valid = vertical ? ComponentSizeValidator + .checkHeights(content) : ComponentSizeValidator + .checkWidths(content); + + if (!valid) { + components = new HashSet<Component>(1); + components.add(content); + } + } else { + for (Iterator<Component> componentIterator = getComponentIterator(); componentIterator + .hasNext();) { + Component component = componentIterator.next(); + boolean valid = vertical ? ComponentSizeValidator + .checkHeights(component) : ComponentSizeValidator + .checkWidths(component); + if (!valid) { + if (components == null) { + components = new HashSet<Component>(); + } + components.add(component); + } + } + } + return components; + } + + private void repaintChildTrees(Collection<Component> dirtyChildren) { + for (Component c : dirtyChildren) { + if (c instanceof ComponentContainer) { + ComponentContainer cc = (ComponentContainer) c; + cc.requestRepaintAll(); + } else { + c.requestRepaint(); + } + } + } + + @Override + public void setHeight(float height, Unit unit) { + /* + * child tree repaints may be needed, due to our fall back support for + * invalid relative sizes + */ + Collection<Component> dirtyChildren = null; + boolean childrenMayBecomeUndefined = false; + if (getHeight() == SIZE_UNDEFINED && height != SIZE_UNDEFINED) { + // children currently in invalid state may need repaint + dirtyChildren = getInvalidSizedChildren(true); + } else if ((height == SIZE_UNDEFINED && getHeight() != SIZE_UNDEFINED) + || (unit == Unit.PERCENTAGE + && getHeightUnits() != Unit.PERCENTAGE && !ComponentSizeValidator + .parentCanDefineHeight(this))) { + /* + * relative height children may get to invalid state if height + * becomes invalid. Height may also become invalid if units become + * percentage due to the fallback support. + */ + childrenMayBecomeUndefined = true; + dirtyChildren = getInvalidSizedChildren(true); + } + super.setHeight(height, unit); + repaintChangedChildTrees(dirtyChildren, childrenMayBecomeUndefined, + true); + } + + @Override + public Iterator<Component> iterator() { + return getComponentIterator(); + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.ui.HasComponents#isComponentVisible(com.vaadin.ui.Component) + */ + @Override + public boolean isComponentVisible(Component childComponent) { + return true; + } +}
\ No newline at end of file diff --git a/server/src/com/vaadin/ui/AbstractField.java b/server/src/com/vaadin/ui/AbstractField.java new file mode 100644 index 0000000000..6fe7f54df5 --- /dev/null +++ b/server/src/com/vaadin/ui/AbstractField.java @@ -0,0 +1,1657 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.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.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.logging.Logger; + +import com.vaadin.data.Buffered; +import com.vaadin.data.Property; +import com.vaadin.data.Validatable; +import com.vaadin.data.Validator; +import com.vaadin.data.Validator.InvalidValueException; +import com.vaadin.data.util.converter.Converter; +import com.vaadin.data.util.converter.Converter.ConversionException; +import com.vaadin.data.util.converter.ConverterUtil; +import com.vaadin.event.Action; +import com.vaadin.event.ShortcutAction; +import com.vaadin.event.ShortcutListener; +import com.vaadin.shared.AbstractFieldState; +import com.vaadin.terminal.AbstractErrorMessage; +import com.vaadin.terminal.CompositeErrorMessage; +import com.vaadin.terminal.ErrorMessage; + +/** + * <p> + * Abstract field component for implementing buffered property editors. The + * field may hold an internal value, or it may be connected to any data source + * that implements the {@link com.vaadin.data.Property}interface. + * <code>AbstractField</code> implements that interface itself, too, so + * accessing the Property value represented by it is straightforward. + * </p> + * + * <p> + * AbstractField also provides the {@link com.vaadin.data.Buffered} interface + * for buffering the data source value. By default the Field is in write + * through-mode and {@link #setWriteThrough(boolean)}should be called to enable + * buffering. + * </p> + * + * <p> + * The class also supports {@link com.vaadin.data.Validator validators} to make + * sure the value contained in the field is valid. + * </p> + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +@SuppressWarnings("serial") +public abstract class AbstractField<T> extends AbstractComponent implements + Field<T>, Property.ReadOnlyStatusChangeListener, + Property.ReadOnlyStatusChangeNotifier, Action.ShortcutNotifier { + + /* Private members */ + + private static final Logger logger = Logger.getLogger(AbstractField.class + .getName()); + + /** + * Value of the abstract field. + */ + private T value; + + /** + * A converter used to convert from the data model type to the field type + * and vice versa. + */ + private Converter<T, Object> converter = null; + /** + * Connected data-source. + */ + private Property<?> dataSource = null; + + /** + * The list of validators. + */ + private LinkedList<Validator> validators = null; + + /** + * Auto commit mode. + */ + private boolean writeThroughMode = true; + + /** + * Reads the value from data-source, when it is not modified. + */ + private boolean readThroughMode = true; + + /** + * Flag to indicate that the field is currently committing its value to the + * datasource. + */ + private boolean committingValueToDataSource = false; + + /** + * Current source exception. + */ + private Buffered.SourceException currentBufferedSourceException = null; + + /** + * Are the invalid values allowed in fields ? + */ + private boolean invalidAllowed = true; + + /** + * Are the invalid values committed ? + */ + private boolean invalidCommitted = false; + + /** + * The error message for the exception that is thrown when the field is + * required but empty. + */ + private String requiredError = ""; + + /** + * The error message that is shown when the field value cannot be converted. + */ + private String conversionError = "Could not convert value to {0}"; + + /** + * Is automatic validation enabled. + */ + private boolean validationVisible = true; + + private boolean valueWasModifiedByDataSourceDuringCommit; + + /** + * Whether this field is currently registered as listening to events from + * its data source. + * + * @see #setPropertyDataSource(Property) + * @see #addPropertyListeners() + * @see #removePropertyListeners() + */ + private boolean isListeningToPropertyEvents = false; + + /* Component basics */ + + /* + * Paints the field. Don't add a JavaDoc comment here, we use the default + * documentation from the implemented interface. + */ + + /** + * Returns true if the error indicator be hidden when painting the component + * even when there are errors. + * + * This is a mostly internal method, but can be overridden in subclasses + * e.g. if the error indicator should also be shown for empty fields in some + * cases. + * + * @return true to hide the error indicator, false to use the normal logic + * to show it when there are errors + */ + protected boolean shouldHideErrors() { + // getErrorMessage() can still return something else than null based on + // validation etc. + return isRequired() && isEmpty() && getComponentError() == null; + } + + /** + * Returns the type of the Field. The methods <code>getValue</code> and + * <code>setValue</code> must be compatible with this type: one must be able + * to safely cast the value returned from <code>getValue</code> to the given + * type and pass any variable assignable to this type as an argument to + * <code>setValue</code>. + * + * @return the type of the Field + */ + @Override + public abstract Class<? extends T> getType(); + + /** + * The abstract field is read only also if the data source is in read only + * mode. + */ + @Override + public boolean isReadOnly() { + return super.isReadOnly() + || (dataSource != null && dataSource.isReadOnly()); + } + + /** + * Changes the readonly state and throw read-only status change events. + * + * @see com.vaadin.ui.Component#setReadOnly(boolean) + */ + @Override + public void setReadOnly(boolean readOnly) { + super.setReadOnly(readOnly); + fireReadOnlyStatusChange(); + } + + /** + * Tests if the invalid data is committed to datasource. + * + * @see com.vaadin.data.BufferedValidatable#isInvalidCommitted() + */ + @Override + public boolean isInvalidCommitted() { + return invalidCommitted; + } + + /** + * Sets if the invalid data should be committed to datasource. + * + * @see com.vaadin.data.BufferedValidatable#setInvalidCommitted(boolean) + */ + @Override + public void setInvalidCommitted(boolean isCommitted) { + invalidCommitted = isCommitted; + } + + /* + * Saves the current value to the data source Don't add a JavaDoc comment + * here, we use the default documentation from the implemented interface. + */ + @Override + public void commit() throws Buffered.SourceException, InvalidValueException { + if (dataSource != null && !dataSource.isReadOnly()) { + if ((isInvalidCommitted() || isValid())) { + try { + + // Commits the value to datasource. + valueWasModifiedByDataSourceDuringCommit = false; + committingValueToDataSource = true; + getPropertyDataSource().setValue(getConvertedValue()); + } catch (final Throwable e) { + + // Sets the buffering state. + SourceException sourceException = new Buffered.SourceException( + this, e); + setCurrentBufferedSourceException(sourceException); + + // Throws the source exception. + throw sourceException; + } finally { + committingValueToDataSource = false; + } + } else { + /* An invalid value and we don't allow them, throw the exception */ + validate(); + } + } + + // The abstract field is not modified anymore + if (isModified()) { + setModified(false); + } + + // If successful, remove set the buffering state to be ok + if (getCurrentBufferedSourceException() != null) { + setCurrentBufferedSourceException(null); + } + + if (valueWasModifiedByDataSourceDuringCommit) { + valueWasModifiedByDataSourceDuringCommit = false; + fireValueChange(false); + } + + } + + /* + * Updates the value from the data source. Don't add a JavaDoc comment here, + * we use the default documentation from the implemented interface. + */ + @Override + public void discard() throws Buffered.SourceException { + if (dataSource != null) { + + // Gets the correct value from datasource + T newFieldValue; + try { + + // Discards buffer by overwriting from datasource + newFieldValue = convertFromDataSource(getDataSourceValue()); + + // If successful, remove set the buffering state to be ok + if (getCurrentBufferedSourceException() != null) { + setCurrentBufferedSourceException(null); + } + } catch (final Throwable e) { + // FIXME: What should really be done here if conversion fails? + + // Sets the buffering state + currentBufferedSourceException = new Buffered.SourceException( + this, e); + requestRepaint(); + + // Throws the source exception + throw currentBufferedSourceException; + } + + final boolean wasModified = isModified(); + setModified(false); + + // If the new value differs from the previous one + if (!equals(newFieldValue, getInternalValue())) { + setInternalValue(newFieldValue); + fireValueChange(false); + } else if (wasModified) { + // If the value did not change, but the modification status did + requestRepaint(); + } + } + } + + /** + * Gets the value from the data source. This is only here because of clarity + * in the code that handles both the data model value and the field value. + * + * @return The value of the property data source + */ + private Object getDataSourceValue() { + return dataSource.getValue(); + } + + /** + * Returns the field value. This is always identical to {@link #getValue()} + * and only here because of clarity in the code that handles both the data + * model value and the field value. + * + * @return The value of the field + */ + private T getFieldValue() { + // Give the value from abstract buffers if the field if possible + if (dataSource == null || !isReadThrough() || isModified()) { + return getInternalValue(); + } + + // There is no buffered value so use whatever the data model provides + return convertFromDataSource(getDataSourceValue()); + } + + /* + * Has the field been modified since the last commit()? Don't add a JavaDoc + * comment here, we use the default documentation from the implemented + * interface. + */ + @Override + public boolean isModified() { + return getState().isModified(); + } + + private void setModified(boolean modified) { + getState().setModified(modified); + requestRepaint(); + } + + /* + * Tests if the field is in write-through mode. Don't add a JavaDoc comment + * here, we use the default documentation from the implemented interface. + */ + @Override + public boolean isWriteThrough() { + return writeThroughMode; + } + + /** + * Sets the field's write-through mode to the specified status. When + * switching the write-through mode on, a {@link #commit()} will be + * performed. + * + * @see #setBuffered(boolean) for an easier way to control read through and + * write through modes + * + * @param writeThrough + * Boolean value to indicate if the object should be in + * write-through mode after the call. + * @throws SourceException + * If the operation fails because of an exception is thrown by + * the data source. + * @throws InvalidValueException + * If the implicit commit operation fails because of a + * validation error. + * @deprecated Use {@link #setBuffered(boolean)} instead. Note that + * setReadThrough(true), setWriteThrough(true) equals + * setBuffered(false) + */ + @Override + @Deprecated + public void setWriteThrough(boolean writeThrough) + throws Buffered.SourceException, InvalidValueException { + if (writeThroughMode == writeThrough) { + return; + } + writeThroughMode = writeThrough; + if (writeThroughMode) { + commit(); + } + } + + /* + * Tests if the field is in read-through mode. Don't add a JavaDoc comment + * here, we use the default documentation from the implemented interface. + */ + @Override + public boolean isReadThrough() { + return readThroughMode; + } + + /** + * Sets the field's read-through mode to the specified status. When + * switching read-through mode on, the object's value is updated from the + * data source. + * + * @see #setBuffered(boolean) for an easier way to control read through and + * write through modes + * + * @param readThrough + * Boolean value to indicate if the object should be in + * read-through mode after the call. + * + * @throws SourceException + * If the operation fails because of an exception is thrown by + * the data source. The cause is included in the exception. + * @deprecated Use {@link #setBuffered(boolean)} instead. Note that + * setReadThrough(true), setWriteThrough(true) equals + * setBuffered(false) + */ + @Override + @Deprecated + public void setReadThrough(boolean readThrough) + throws Buffered.SourceException { + if (readThroughMode == readThrough) { + return; + } + readThroughMode = readThrough; + if (!isModified() && readThroughMode && getPropertyDataSource() != null) { + setInternalValue(convertFromDataSource(getDataSourceValue())); + fireValueChange(false); + } + } + + /** + * Sets the buffered mode of this Field. + * <p> + * When the field is in buffered mode, changes will not be committed to the + * property data source until {@link #commit()} is called. + * </p> + * <p> + * Changing buffered mode will change the read through and write through + * state for the field. + * </p> + * <p> + * Mixing calls to {@link #setBuffered(boolean)} and + * {@link #setReadThrough(boolean)} or {@link #setWriteThrough(boolean)} is + * generally a bad idea. + * </p> + * + * @param buffered + * true if buffered mode should be turned on, false otherwise + */ + @Override + public void setBuffered(boolean buffered) { + setReadThrough(!buffered); + setWriteThrough(!buffered); + } + + /** + * Checks the buffered mode of this Field. + * <p> + * This method only returns true if both read and write buffering is used. + * + * @return true if buffered mode is on, false otherwise + */ + @Override + public boolean isBuffered() { + return !isReadThrough() && !isWriteThrough(); + } + + /* Property interface implementation */ + + /** + * Returns the (field) value converted to a String using toString(). + * + * @see java.lang.Object#toString() + * @deprecated Instead use {@link #getValue()} to get the value of the + * field, {@link #getConvertedValue()} to get the field value + * converted to the data model type or + * {@link #getPropertyDataSource()} .getValue() to get the value + * of the data source. + */ + @Deprecated + @Override + public String toString() { + logger.warning("You are using AbstractField.toString() to get the value for a " + + getClass().getSimpleName() + + ". This is not recommended and will not be supported in future versions."); + final Object value = getFieldValue(); + if (value == null) { + return null; + } + return value.toString(); + } + + /** + * Gets the current value of the field. + * + * <p> + * This is the visible, modified and possible invalid value the user have + * entered to the field. + * </p> + * + * <p> + * Note that the object returned is compatible with getType(). For example, + * if the type is String, this returns Strings even when the underlying + * datasource is of some other type. In order to access the converted value, + * use {@link #getConvertedValue()} and to access the value of the property + * data source, use {@link Property#getValue()} for the property data + * source. + * </p> + * + * <p> + * Since Vaadin 7.0, no implicit conversions between other data types and + * String are performed, but a converter is used if set. + * </p> + * + * @return the current value of the field. + */ + @Override + public T getValue() { + return getFieldValue(); + } + + /** + * Sets the value of the field. + * + * @param newFieldValue + * the New value of the field. + * @throws Property.ReadOnlyException + */ + @Override + public void setValue(Object newFieldValue) + throws Property.ReadOnlyException, Converter.ConversionException { + // This check is needed as long as setValue accepts Object instead of T + if (newFieldValue != null) { + if (!getType().isAssignableFrom(newFieldValue.getClass())) { + throw new Converter.ConversionException("Value of type " + + newFieldValue.getClass() + " cannot be assigned to " + + getType().getName()); + } + } + setValue((T) newFieldValue, false); + } + + /** + * Sets the value of the field. + * + * @param newFieldValue + * the New value of the field. + * @param repaintIsNotNeeded + * True iff caller is sure that repaint is not needed. + * @throws Property.ReadOnlyException + */ + protected void setValue(T newFieldValue, boolean repaintIsNotNeeded) + throws Property.ReadOnlyException, Converter.ConversionException, + InvalidValueException { + + if (!equals(newFieldValue, getInternalValue())) { + + // Read only fields can not be changed + if (isReadOnly()) { + throw new Property.ReadOnlyException(); + } + + // Repaint is needed even when the client thinks that it knows the + // new state if validity of the component may change + if (repaintIsNotNeeded + && (isRequired() || getValidators() != null || getConverter() != null)) { + repaintIsNotNeeded = false; + } + + if (!isInvalidAllowed()) { + /* + * If invalid values are not allowed the value must be validated + * before it is set. If validation fails, the + * InvalidValueException is thrown and the internal value is not + * updated. + */ + validate(newFieldValue); + } + + // Changes the value + setInternalValue(newFieldValue); + setModified(dataSource != null); + + valueWasModifiedByDataSourceDuringCommit = false; + // In write through mode , try to commit + if (isWriteThrough() && dataSource != null + && (isInvalidCommitted() || isValid())) { + try { + + // Commits the value to datasource + committingValueToDataSource = true; + getPropertyDataSource().setValue( + convertToModel(newFieldValue)); + + // The buffer is now unmodified + setModified(false); + + } catch (final Throwable e) { + + // Sets the buffering state + currentBufferedSourceException = new Buffered.SourceException( + this, e); + requestRepaint(); + + // Throws the source exception + throw currentBufferedSourceException; + } finally { + committingValueToDataSource = false; + } + } + + // If successful, remove set the buffering state to be ok + if (getCurrentBufferedSourceException() != null) { + setCurrentBufferedSourceException(null); + } + + if (valueWasModifiedByDataSourceDuringCommit) { + /* + * Value was modified by datasource. Force repaint even if + * repaint was not requested. + */ + valueWasModifiedByDataSourceDuringCommit = repaintIsNotNeeded = false; + } + + // Fires the value change + fireValueChange(repaintIsNotNeeded); + + } + } + + private static boolean equals(Object value1, Object value2) { + if (value1 == null) { + return value2 == null; + } + return value1.equals(value2); + } + + /* External data source */ + + /** + * Gets the current data source of the field, if any. + * + * @return the current data source as a Property, or <code>null</code> if + * none defined. + */ + @Override + public Property getPropertyDataSource() { + return dataSource; + } + + /** + * <p> + * Sets the specified Property as the data source for the field. All + * uncommitted changes are replaced with a value from the new data source. + * </p> + * + * <p> + * If the datasource has any validators, the same validators are added to + * the field. Because the default behavior of the field is to allow invalid + * values, but not to allow committing them, this only adds visual error + * messages to fields and do not allow committing them as long as the value + * is invalid. After the value is valid, the error message is not shown and + * the commit can be done normally. + * </p> + * + * <p> + * If the data source implements + * {@link com.vaadin.data.Property.ValueChangeNotifier} and/or + * {@link com.vaadin.data.Property.ReadOnlyStatusChangeNotifier}, the field + * registers itself as a listener and updates itself according to the events + * it receives. To avoid memory leaks caused by references to a field no + * longer in use, the listener registrations are removed on + * {@link AbstractField#detach() detach} and re-added on + * {@link AbstractField#attach() attach}. + * </p> + * + * <p> + * Note: before 6.5 we actually called discard() method in the beginning of + * the method. This was removed to simplify implementation, avoid excess + * calls to backing property and to avoid odd value change events that were + * previously fired (developer expects 0-1 value change events if this + * method is called). Some complex field implementations might now need to + * override this method to do housekeeping similar to discard(). + * </p> + * + * @param newDataSource + * the new data source Property. + */ + @Override + public void setPropertyDataSource(Property newDataSource) { + + // Saves the old value + final Object oldValue = getInternalValue(); + + // Stop listening to the old data source + removePropertyListeners(); + + // Sets the new data source + dataSource = newDataSource; + getState().setPropertyReadOnly( + dataSource == null ? false : dataSource.isReadOnly()); + + // Check if the current converter is compatible. + if (newDataSource != null + && !ConverterUtil.canConverterHandle(getConverter(), getType(), + newDataSource.getType())) { + // Changing from e.g. Number -> Double should set a new converter, + // changing from Double -> Number can keep the old one (Property + // accepts Number) + + // Set a new converter if there is a new data source and + // there is no old converter or the old is incompatible. + setConverter(newDataSource.getType()); + } + // Gets the value from source + try { + if (dataSource != null) { + T fieldValue = convertFromDataSource(getDataSourceValue()); + setInternalValue(fieldValue); + } + setModified(false); + if (getCurrentBufferedSourceException() != null) { + setCurrentBufferedSourceException(null); + } + } catch (final Throwable e) { + setCurrentBufferedSourceException(new Buffered.SourceException( + this, e)); + setModified(true); + } + + // Listen to new data source if possible + addPropertyListeners(); + + // Copy the validators from the data source + if (dataSource instanceof Validatable) { + final Collection<Validator> validators = ((Validatable) dataSource) + .getValidators(); + if (validators != null) { + for (final Iterator<Validator> i = validators.iterator(); i + .hasNext();) { + addValidator(i.next()); + } + } + } + + // Fires value change if the value has changed + T value = getInternalValue(); + if ((value != oldValue) + && ((value != null && !value.equals(oldValue)) || value == null)) { + fireValueChange(false); + } + } + + /** + * Retrieves a converter for the field from the converter factory defined + * for the application. Clears the converter if no application reference is + * available or if the factory returns null. + * + * @param datamodelType + * The type of the data model that we want to be able to convert + * from + */ + public void setConverter(Class<?> datamodelType) { + Converter<T, ?> c = (Converter<T, ?>) ConverterUtil.getConverter( + getType(), datamodelType, getApplication()); + setConverter(c); + } + + /** + * Convert the given value from the data source type to the UI type. + * + * @param newValue + * The data source value to convert. + * @return The converted value that is compatible with the UI type or the + * original value if its type is compatible and no converter is set. + * @throws Converter.ConversionException + * if there is no converter and the type is not compatible with + * the data source type. + */ + private T convertFromDataSource(Object newValue) { + return ConverterUtil.convertFromModel(newValue, getType(), + getConverter(), getLocale()); + } + + /** + * Convert the given value from the UI type to the data source type. + * + * @param fieldValue + * The value to convert. Typically returned by + * {@link #getFieldValue()} + * @return The converted value that is compatible with the data source type. + * @throws Converter.ConversionException + * if there is no converter and the type is not compatible with + * the data source type. + */ + private Object convertToModel(T fieldValue) + throws Converter.ConversionException { + try { + Class<?> modelType = null; + Property pd = getPropertyDataSource(); + if (pd != null) { + modelType = pd.getType(); + } else if (getConverter() != null) { + modelType = getConverter().getModelType(); + } + return ConverterUtil.convertToModel(fieldValue, + (Class<Object>) modelType, getConverter(), getLocale()); + } catch (ConversionException e) { + throw new ConversionException( + getConversionError(converter.getModelType()), e); + } + } + + /** + * Returns the conversion error with {0} replaced by the data source type. + * + * @param dataSourceType + * The type of the data source + * @return The value conversion error string with parameters replaced. + */ + protected String getConversionError(Class<?> dataSourceType) { + if (dataSourceType == null) { + return getConversionError(); + } else { + return getConversionError().replace("{0}", + dataSourceType.getSimpleName()); + } + } + + /** + * Returns the current value (as returned by {@link #getValue()}) converted + * to the data source type. + * <p> + * This returns the same as {@link AbstractField#getValue()} if no converter + * has been set. The value is not necessarily the same as the data source + * value e.g. if the field is in buffered mode and has been modified. + * </p> + * + * @return The converted value that is compatible with the data source type + */ + public Object getConvertedValue() { + return convertToModel(getFieldValue()); + } + + /** + * Sets the value of the field using a value of the data source type. The + * value given is converted to the field type and then assigned to the + * field. This will update the property data source in the same way as when + * {@link #setValue(Object)} is called. + * + * @param value + * The value to set. Must be the same type as the data source. + */ + public void setConvertedValue(Object value) { + setValue(convertFromDataSource(value)); + } + + /* Validation */ + + /** + * Adds a new validator for the field's value. All validators added to a + * field are checked each time the its value changes. + * + * @param validator + * the new validator to be added. + */ + @Override + public void addValidator(Validator validator) { + if (validators == null) { + validators = new LinkedList<Validator>(); + } + validators.add(validator); + requestRepaint(); + } + + /** + * Gets the validators of the field. + * + * @return the Unmodifiable collection that holds all validators for the + * field. + */ + @Override + public Collection<Validator> getValidators() { + if (validators == null || validators.isEmpty()) { + return null; + } + return Collections.unmodifiableCollection(validators); + } + + /** + * Removes the validator from the field. + * + * @param validator + * the validator to remove. + */ + @Override + public void removeValidator(Validator validator) { + if (validators != null) { + validators.remove(validator); + } + requestRepaint(); + } + + /** + * Removes all validators from the field. + */ + public void removeAllValidators() { + if (validators != null) { + validators.clear(); + } + requestRepaint(); + } + + /** + * Tests the current value against registered validators if the field is not + * empty. If the field is empty it is considered valid if it is not required + * and invalid otherwise. Validators are never checked for empty fields. + * + * In most cases, {@link #validate()} should be used instead of + * {@link #isValid()} to also get the error message. + * + * @return <code>true</code> if all registered validators claim that the + * current value is valid or if the field is empty and not required, + * <code>false</code> otherwise. + */ + @Override + public boolean isValid() { + + try { + validate(); + return true; + } catch (InvalidValueException e) { + return false; + } + } + + /** + * Checks the validity of the Field. + * + * A field is invalid if it is set as required (using + * {@link #setRequired(boolean)} and is empty, if one or several of the + * validators added to the field indicate it is invalid or if the value + * cannot be converted provided a converter has been set. + * + * The "required" validation is a built-in validation feature. If the field + * is required and empty this method throws an EmptyValueException with the + * error message set using {@link #setRequiredError(String)}. + * + * @see com.vaadin.data.Validatable#validate() + */ + @Override + public void validate() throws Validator.InvalidValueException { + + if (isRequired() && isEmpty()) { + throw new Validator.EmptyValueException(requiredError); + } + validate(getFieldValue()); + } + + /** + * Validates that the given value pass the validators for the field. + * <p> + * This method does not check the requiredness of the field. + * + * @param fieldValue + * The value to check + * @throws Validator.InvalidValueException + * if one or several validators fail + */ + protected void validate(T fieldValue) + throws Validator.InvalidValueException { + + Object valueToValidate = fieldValue; + + // If there is a converter we start by converting the value as we want + // to validate the converted value + if (getConverter() != null) { + try { + valueToValidate = getConverter().convertToModel(fieldValue, + getLocale()); + } catch (Exception e) { + throw new InvalidValueException( + getConversionError(getConverter().getModelType())); + } + } + + List<InvalidValueException> validationExceptions = new ArrayList<InvalidValueException>(); + if (validators != null) { + // Gets all the validation errors + for (Validator v : validators) { + try { + v.validate(valueToValidate); + } catch (final Validator.InvalidValueException e) { + validationExceptions.add(e); + } + } + } + + // If there were no errors + if (validationExceptions.isEmpty()) { + return; + } + + // If only one error occurred, throw it forwards + if (validationExceptions.size() == 1) { + throw validationExceptions.get(0); + } + + InvalidValueException[] exceptionArray = validationExceptions + .toArray(new InvalidValueException[validationExceptions.size()]); + + // Create a composite validator and include all exceptions + throw new Validator.InvalidValueException(null, exceptionArray); + } + + /** + * Fields allow invalid values by default. In most cases this is wanted, + * because the field otherwise visually forget the user input immediately. + * + * @return true iff the invalid values are allowed. + * @see com.vaadin.data.Validatable#isInvalidAllowed() + */ + @Override + public boolean isInvalidAllowed() { + return invalidAllowed; + } + + /** + * Fields allow invalid values by default. In most cases this is wanted, + * because the field otherwise visually forget the user input immediately. + * <p> + * In common setting where the user wants to assure the correctness of the + * datasource, but allow temporarily invalid contents in the field, the user + * should add the validators to datasource, that should not allow invalid + * values. The validators are automatically copied to the field when the + * datasource is set. + * </p> + * + * @see com.vaadin.data.Validatable#setInvalidAllowed(boolean) + */ + @Override + public void setInvalidAllowed(boolean invalidAllowed) + throws UnsupportedOperationException { + this.invalidAllowed = invalidAllowed; + } + + /** + * Error messages shown by the fields are composites of the error message + * thrown by the superclasses (that is the component error message), + * validation errors and buffered source errors. + * + * @see com.vaadin.ui.AbstractComponent#getErrorMessage() + */ + @Override + public ErrorMessage getErrorMessage() { + + /* + * Check validation errors only if automatic validation is enabled. + * Empty, required fields will generate a validation error containing + * the requiredError string. For these fields the exclamation mark will + * be hidden but the error must still be sent to the client. + */ + Validator.InvalidValueException validationError = null; + if (isValidationVisible()) { + try { + validate(); + } catch (Validator.InvalidValueException e) { + if (!e.isInvisible()) { + validationError = e; + } + } + } + + // Check if there are any systems errors + final ErrorMessage superError = super.getErrorMessage(); + + // Return if there are no errors at all + if (superError == null && validationError == null + && getCurrentBufferedSourceException() == null) { + return null; + } + + // Throw combination of the error types + return new CompositeErrorMessage( + new ErrorMessage[] { + superError, + AbstractErrorMessage + .getErrorMessageForException(validationError), + AbstractErrorMessage + .getErrorMessageForException(getCurrentBufferedSourceException()) }); + + } + + /* Value change events */ + + private static final Method VALUE_CHANGE_METHOD; + + static { + try { + VALUE_CHANGE_METHOD = Property.ValueChangeListener.class + .getDeclaredMethod("valueChange", + new Class[] { Property.ValueChangeEvent.class }); + } catch (final java.lang.NoSuchMethodException e) { + // This should never happen + throw new java.lang.RuntimeException( + "Internal error finding methods in AbstractField"); + } + } + + /* + * Adds a value change listener for the field. Don't add a JavaDoc comment + * here, we use the default documentation from the implemented interface. + */ + @Override + public void addListener(Property.ValueChangeListener listener) { + addListener(AbstractField.ValueChangeEvent.class, listener, + VALUE_CHANGE_METHOD); + } + + /* + * Removes a value change listener from the field. Don't add a JavaDoc + * comment here, we use the default documentation from the implemented + * interface. + */ + @Override + public void removeListener(Property.ValueChangeListener listener) { + removeListener(AbstractField.ValueChangeEvent.class, listener, + VALUE_CHANGE_METHOD); + } + + /** + * Emits the value change event. The value contained in the field is + * validated before the event is created. + */ + protected void fireValueChange(boolean repaintIsNotNeeded) { + fireEvent(new AbstractField.ValueChangeEvent(this)); + if (!repaintIsNotNeeded) { + requestRepaint(); + } + } + + /* Read-only status change events */ + + private static final Method READ_ONLY_STATUS_CHANGE_METHOD; + + static { + try { + READ_ONLY_STATUS_CHANGE_METHOD = Property.ReadOnlyStatusChangeListener.class + .getDeclaredMethod( + "readOnlyStatusChange", + new Class[] { Property.ReadOnlyStatusChangeEvent.class }); + } catch (final java.lang.NoSuchMethodException e) { + // This should never happen + throw new java.lang.RuntimeException( + "Internal error finding methods in AbstractField"); + } + } + + /** + * React to read only status changes of the property by requesting a + * repaint. + * + * @see Property.ReadOnlyStatusChangeListener + */ + @Override + public void readOnlyStatusChange(Property.ReadOnlyStatusChangeEvent event) { + getState().setPropertyReadOnly(event.getProperty().isReadOnly()); + requestRepaint(); + } + + /** + * An <code>Event</code> object specifying the Property whose read-only + * status has changed. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ + public static class ReadOnlyStatusChangeEvent extends Component.Event + implements Property.ReadOnlyStatusChangeEvent, Serializable { + + /** + * New instance of text change event. + * + * @param source + * the Source of the event. + */ + public ReadOnlyStatusChangeEvent(AbstractField source) { + super(source); + } + + /** + * Property where the event occurred. + * + * @return the Source of the event. + */ + @Override + public Property getProperty() { + return (Property) getSource(); + } + } + + /* + * Adds a read-only status change listener for the field. Don't add a + * JavaDoc comment here, we use the default documentation from the + * implemented interface. + */ + @Override + public void addListener(Property.ReadOnlyStatusChangeListener listener) { + addListener(Property.ReadOnlyStatusChangeEvent.class, listener, + READ_ONLY_STATUS_CHANGE_METHOD); + } + + /* + * Removes a read-only status change listener from the field. Don't add a + * JavaDoc comment here, we use the default documentation from the + * implemented interface. + */ + @Override + public void removeListener(Property.ReadOnlyStatusChangeListener listener) { + removeListener(Property.ReadOnlyStatusChangeEvent.class, listener, + READ_ONLY_STATUS_CHANGE_METHOD); + } + + /** + * Emits the read-only status change event. The value contained in the field + * is validated before the event is created. + */ + protected void fireReadOnlyStatusChange() { + fireEvent(new AbstractField.ReadOnlyStatusChangeEvent(this)); + } + + /** + * This method listens to data source value changes and passes the changes + * forwards. + * + * Changes are not forwarded to the listeners of the field during internal + * operations of the field to avoid duplicate notifications. + * + * @param event + * the value change event telling the data source contents have + * changed. + */ + @Override + public void valueChange(Property.ValueChangeEvent event) { + if (isReadThrough()) { + if (committingValueToDataSource) { + boolean propertyNotifiesOfTheBufferedValue = equals(event + .getProperty().getValue(), getInternalValue()); + if (!propertyNotifiesOfTheBufferedValue) { + /* + * Property (or chained property like PropertyFormatter) now + * reports different value than the one the field has just + * committed to it. In this case we respect the property + * value. + * + * Still, we don't fire value change yet, but instead + * postpone it until "commit" is done. See setValue(Object, + * boolean) and commit(). + */ + readValueFromProperty(event); + valueWasModifiedByDataSourceDuringCommit = true; + } + } else if (!isModified()) { + readValueFromProperty(event); + fireValueChange(false); + } + } + } + + private void readValueFromProperty(Property.ValueChangeEvent event) { + setInternalValue(convertFromDataSource(event.getProperty().getValue())); + } + + /** + * {@inheritDoc} + */ + @Override + public void focus() { + super.focus(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.Component.Focusable#getTabIndex() + */ + @Override + public int getTabIndex() { + return getState().getTabIndex(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.Component.Focusable#setTabIndex(int) + */ + @Override + public void setTabIndex(int tabIndex) { + getState().setTabIndex(tabIndex); + requestRepaint(); + } + + /** + * Returns the internal field value, which might not match the data source + * value e.g. if the field has been modified and is not in write-through + * mode. + * + * This method can be overridden by subclasses together with + * {@link #setInternalValue(Object)} to compute internal field value at + * runtime. When doing so, typically also {@link #isModified()} needs to be + * overridden and care should be taken in the management of the empty state + * and buffering support. + * + * @return internal field value + */ + protected T getInternalValue() { + return value; + } + + /** + * Sets the internal field value. This is purely used by AbstractField to + * change the internal Field value. It does not trigger valuechange events. + * It can be overridden by the inheriting classes to update all dependent + * variables. + * + * Subclasses can also override {@link #getInternalValue()} if necessary. + * + * @param newValue + * the new value to be set. + */ + protected void setInternalValue(T newValue) { + value = newValue; + if (validators != null && !validators.isEmpty()) { + requestRepaint(); + } + } + + /** + * Notifies the component that it is connected to an application. + * + * @see com.vaadin.ui.Component#attach() + */ + @Override + public void attach() { + super.attach(); + + if (!isListeningToPropertyEvents) { + addPropertyListeners(); + if (!isModified() && isReadThrough()) { + // Update value from data source + discard(); + } + } + } + + @Override + public void detach() { + super.detach(); + // Stop listening to data source events on detach to avoid a potential + // memory leak. See #6155. + removePropertyListeners(); + } + + /** + * Is this field required. Required fields must filled by the user. + * + * If the field is required, it is visually indicated in the user interface. + * Furthermore, setting field to be required implicitly adds "non-empty" + * validator and thus isValid() == false or any isEmpty() fields. In those + * cases validation errors are not painted as it is obvious that the user + * must fill in the required fields. + * + * On the other hand, for the non-required fields isValid() == true if the + * field isEmpty() regardless of any attached validators. + * + * + * @return <code>true</code> if the field is required, otherwise + * <code>false</code>. + */ + @Override + public boolean isRequired() { + return getState().isRequired(); + } + + /** + * Sets the field required. Required fields must filled by the user. + * + * If the field is required, it is visually indicated in the user interface. + * Furthermore, setting field to be required implicitly adds "non-empty" + * validator and thus isValid() == false or any isEmpty() fields. In those + * cases validation errors are not painted as it is obvious that the user + * must fill in the required fields. + * + * On the other hand, for the non-required fields isValid() == true if the + * field isEmpty() regardless of any attached validators. + * + * @param required + * Is the field required. + */ + @Override + public void setRequired(boolean required) { + getState().setRequired(required); + requestRepaint(); + } + + /** + * Set the error that is show if this field is required, but empty. When + * setting requiredMessage to be "" or null, no error pop-up or exclamation + * mark is shown for a empty required field. This faults to "". Even in + * those cases isValid() returns false for empty required fields. + * + * @param requiredMessage + * Message to be shown when this field is required, but empty. + */ + @Override + public void setRequiredError(String requiredMessage) { + requiredError = requiredMessage; + requestRepaint(); + } + + @Override + public String getRequiredError() { + return requiredError; + } + + /** + * Gets the error that is shown if the field value cannot be converted to + * the data source type. + * + * @return The error that is shown if conversion of the field value fails + */ + public String getConversionError() { + return conversionError; + } + + /** + * Sets the error that is shown if the field value cannot be converted to + * the data source type. If {0} is present in the message, it will be + * replaced by the simple name of the data source type. + * + * @param valueConversionError + * Message to be shown when conversion of the value fails + */ + public void setConversionError(String valueConversionError) { + this.conversionError = valueConversionError; + requestRepaint(); + } + + /** + * Is the field empty? + * + * In general, "empty" state is same as null. As an exception, TextField + * also treats empty string as "empty". + */ + protected boolean isEmpty() { + return (getFieldValue() == null); + } + + /** + * Is automatic, visible validation enabled? + * + * If automatic validation is enabled, any validators connected to this + * component are evaluated while painting the component and potential error + * messages are sent to client. If the automatic validation is turned off, + * isValid() and validate() methods still work, but one must show the + * validation in their own code. + * + * @return True, if automatic validation is enabled. + */ + public boolean isValidationVisible() { + return validationVisible; + } + + /** + * Enable or disable automatic, visible validation. + * + * If automatic validation is enabled, any validators connected to this + * component are evaluated while painting the component and potential error + * messages are sent to client. If the automatic validation is turned off, + * isValid() and validate() methods still work, but one must show the + * validation in their own code. + * + * @param validateAutomatically + * True, if automatic validation is enabled. + */ + public void setValidationVisible(boolean validateAutomatically) { + if (validationVisible != validateAutomatically) { + requestRepaint(); + validationVisible = validateAutomatically; + } + } + + /** + * Sets the current buffered source exception. + * + * @param currentBufferedSourceException + */ + public void setCurrentBufferedSourceException( + Buffered.SourceException currentBufferedSourceException) { + this.currentBufferedSourceException = currentBufferedSourceException; + requestRepaint(); + } + + /** + * Gets the current buffered source exception. + * + * @return The current source exception + */ + protected Buffered.SourceException getCurrentBufferedSourceException() { + return currentBufferedSourceException; + } + + /** + * A ready-made {@link ShortcutListener} that focuses the given + * {@link Focusable} (usually a {@link Field}) when the keyboard shortcut is + * invoked. + * + */ + public static class FocusShortcut extends ShortcutListener { + protected Focusable focusable; + + /** + * Creates a keyboard shortcut for focusing the given {@link Focusable} + * using the shorthand notation defined in {@link ShortcutAction}. + * + * @param focusable + * to focused when the shortcut is invoked + * @param shorthandCaption + * caption with keycode and modifiers indicated + */ + public FocusShortcut(Focusable focusable, String shorthandCaption) { + super(shorthandCaption); + this.focusable = focusable; + } + + /** + * Creates a keyboard shortcut for focusing the given {@link Focusable}. + * + * @param focusable + * to focused when the shortcut is invoked + * @param keyCode + * keycode that invokes the shortcut + * @param modifiers + * modifiers required to invoke the shortcut + */ + public FocusShortcut(Focusable focusable, int keyCode, int... modifiers) { + super(null, keyCode, modifiers); + this.focusable = focusable; + } + + /** + * Creates a keyboard shortcut for focusing the given {@link Focusable}. + * + * @param focusable + * to focused when the shortcut is invoked + * @param keyCode + * keycode that invokes the shortcut + */ + public FocusShortcut(Focusable focusable, int keyCode) { + this(focusable, keyCode, null); + } + + @Override + public void handleAction(Object sender, Object target) { + focusable.focus(); + } + } + + /** + * Gets the converter used to convert the property data source value to the + * field value. + * + * @return The converter or null if none is set. + */ + public Converter<T, Object> getConverter() { + return converter; + } + + /** + * Sets the converter used to convert the field value to property data + * source type. The converter must have a presentation type that matches the + * field type. + * + * @param converter + * The new converter to use. + */ + public void setConverter(Converter<T, ?> converter) { + this.converter = (Converter<T, Object>) converter; + requestRepaint(); + } + + @Override + public AbstractFieldState getState() { + return (AbstractFieldState) super.getState(); + } + + @Override + public void updateState() { + super.updateState(); + + // Hide the error indicator if needed + getState().setHideErrors(shouldHideErrors()); + } + + /** + * Registers this as an event listener for events sent by the data source + * (if any). Does nothing if + * <code>isListeningToPropertyEvents == true</code>. + */ + private void addPropertyListeners() { + if (!isListeningToPropertyEvents) { + if (dataSource instanceof Property.ValueChangeNotifier) { + ((Property.ValueChangeNotifier) dataSource).addListener(this); + } + if (dataSource instanceof Property.ReadOnlyStatusChangeNotifier) { + ((Property.ReadOnlyStatusChangeNotifier) dataSource) + .addListener(this); + } + isListeningToPropertyEvents = true; + } + } + + /** + * Stops listening to events sent by the data source (if any). Does nothing + * if <code>isListeningToPropertyEvents == false</code>. + */ + private void removePropertyListeners() { + if (isListeningToPropertyEvents) { + if (dataSource instanceof Property.ValueChangeNotifier) { + ((Property.ValueChangeNotifier) dataSource) + .removeListener(this); + } + if (dataSource instanceof Property.ReadOnlyStatusChangeNotifier) { + ((Property.ReadOnlyStatusChangeNotifier) dataSource) + .removeListener(this); + } + isListeningToPropertyEvents = false; + } + } +} diff --git a/server/src/com/vaadin/ui/AbstractJavaScriptComponent.java b/server/src/com/vaadin/ui/AbstractJavaScriptComponent.java new file mode 100644 index 0000000000..5ec80573ab --- /dev/null +++ b/server/src/com/vaadin/ui/AbstractJavaScriptComponent.java @@ -0,0 +1,165 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.ui; + +import com.vaadin.shared.ui.JavaScriptComponentState; +import com.vaadin.terminal.JavaScriptCallbackHelper; +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.terminal.gwt.client.ui.JavaScriptWidget; + +/** + * Base class for Components with all client-side logic implemented using + * JavaScript. + * <p> + * When a new JavaScript component is initialized in the browser, the framework + * will look for a globally defined JavaScript function that will initialize the + * component. 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_MyComponent</code> for the + * server-side + * <code>com.example.MyComponent extends AbstractJavaScriptComponent</code> + * class. If MyComponent instead extends <code>com.example.SuperComponent</code> + * , then <code>com_example_SuperComponent</code> will also be attempted if + * <code>com_example_MyComponent</code> has not been defined. + * <p> + * JavaScript components have a very simple GWT widget ({@link JavaScriptWidget} + * ) just consisting of a <code>div</code> element to which the JavaScript code + * should initialize its own user interface. + * <p> + * The initialization function will be called with <code>this</code> pointing to + * a connector wrapper object providing integration to Vaadin with the following + * functions: + * <ul> + * <li><code>getConnectorId()</code> - returns a string with the id of the + * connector.</li> + * <li><code>getParentId([connectorId])</code> - returns a string with the id of + * the connector's parent. If <code>connectorId</code> is provided, the id of + * the parent of the corresponding connector with the passed id is returned + * instead.</li> + * <li><code>getElement([connectorId])</code> - returns the DOM Element that is + * the root of a connector's widget. <code>null</code> is returned if the + * connector can not be found or if the connector doesn't have a widget. If + * <code>connectorId</code> is not provided, the connector id of the current + * connector will be used.</li> + * <li><code>getState()</code> - returns an object corresponding to the shared + * state defined on the server. The scheme for conversion between Java and + * JavaScript types is described bellow.</li> + * <li><code>registerRpc([name, ] rpcObject)</code> - registers the + * <code>rpcObject</code> as a RPC handler. <code>rpcObject</code> should be an + * object with field containing functions for all eligible RPC functions. If + * <code>name</code> is provided, the RPC handler will only used for RPC calls + * for the RPC interface with the same fully qualified Java name. If no + * <code>name</code> is provided, the RPC handler will be used for all incoming + * RPC invocations where the RPC method name is defined as a function field in + * the handler. The scheme for conversion between Java types in the RPC + * interface definition and the JavaScript values passed as arguments to the + * handler functions is described bellow.</li> + * <li><code>getRpcProxy([name])</code> - returns an RPC proxy object. If + * <code>name</code> is provided, the proxy object will contain functions for + * all methods in the RPC interface with the same fully qualified name, provided + * a RPC handler has been registered by the server-side code. If no + * <code>name</code> is provided, the returned RPC proxy object will contain + * functions for all methods in all RPC interfaces registered for the connector + * on the server. If the same method name is present in multiple registered RPC + * interfaces, the corresponding function in the RPC proxy object will throw an + * exception when called. The scheme for conversion between Java types in the + * RPC interface and the JavaScript values that should be passed to the + * functions is described bellow.</li> + * <li><code>translateVaadinUri(uri)</code> - Translates a Vaadin URI to a URL + * that can be used in the browser. This is just way of accessing + * {@link ApplicationConnection#translateVaadinUri(String)}</li> + * </ul> + * The connector wrapper also supports these special functions: + * <ul> + * <li><code>onStateChange</code> - If the JavaScript code assigns a function to + * the field, that function is called whenever the contents of the shared state + * is changed.</li> + * <li>Any field name corresponding to a call to + * {@link #addFunction(String, JavaScriptFunction)} on the server will + * automatically be present as a function that triggers the registered function + * on the server.</li> + * <li>Any field name referred to using + * {@link #callFunction(String, Object...)} on the server will be called if a + * function has been assigned to the field.</li> + * </ul> + * <p> + * + * Values in the Shared State and in RPC calls are converted between Java and + * JavaScript using the following conventions: + * <ul> + * <li>Primitive Java numbers (byte, char, int, long, float, double) and their + * boxed types (Byte, Character, Integer, Long, Float, Double) are represented + * by JavaScript numbers.</li> + * <li>The primitive Java boolean and the boxed Boolean are represented by + * JavaScript booleans.</li> + * <li>Java Strings are represented by JavaScript strings.</li> + * <li>List, Set and all arrays in Java are represented by JavaScript arrays.</li> + * <li>Map<String, ?> in Java is represented by JavaScript object with fields + * corresponding to the map keys.</li> + * <li>Any other Java Map is represented by a JavaScript array containing two + * arrays, the first contains the keys and the second contains the values in the + * same order.</li> + * <li>A Java Bean is represented by a JavaScript object with fields + * corresponding to the bean's properties.</li> + * <li>A Java Connector is represented by a JavaScript string containing the + * connector's id.</li> + * <li>A pluggable serialization mechanism is provided for types not described + * here. Please refer to the documentation for specific types for serialization + * information.</li> + * </ul> + * + * @author Vaadin Ltd + * @version @VERSION@ + * @since 7.0.0 + */ +public abstract class AbstractJavaScriptComponent extends AbstractComponent { + private JavaScriptCallbackHelper callbackHelper = new JavaScriptCallbackHelper( + this); + + @Override + protected <T> void registerRpc(T implementation, Class<T> 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 function + * @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 should only contain + * data types that can be represented in JavaScript including primitives, + * their boxed types, arrays, String, List, Set, Map, Connector and + * JavaBeans. + * + * @param name + * the name of the function + * @param arguments + * function arguments + */ + protected void callFunction(String name, Object... arguments) { + callbackHelper.invokeCallback(name, arguments); + } + + @Override + public JavaScriptComponentState getState() { + return (JavaScriptComponentState) super.getState(); + } +} diff --git a/server/src/com/vaadin/ui/AbstractLayout.java b/server/src/com/vaadin/ui/AbstractLayout.java new file mode 100644 index 0000000000..7b3a537d06 --- /dev/null +++ b/server/src/com/vaadin/ui/AbstractLayout.java @@ -0,0 +1,77 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import com.vaadin.shared.ui.AbstractLayoutState; +import com.vaadin.ui.Layout.MarginHandler; + +/** + * An abstract class that defines default implementation for the {@link Layout} + * interface. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 5.0 + */ +@SuppressWarnings("serial") +public abstract class AbstractLayout extends AbstractComponentContainer + implements Layout, MarginHandler { + + protected MarginInfo margins = new MarginInfo(false); + + @Override + public AbstractLayoutState getState() { + return (AbstractLayoutState) super.getState(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.Layout#setMargin(boolean) + */ + @Override + public void setMargin(boolean enabled) { + margins.setMargins(enabled); + getState().setMarginsBitmask(margins.getBitMask()); + requestRepaint(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.Layout.MarginHandler#getMargin() + */ + @Override + public MarginInfo getMargin() { + return margins; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.Layout.MarginHandler#setMargin(MarginInfo) + */ + @Override + public void setMargin(MarginInfo marginInfo) { + margins.setMargins(marginInfo); + getState().setMarginsBitmask(margins.getBitMask()); + requestRepaint(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.Layout#setMargin(boolean, boolean, boolean, boolean) + */ + @Override + public void setMargin(boolean topEnabled, boolean rightEnabled, + boolean bottomEnabled, boolean leftEnabled) { + margins.setMargins(topEnabled, rightEnabled, bottomEnabled, leftEnabled); + getState().setMarginsBitmask(margins.getBitMask()); + requestRepaint(); + } + +} diff --git a/server/src/com/vaadin/ui/AbstractMedia.java b/server/src/com/vaadin/ui/AbstractMedia.java new file mode 100644 index 0000000000..71b2e38ef3 --- /dev/null +++ b/server/src/com/vaadin/ui/AbstractMedia.java @@ -0,0 +1,196 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.util.ArrayList; +import java.util.List; + +import com.vaadin.shared.communication.URLReference; +import com.vaadin.shared.ui.AbstractMediaState; +import com.vaadin.shared.ui.MediaControl; +import com.vaadin.terminal.Resource; +import com.vaadin.terminal.gwt.server.ResourceReference; + +/** + * Abstract base class for the HTML5 media components. + * + * @author Vaadin Ltd + */ +public abstract class AbstractMedia extends AbstractComponent { + + @Override + public AbstractMediaState getState() { + return (AbstractMediaState) super.getState(); + } + + /** + * Sets a single media file as the source of the media component. + * + * @param source + */ + public void setSource(Resource source) { + clearSources(); + + addSource(source); + } + + private void clearSources() { + getState().getSources().clear(); + getState().getSourceTypes().clear(); + } + + /** + * Adds an alternative media file to the sources list. Which of the sources + * is used is selected by the browser depending on which file formats it + * supports. See <a + * href="http://en.wikipedia.org/wiki/HTML5_video#Table">wikipedia</a> for a + * table of formats supported by different browsers. + * + * @param source + */ + public void addSource(Resource source) { + if (source != null) { + getState().getSources().add(new ResourceReference(source)); + getState().getSourceTypes().add(source.getMIMEType()); + requestRepaint(); + } + } + + /** + * Set multiple sources at once. Which of the sources is used is selected by + * the browser depending on which file formats it supports. See <a + * href="http://en.wikipedia.org/wiki/HTML5_video#Table">wikipedia</a> for a + * table of formats supported by different browsers. + * + * @param sources + */ + public void setSources(Resource... sources) { + clearSources(); + for (Resource source : sources) { + addSource(source); + } + } + + /** + * @return The sources pointed to in this media. + */ + public List<Resource> getSources() { + ArrayList<Resource> sources = new ArrayList<Resource>(); + for (URLReference ref : getState().getSources()) { + sources.add(((ResourceReference) ref).getResource()); + } + return sources; + } + + /** + * Sets whether or not the browser should show native media controls. + * + * @param showControls + */ + public void setShowControls(boolean showControls) { + getState().setShowControls(showControls); + requestRepaint(); + } + + /** + * @return true if the browser is to show native media controls. + */ + public boolean isShowControls() { + return getState().isShowControls(); + } + + /** + * Sets the alternative text to be displayed if the browser does not support + * HTML5. This text is rendered as HTML if + * {@link #setHtmlContentAllowed(boolean)} is set to true. With HTML + * rendering, this method can also be used to implement fallback to a + * flash-based player, see the <a href= + * "https://developer.mozilla.org/En/Using_audio_and_video_in_Firefox#Using_Flash" + * >Mozilla Developer Network</a> for details. + * + * @param altText + */ + public void setAltText(String altText) { + getState().setAltText(altText); + requestRepaint(); + } + + /** + * @return The text/html that is displayed when a browser doesn't support + * HTML5. + */ + public String getAltText() { + return getState().getAltText(); + } + + /** + * Set whether the alternative text ({@link #setAltText(String)}) is + * rendered as HTML or not. + * + * @param htmlContentAllowed + */ + public void setHtmlContentAllowed(boolean htmlContentAllowed) { + getState().setHtmlContentAllowed(htmlContentAllowed); + requestRepaint(); + } + + /** + * @return true if the alternative text ({@link #setAltText(String)}) is to + * be rendered as HTML. + */ + public boolean isHtmlContentAllowed() { + return getState().isHtmlContentAllowed(); + } + + /** + * Sets whether the media is to automatically start playback when enough + * data has been loaded. + * + * @param autoplay + */ + public void setAutoplay(boolean autoplay) { + getState().setAutoplay(autoplay); + requestRepaint(); + } + + /** + * @return true if the media is set to automatically start playback. + */ + public boolean isAutoplay() { + return getState().isAutoplay(); + } + + /** + * Set whether to mute the audio or not. + * + * @param muted + */ + public void setMuted(boolean muted) { + getState().setMuted(muted); + requestRepaint(); + } + + /** + * @return true if the audio is muted. + */ + public boolean isMuted() { + return getState().isMuted(); + } + + /** + * Pauses the media. + */ + public void pause() { + getRpcProxy(MediaControl.class).pause(); + } + + /** + * Starts playback of the media. + */ + public void play() { + getRpcProxy(MediaControl.class).play(); + } + +} diff --git a/server/src/com/vaadin/ui/AbstractOrderedLayout.java b/server/src/com/vaadin/ui/AbstractOrderedLayout.java new file mode 100644 index 0000000000..0581d0a279 --- /dev/null +++ b/server/src/com/vaadin/ui/AbstractOrderedLayout.java @@ -0,0 +1,383 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.util.Iterator; +import java.util.LinkedList; + +import com.vaadin.event.LayoutEvents.LayoutClickEvent; +import com.vaadin.event.LayoutEvents.LayoutClickListener; +import com.vaadin.event.LayoutEvents.LayoutClickNotifier; +import com.vaadin.shared.Connector; +import com.vaadin.shared.MouseEventDetails; +import com.vaadin.shared.ui.orderedlayout.AbstractOrderedLayoutServerRpc; +import com.vaadin.shared.ui.orderedlayout.AbstractOrderedLayoutState; +import com.vaadin.shared.ui.orderedlayout.AbstractOrderedLayoutState.ChildComponentData; +import com.vaadin.terminal.Sizeable; +import com.vaadin.terminal.gwt.client.ui.LayoutClickEventHandler; + +@SuppressWarnings("serial") +public abstract class AbstractOrderedLayout extends AbstractLayout implements + Layout.AlignmentHandler, Layout.SpacingHandler, LayoutClickNotifier { + + private AbstractOrderedLayoutServerRpc rpc = new AbstractOrderedLayoutServerRpc() { + + @Override + public void layoutClick(MouseEventDetails mouseDetails, + Connector clickedConnector) { + fireEvent(LayoutClickEvent.createEvent(AbstractOrderedLayout.this, + mouseDetails, clickedConnector)); + } + }; + + public static final Alignment ALIGNMENT_DEFAULT = Alignment.TOP_LEFT; + + /** + * Custom layout slots containing the components. + */ + protected LinkedList<Component> components = new LinkedList<Component>(); + + /* Child component alignments */ + + /** + * Mapping from components to alignments (horizontal + vertical). + */ + public AbstractOrderedLayout() { + registerRpc(rpc); + } + + @Override + public AbstractOrderedLayoutState getState() { + return (AbstractOrderedLayoutState) super.getState(); + } + + /** + * Add a component into this container. The component is added to the right + * or under the previous component. + * + * @param c + * the component to be added. + */ + @Override + public void addComponent(Component c) { + // Add to components before calling super.addComponent + // so that it is available to AttachListeners + components.add(c); + try { + super.addComponent(c); + } catch (IllegalArgumentException e) { + components.remove(c); + throw e; + } + componentAdded(c); + } + + /** + * Adds a component into this container. The component is added to the left + * or on top of the other components. + * + * @param c + * the component to be added. + */ + public void addComponentAsFirst(Component c) { + // If c is already in this, we must remove it before proceeding + // see ticket #7668 + if (c.getParent() == this) { + removeComponent(c); + } + components.addFirst(c); + try { + super.addComponent(c); + } catch (IllegalArgumentException e) { + components.remove(c); + throw e; + } + componentAdded(c); + + } + + /** + * Adds a component into indexed position in this container. + * + * @param c + * the component to be added. + * @param index + * the index of the component position. The components currently + * in and after the position are shifted forwards. + */ + public void addComponent(Component c, int index) { + // If c is already in this, we must remove it before proceeding + // see ticket #7668 + if (c.getParent() == this) { + // When c is removed, all components after it are shifted down + if (index > getComponentIndex(c)) { + index--; + } + removeComponent(c); + } + components.add(index, c); + try { + super.addComponent(c); + } catch (IllegalArgumentException e) { + components.remove(c); + throw e; + } + + componentAdded(c); + } + + private void componentRemoved(Component c) { + getState().getChildData().remove(c); + requestRepaint(); + } + + private void componentAdded(Component c) { + getState().getChildData().put(c, new ChildComponentData()); + requestRepaint(); + + } + + /** + * Removes the component from this container. + * + * @param c + * the component to be removed. + */ + @Override + public void removeComponent(Component c) { + components.remove(c); + super.removeComponent(c); + componentRemoved(c); + } + + /** + * Gets the component container iterator for going trough all the components + * in the container. + * + * @return the Iterator of the components inside the container. + */ + @Override + public Iterator<Component> getComponentIterator() { + return components.iterator(); + } + + /** + * Gets the number of contained components. Consistent with the iterator + * returned by {@link #getComponentIterator()}. + * + * @return the number of contained components + */ + @Override + public int getComponentCount() { + return components.size(); + } + + /* Documented in superclass */ + @Override + public void replaceComponent(Component oldComponent, Component newComponent) { + + // Gets the locations + int oldLocation = -1; + int newLocation = -1; + int location = 0; + for (final Iterator<Component> i = components.iterator(); i.hasNext();) { + final Component component = i.next(); + + if (component == oldComponent) { + oldLocation = location; + } + if (component == newComponent) { + newLocation = location; + } + + location++; + } + + if (oldLocation == -1) { + addComponent(newComponent); + } else if (newLocation == -1) { + removeComponent(oldComponent); + addComponent(newComponent, oldLocation); + } else { + // Both old and new are in the layout + if (oldLocation > newLocation) { + components.remove(oldComponent); + components.add(newLocation, oldComponent); + components.remove(newComponent); + components.add(oldLocation, newComponent); + } else { + components.remove(newComponent); + components.add(oldLocation, newComponent); + components.remove(oldComponent); + components.add(newLocation, oldComponent); + } + + requestRepaint(); + } + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.Layout.AlignmentHandler#setComponentAlignment(com + * .vaadin.ui.Component, int, int) + */ + @Override + public void setComponentAlignment(Component childComponent, + int horizontalAlignment, int verticalAlignment) { + Alignment a = new Alignment(horizontalAlignment + verticalAlignment); + setComponentAlignment(childComponent, a); + } + + @Override + public void setComponentAlignment(Component childComponent, + Alignment alignment) { + ChildComponentData childData = getState().getChildData().get( + childComponent); + if (childData != null) { + // Alignments are bit masks + childData.setAlignmentBitmask(alignment.getBitMask()); + requestRepaint(); + } else { + throw new IllegalArgumentException( + "Component must be added to layout before using setComponentAlignment()"); + } + + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.Layout.AlignmentHandler#getComponentAlignment(com + * .vaadin.ui.Component) + */ + @Override + public Alignment getComponentAlignment(Component childComponent) { + ChildComponentData childData = getState().getChildData().get( + childComponent); + if (childData == null) { + throw new IllegalArgumentException( + "The given component is not a child of this layout"); + } + + return new Alignment(childData.getAlignmentBitmask()); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.Layout.SpacingHandler#setSpacing(boolean) + */ + @Override + public void setSpacing(boolean spacing) { + getState().setSpacing(spacing); + requestRepaint(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.Layout.SpacingHandler#isSpacing() + */ + @Override + public boolean isSpacing() { + return getState().isSpacing(); + } + + /** + * <p> + * This method is used to control how excess space in layout is distributed + * among components. Excess space may exist if layout is sized and contained + * non relatively sized components don't consume all available space. + * + * <p> + * Example how to distribute 1:3 (33%) for component1 and 2:3 (67%) for + * component2 : + * + * <code> + * layout.setExpandRatio(component1, 1);<br> + * layout.setExpandRatio(component2, 2); + * </code> + * + * <p> + * If no ratios have been set, the excess space is distributed evenly among + * all components. + * + * <p> + * Note, that width or height (depending on orientation) needs to be defined + * for this method to have any effect. + * + * @see Sizeable + * + * @param component + * the component in this layout which expand ratio is to be set + * @param ratio + */ + public void setExpandRatio(Component component, float ratio) { + ChildComponentData childData = getState().getChildData().get(component); + if (childData == null) { + throw new IllegalArgumentException( + "The given component is not a child of this layout"); + } + + childData.setExpandRatio(ratio); + requestRepaint(); + }; + + /** + * Returns the expand ratio of given component. + * + * @param component + * which expand ratios is requested + * @return expand ratio of given component, 0.0f by default. + */ + public float getExpandRatio(Component component) { + ChildComponentData childData = getState().getChildData().get(component); + if (childData == null) { + throw new IllegalArgumentException( + "The given component is not a child of this layout"); + } + + return childData.getExpandRatio(); + } + + @Override + public void addListener(LayoutClickListener listener) { + addListener(LayoutClickEventHandler.LAYOUT_CLICK_EVENT_IDENTIFIER, + LayoutClickEvent.class, listener, + LayoutClickListener.clickMethod); + } + + @Override + public void removeListener(LayoutClickListener listener) { + removeListener(LayoutClickEventHandler.LAYOUT_CLICK_EVENT_IDENTIFIER, + LayoutClickEvent.class, listener); + } + + /** + * Returns the index of the given component. + * + * @param component + * The component to look up. + * @return The index of the component or -1 if the component is not a child. + */ + public int getComponentIndex(Component component) { + return components.indexOf(component); + } + + /** + * Returns the component at the given position. + * + * @param index + * The position of the component. + * @return The component at the given index. + * @throws IndexOutOfBoundsException + * If the index is out of range. + */ + public Component getComponent(int index) throws IndexOutOfBoundsException { + return components.get(index); + } + +} diff --git a/server/src/com/vaadin/ui/AbstractSelect.java b/server/src/com/vaadin/ui/AbstractSelect.java new file mode 100644 index 0000000000..0a97ceb649 --- /dev/null +++ b/server/src/com/vaadin/ui/AbstractSelect.java @@ -0,0 +1,2029 @@ +/* + * @VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.io.Serializable; +import java.util.Collection; +import java.util.Collections; +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 com.vaadin.data.Container; +import com.vaadin.data.Item; +import com.vaadin.data.Property; +import com.vaadin.data.util.IndexedContainer; +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.shared.ui.dd.VerticalDropLocation; +import com.vaadin.terminal.KeyMapper; +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.PaintTarget; +import com.vaadin.terminal.Resource; +import com.vaadin.terminal.Vaadin6Component; +import com.vaadin.ui.AbstractSelect.ItemCaptionMode; + +/** + * <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.data.Item}s in a + * {@link com.vaadin.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. + * @version + * @VERSION@ + * @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, Vaadin6Component { + + public enum ItemCaptionMode { + /** + * Item caption mode: Item's ID's <code>String</code> representation is + * used as caption. + */ + ID, + /** + * 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's <code>String</code> representation 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 from 7.0, use {@link ItemCaptionMode.ID} instead + */ + @Deprecated + public static final ItemCaptionMode ITEM_CAPTION_MODE_ID = ItemCaptionMode.ID; + + /** + * @deprecated from 7.0, use {@link ItemCaptionMode.ID} instead + */ + @Deprecated + public static final ItemCaptionMode ITEM_CAPTION_MODE_ITEM = ItemCaptionMode.ITEM; + + /** + * @deprecated from 7.0, use {@link ItemCaptionMode.ID} instead + */ + @Deprecated + public static final ItemCaptionMode ITEM_CAPTION_MODE_INDEX = ItemCaptionMode.INDEX; + + /** + * @deprecated from 7.0, use {@link ItemCaptionMode.ID} instead + */ + @Deprecated + public static final ItemCaptionMode ITEM_CAPTION_MODE_EXPLICIT_DEFAULTS_ID = ItemCaptionMode.EXPLICIT_DEFAULTS_ID; + + /** + * @deprecated from 7.0, use {@link ItemCaptionMode.ID} instead + */ + @Deprecated + public static final ItemCaptionMode ITEM_CAPTION_MODE_EXPLICIT = ItemCaptionMode.EXPLICIT; + + /** + * @deprecated from 7.0, use {@link ItemCaptionMode.ID} instead + */ + @Deprecated + public static final ItemCaptionMode ITEM_CAPTION_MODE_ICON_ONLY = ItemCaptionMode.ICON_ONLY; + + /** + * @deprecated from 7.0, use {@link ItemCaptionMode.ID} 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 { + public static final int FILTERINGMODE_OFF = 0; + public static final int FILTERINGMODE_STARTSWITH = 1; + public static final int FILTERINGMODE_CONTAINS = 2; + + /** + * Sets the option filtering mode. + * + * @param filteringMode + * the filtering mode to use + */ + public void setFilteringMode(int filteringMode); + + /** + * Gets the current filtering mode. + * + * @return the filtering mode in use + */ + public int getFilteringMode(); + + } + + /** + * Multi select modes that controls how multi select behaves. + */ + public enum MultiSelectMode { + /** + * The default behavior of the multi select mode + */ + DEFAULT, + + /** + * The previous more simple behavior of the multselect + */ + SIMPLE + } + + /** + * Is the select in multiselect mode? + */ + private boolean multiSelect = false; + + /** + * 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 (isMultiSelect()) { + target.addAttribute("selectmode", "multi"); + } + 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 + requestRepaint(); + } else if (id != null && containsId(id)) { + acceptedSelections.add(id); + } + } + + if (!isNullSelectionAllowed() && acceptedSelections.size() < 1) { + // empty selection not allowed, keep old value + requestRepaint(); + 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())) { + requestRepaint(); + 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 { + final Object id = itemIdMapper + .get(clientSideSelectedKeys[0]); + if (!isNullSelectionAllowed() && id == null) { + requestRepaint(); + } 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.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.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> + * + * @param newValue + * the New selected item or collection of selected items. + * @param repaintIsNotNeeded + * True if caller is sure that repaint is not needed. + * @see com.vaadin.ui.AbstractField#setValue(java.lang.Object, + * java.lang.Boolean) + */ + @Override + protected void setValue(Object newValue, boolean repaintIsNotNeeded) + throws Property.ReadOnlyException { + + if (isMultiSelect()) { + if (newValue == null) { + super.setValue(new LinkedHashSet<Object>(), repaintIsNotNeeded); + } else if (Collection.class.isAssignableFrom(newValue.getClass())) { + super.setValue(new LinkedHashSet<Object>( + (Collection<?>) newValue), repaintIsNotNeeded); + } + } else if (newValue == null || items.containsId(newValue)) { + super.setValue(newValue, repaintIsNotNeeded); + } + } + + /* 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.vaadin.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() { + return items.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.vaadin.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.vaadin.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.vaadin.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.vaadin.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.vaadin.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; + } + + /* + * (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; + } + + /** + * 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.vaadin.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.vaadin.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) + .removeListener(this); + } + if (items instanceof Container.PropertySetChangeNotifier) { + ((Container.PropertySetChangeNotifier) items) + .removeListener(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).addListener(this); + } + if (items instanceof Container.PropertySetChangeNotifier) { + ((Container.PropertySetChangeNotifier) items) + .addListener(this); + } + } + + /* + * We expect changing the data source should also clean value. See + * #810, #4607, #5281 + */ + setValue(null); + + requestRepaint(); + + } + } + + /** + * Gets the viewing data-source container. + * + * @see com.vaadin.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 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 != this.multiSelect) { + + // Selection before mode change + final Object oldValue = getValue(); + + this.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()); + } + } + + requestRepaint(); + } + } + + /** + * 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; + + requestRepaint(); + } + } + + /** + * 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); + requestRepaint(); + } + } + + /** + * 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 = 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 = itemId.toString(); + } + 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 : ""; + } + + /** + * Sets tqhe 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); + } + requestRepaint(); + } + } + + /** + * 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. + * + * <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> + * + * @param mode + * the One of the modes listed above. + */ + public void setItemCaptionMode(ItemCaptionMode mode) { + if (mode != null) { + itemCaptionMode = mode; + requestRepaint(); + } + } + + /** + * 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); + requestRepaint(); + } else { + itemCaptionPropertyId = null; + if (getItemCaptionMode() == ITEM_CAPTION_MODE_PROPERTY) { + setItemCaptionMode(ITEM_CAPTION_MODE_EXPLICIT_DEFAULTS_ID); + } + requestRepaint(); + } + } + + /** + * 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"); + } + requestRepaint(); + } + + /** + * 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.vaadin.data.Container.PropertySetChangeListener#containerPropertySetChange(com.vaadin.data.Container.PropertySetChangeEvent) + */ + @Override + public void containerPropertySetChange( + Container.PropertySetChangeEvent event) { + firePropertySetChange(); + } + + /** + * Adds a new Property set change listener for this Container. + * + * @see com.vaadin.data.Container.PropertySetChangeNotifier#addListener(com.vaadin.data.Container.PropertySetChangeListener) + */ + @Override + public void addListener(Container.PropertySetChangeListener listener) { + if (propertySetEventListeners == null) { + propertySetEventListeners = new LinkedHashSet<Container.PropertySetChangeListener>(); + } + propertySetEventListeners.add(listener); + } + + /** + * Removes a previously registered Property set change listener. + * + * @see com.vaadin.data.Container.PropertySetChangeNotifier#removeListener(com.vaadin.data.Container.PropertySetChangeListener) + */ + @Override + public void removeListener(Container.PropertySetChangeListener listener) { + if (propertySetEventListeners != null) { + propertySetEventListeners.remove(listener); + if (propertySetEventListeners.isEmpty()) { + propertySetEventListeners = null; + } + } + } + + /** + * Adds an Item set change listener for the object. + * + * @see com.vaadin.data.Container.ItemSetChangeNotifier#addListener(com.vaadin.data.Container.ItemSetChangeListener) + */ + @Override + public void addListener(Container.ItemSetChangeListener listener) { + if (itemSetEventListeners == null) { + itemSetEventListeners = new LinkedHashSet<Container.ItemSetChangeListener>(); + } + itemSetEventListeners.add(listener); + } + + /** + * Removes the Item set change listener from the object. + * + * @see com.vaadin.data.Container.ItemSetChangeNotifier#removeListener(com.vaadin.data.Container.ItemSetChangeListener) + */ + @Override + public void removeListener(Container.ItemSetChangeListener listener) { + if (itemSetEventListeners != null) { + itemSetEventListeners.remove(listener); + if (itemSetEventListeners.isEmpty()) { + itemSetEventListeners = null; + } + } + } + + @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.vaadin.data.Container.ItemSetChangeListener#containerItemSetChange(com.vaadin.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(); + final Object[] listeners = propertySetEventListeners.toArray(); + for (int i = 0; i < listeners.length; i++) { + ((Container.PropertySetChangeListener) listeners[i]) + .containerPropertySetChange(event); + } + } + requestRepaint(); + } + + /** + * Fires the item set change event. + */ + protected void fireItemSetChange() { + if (itemSetEventListeners != null && !itemSetEventListeners.isEmpty()) { + final Container.ItemSetChangeEvent event = new ItemSetChangeEvent(); + final Object[] listeners = itemSetEventListeners.toArray(); + for (int i = 0; i < listeners.length; i++) { + ((Container.ItemSetChangeListener) listeners[i]) + .containerItemSetChange(event); + } + } + requestRepaint(); + } + + /** + * Implementation of item set change event. + */ + private class ItemSetChangeEvent implements Serializable, + Container.ItemSetChangeEvent { + + /** + * Gets the Property where the event occurred. + * + * @see com.vaadin.data.Container.ItemSetChangeEvent#getContainer() + */ + @Override + public Container getContainer() { + return AbstractSelect.this; + } + + } + + /** + * Implementation of property set change event. + */ + private class PropertySetChangeEvent implements + Container.PropertySetChangeEvent, Serializable { + + /** + * Retrieves the Container whose contents have been modified. + * + * @see com.vaadin.data.Container.PropertySetChangeEvent#getContainer() + */ + @Override + public Container getContainer() { + return AbstractSelect.this; + } + + } + + /** + * For multi-selectable fields, also an empty collection of values is + * considered to be an empty field. + * + * @see AbstractField#isEmpty(). + */ + @Override + protected boolean isEmpty() { + if (!multiSelect) { + 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; + requestRepaint(); + } + } + + /** + * 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.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) + .addListener(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) + .addListener(getCaptionChangeListener()); + captionChangeNotifiers.add(p); + } + } + + } + break; + case PROPERTY: + final Property<?> p = getContainerProperty(itemId, + getItemCaptionPropertyId()); + if (p != null && p instanceof Property.ValueChangeNotifier) { + ((Property.ValueChangeNotifier) p) + .addListener(getCaptionChangeListener()); + captionChangeNotifiers.add(p); + } + break; + + } + } + + public void clear() { + for (Iterator<Object> it = captionChangeNotifiers.iterator(); it + .hasNext();) { + Object notifier = it.next(); + if (notifier instanceof Item.PropertySetChangeNotifier) { + ((Item.PropertySetChangeNotifier) notifier) + .removeListener(getCaptionChangeListener()); + } else { + ((Property.ValueChangeNotifier) notifier) + .removeListener(getCaptionChangeListener()); + } + } + captionChangeNotifiers.clear(); + } + + @Override + public void valueChange(com.vaadin.data.Property.ValueChangeEvent event) { + requestRepaint(); + } + + @Override + public void itemPropertySetChange( + com.vaadin.data.Item.PropertySetChangeEvent event) { + requestRepaint(); + } + + } + + /** + * 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); + } +} diff --git a/server/src/com/vaadin/ui/AbstractSplitPanel.java b/server/src/com/vaadin/ui/AbstractSplitPanel.java new file mode 100644 index 0000000000..90dc38ff65 --- /dev/null +++ b/server/src/com/vaadin/ui/AbstractSplitPanel.java @@ -0,0 +1,521 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.io.Serializable; +import java.lang.reflect.Method; +import java.util.Iterator; + +import com.vaadin.event.ComponentEventListener; +import com.vaadin.event.MouseEvents.ClickEvent; +import com.vaadin.shared.MouseEventDetails; +import com.vaadin.shared.ui.splitpanel.AbstractSplitPanelRpc; +import com.vaadin.shared.ui.splitpanel.AbstractSplitPanelState; +import com.vaadin.shared.ui.splitpanel.AbstractSplitPanelState.SplitterState; +import com.vaadin.terminal.Sizeable; +import com.vaadin.terminal.gwt.client.ui.ClickEventHandler; +import com.vaadin.tools.ReflectTools; + +/** + * AbstractSplitPanel. + * + * <code>AbstractSplitPanel</code> is base class for a component container that + * can contain two components. The components are split by a divider element. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 6.5 + */ +public abstract class AbstractSplitPanel extends AbstractComponentContainer { + + // TODO use Unit in AbstractSplitPanelState and remove these + private Unit posUnit; + private Unit posMinUnit; + private Unit posMaxUnit; + + private AbstractSplitPanelRpc rpc = new AbstractSplitPanelRpc() { + + @Override + public void splitterClick(MouseEventDetails mouseDetails) { + fireEvent(new SplitterClickEvent(AbstractSplitPanel.this, + mouseDetails)); + } + + @Override + public void setSplitterPosition(float position) { + getSplitterState().setPosition(position); + } + }; + + public AbstractSplitPanel() { + registerRpc(rpc); + setSplitPosition(50, Unit.PERCENTAGE, false); + setSplitPositionLimits(0, Unit.PERCENTAGE, 100, Unit.PERCENTAGE); + } + + /** + * Modifiable and Serializable Iterator for the components, used by + * {@link AbstractSplitPanel#getComponentIterator()}. + */ + private class ComponentIterator implements Iterator<Component>, + Serializable { + + int i = 0; + + @Override + public boolean hasNext() { + if (i < getComponentCount()) { + return true; + } + return false; + } + + @Override + public Component next() { + if (!hasNext()) { + return null; + } + i++; + if (i == 1) { + return (getFirstComponent() == null ? getSecondComponent() + : getFirstComponent()); + } else if (i == 2) { + return getSecondComponent(); + } + return null; + } + + @Override + public void remove() { + if (i == 1) { + if (getFirstComponent() != null) { + setFirstComponent(null); + i = 0; + } else { + setSecondComponent(null); + } + } else if (i == 2) { + setSecondComponent(null); + } + } + } + + /** + * Add a component into this container. The component is added to the right + * or under the previous component. + * + * @param c + * the component to be added. + */ + + @Override + public void addComponent(Component c) { + if (getFirstComponent() == null) { + setFirstComponent(c); + } else if (getSecondComponent() == null) { + setSecondComponent(c); + } else { + throw new UnsupportedOperationException( + "Split panel can contain only two components"); + } + } + + /** + * Sets the first component of this split panel. Depending on the direction + * the first component is shown at the top or to the left. + * + * @param c + * The component to use as first component + */ + public void setFirstComponent(Component c) { + if (getFirstComponent() == c) { + // Nothing to do + return; + } + + if (getFirstComponent() != null) { + // detach old + removeComponent(getFirstComponent()); + } + getState().setFirstChild(c); + if (c != null) { + super.addComponent(c); + } + + requestRepaint(); + } + + /** + * Sets the second component of this split panel. Depending on the direction + * the second component is shown at the bottom or to the left. + * + * @param c + * The component to use as first component + */ + public void setSecondComponent(Component c) { + if (getSecondComponent() == c) { + // Nothing to do + return; + } + + if (getSecondComponent() != null) { + // detach old + removeComponent(getSecondComponent()); + } + getState().setSecondChild(c); + if (c != null) { + super.addComponent(c); + } + requestRepaint(); + } + + /** + * Gets the first component of this split panel. Depending on the direction + * this is either the component shown at the top or to the left. + * + * @return the first component of this split panel + */ + public Component getFirstComponent() { + return (Component) getState().getFirstChild(); + } + + /** + * Gets the second component of this split panel. Depending on the direction + * this is either the component shown at the top or to the left. + * + * @return the second component of this split panel + */ + public Component getSecondComponent() { + return (Component) getState().getSecondChild(); + } + + /** + * Removes the component from this container. + * + * @param c + * the component to be removed. + */ + + @Override + public void removeComponent(Component c) { + super.removeComponent(c); + if (c == getFirstComponent()) { + getState().setFirstChild(null); + } else if (c == getSecondComponent()) { + getState().setSecondChild(null); + } + requestRepaint(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.ComponentContainer#getComponentIterator() + */ + + @Override + public Iterator<Component> getComponentIterator() { + return new ComponentIterator(); + } + + /** + * Gets the number of contained components. Consistent with the iterator + * returned by {@link #getComponentIterator()}. + * + * @return the number of contained components (zero, one or two) + */ + + @Override + public int getComponentCount() { + int count = 0; + if (getFirstComponent() != null) { + count++; + } + if (getSecondComponent() != null) { + count++; + } + return count; + } + + /* Documented in superclass */ + + @Override + public void replaceComponent(Component oldComponent, Component newComponent) { + if (oldComponent == getFirstComponent()) { + setFirstComponent(newComponent); + } else if (oldComponent == getSecondComponent()) { + setSecondComponent(newComponent); + } + requestRepaint(); + } + + /** + * Moves the position of the splitter. + * + * @param pos + * the new size of the first region in the unit that was last + * used (default is percentage). Fractions are only allowed when + * unit is percentage. + */ + public void setSplitPosition(float pos) { + setSplitPosition(pos, posUnit, false); + } + + /** + * Moves the position of the splitter. + * + * @param pos + * the new size of the region in the unit that was last used + * (default is percentage). Fractions are only allowed when unit + * is percentage. + * + * @param reverse + * if set to true the split splitter position is measured by the + * second region else it is measured by the first region + */ + public void setSplitPosition(float pos, boolean reverse) { + setSplitPosition(pos, posUnit, reverse); + } + + /** + * Moves the position of the splitter with given position and unit. + * + * @param pos + * the new size of the first region. Fractions are only allowed + * when unit is percentage. + * @param unit + * the unit (from {@link Sizeable}) in which the size is given. + */ + public void setSplitPosition(float pos, Unit unit) { + setSplitPosition(pos, unit, false); + } + + /** + * Moves the position of the splitter with given position and unit. + * + * @param pos + * the new size of the first region. Fractions are only allowed + * when unit is percentage. + * @param unit + * the unit (from {@link Sizeable}) in which the size is given. + * @param reverse + * if set to true the split splitter position is measured by the + * second region else it is measured by the first region + * + */ + public void setSplitPosition(float pos, Unit unit, boolean reverse) { + if (unit != Unit.PERCENTAGE && unit != Unit.PIXELS) { + throw new IllegalArgumentException( + "Only percentage and pixel units are allowed"); + } + if (unit != Unit.PERCENTAGE) { + pos = Math.round(pos); + } + SplitterState splitterState = getSplitterState(); + splitterState.setPosition(pos); + splitterState.setPositionUnit(unit.getSymbol()); + splitterState.setPositionReversed(reverse); + posUnit = unit; + + requestRepaint(); + } + + /** + * Returns the current position of the splitter, in + * {@link #getSplitPositionUnit()} units. + * + * @return position of the splitter + */ + public float getSplitPosition() { + return getSplitterState().getPosition(); + } + + /** + * Returns the unit of position of the splitter + * + * @return unit of position of the splitter + */ + public Unit getSplitPositionUnit() { + return posUnit; + } + + /** + * Sets the minimum split position to the given position and unit. If the + * split position is reversed, maximum and minimum are also reversed. + * + * @param pos + * the minimum position of the split + * @param unit + * the unit (from {@link Sizeable}) in which the size is given. + * Allowed units are UNITS_PERCENTAGE and UNITS_PIXELS + */ + public void setMinSplitPosition(int pos, Unit unit) { + setSplitPositionLimits(pos, unit, getSplitterState().getMaxPosition(), + posMaxUnit); + } + + /** + * Returns the current minimum position of the splitter, in + * {@link #getMinSplitPositionUnit()} units. + * + * @return the minimum position of the splitter + */ + public float getMinSplitPosition() { + return getSplitterState().getMinPosition(); + } + + /** + * Returns the unit of the minimum position of the splitter. + * + * @return the unit of the minimum position of the splitter + */ + public Unit getMinSplitPositionUnit() { + return posMinUnit; + } + + /** + * Sets the maximum split position to the given position and unit. If the + * split position is reversed, maximum and minimum are also reversed. + * + * @param pos + * the maximum position of the split + * @param unit + * the unit (from {@link Sizeable}) in which the size is given. + * Allowed units are UNITS_PERCENTAGE and UNITS_PIXELS + */ + public void setMaxSplitPosition(float pos, Unit unit) { + setSplitPositionLimits(getSplitterState().getMinPosition(), posMinUnit, + pos, unit); + } + + /** + * Returns the current maximum position of the splitter, in + * {@link #getMaxSplitPositionUnit()} units. + * + * @return the maximum position of the splitter + */ + public float getMaxSplitPosition() { + return getSplitterState().getMaxPosition(); + } + + /** + * Returns the unit of the maximum position of the splitter + * + * @return the unit of the maximum position of the splitter + */ + public Unit getMaxSplitPositionUnit() { + return posMaxUnit; + } + + /** + * Sets the maximum and minimum position of the splitter. If the split + * position is reversed, maximum and minimum are also reversed. + * + * @param minPos + * the new minimum position + * @param minPosUnit + * the unit (from {@link Sizeable}) in which the minimum position + * is given. + * @param maxPos + * the new maximum position + * @param maxPosUnit + * the unit (from {@link Sizeable}) in which the maximum position + * is given. + */ + private void setSplitPositionLimits(float minPos, Unit minPosUnit, + float maxPos, Unit maxPosUnit) { + if ((minPosUnit != Unit.PERCENTAGE && minPosUnit != Unit.PIXELS) + || (maxPosUnit != Unit.PERCENTAGE && maxPosUnit != Unit.PIXELS)) { + throw new IllegalArgumentException( + "Only percentage and pixel units are allowed"); + } + + SplitterState state = getSplitterState(); + + state.setMinPosition(minPos); + state.setMinPositionUnit(minPosUnit.getSymbol()); + posMinUnit = minPosUnit; + + state.setMaxPosition(maxPos); + state.setMaxPositionUnit(maxPosUnit.getSymbol()); + posMaxUnit = maxPosUnit; + + requestRepaint(); + } + + /** + * Lock the SplitPanels position, disabling the user from dragging the split + * handle. + * + * @param locked + * Set <code>true</code> if locked, <code>false</code> otherwise. + */ + public void setLocked(boolean locked) { + getSplitterState().setLocked(locked); + requestRepaint(); + } + + /** + * Is the SplitPanel handle locked (user not allowed to change split + * position by dragging). + * + * @return <code>true</code> if locked, <code>false</code> otherwise. + */ + public boolean isLocked() { + return getSplitterState().isLocked(); + } + + /** + * <code>SplitterClickListener</code> interface for listening for + * <code>SplitterClickEvent</code> fired by a <code>SplitPanel</code>. + * + * @see SplitterClickEvent + * @since 6.2 + */ + public interface SplitterClickListener extends ComponentEventListener { + + public static final Method clickMethod = ReflectTools.findMethod( + SplitterClickListener.class, "splitterClick", + SplitterClickEvent.class); + + /** + * SplitPanel splitter has been clicked + * + * @param event + * SplitterClickEvent event. + */ + public void splitterClick(SplitterClickEvent event); + } + + public class SplitterClickEvent extends ClickEvent { + + public SplitterClickEvent(Component source, + MouseEventDetails mouseEventDetails) { + super(source, mouseEventDetails); + } + + } + + public void addListener(SplitterClickListener listener) { + addListener(ClickEventHandler.CLICK_EVENT_IDENTIFIER, + SplitterClickEvent.class, listener, + SplitterClickListener.clickMethod); + } + + public void removeListener(SplitterClickListener listener) { + removeListener(ClickEventHandler.CLICK_EVENT_IDENTIFIER, + SplitterClickEvent.class, listener); + } + + @Override + public AbstractSplitPanelState getState() { + return (AbstractSplitPanelState) super.getState(); + } + + private SplitterState getSplitterState() { + return getState().getSplitterState(); + } +} diff --git a/server/src/com/vaadin/ui/AbstractTextField.java b/server/src/com/vaadin/ui/AbstractTextField.java new file mode 100644 index 0000000000..2326c07d97 --- /dev/null +++ b/server/src/com/vaadin/ui/AbstractTextField.java @@ -0,0 +1,674 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.util.Map; + +import com.vaadin.event.FieldEvents.BlurEvent; +import com.vaadin.event.FieldEvents.BlurListener; +import com.vaadin.event.FieldEvents.BlurNotifier; +import com.vaadin.event.FieldEvents.FocusEvent; +import com.vaadin.event.FieldEvents.FocusListener; +import com.vaadin.event.FieldEvents.FocusNotifier; +import com.vaadin.event.FieldEvents.TextChangeEvent; +import com.vaadin.event.FieldEvents.TextChangeListener; +import com.vaadin.event.FieldEvents.TextChangeNotifier; +import com.vaadin.shared.ui.textfield.AbstractTextFieldState; +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.PaintTarget; +import com.vaadin.terminal.Vaadin6Component; +import com.vaadin.terminal.gwt.client.ui.textfield.VTextField; + +public abstract class AbstractTextField extends AbstractField<String> implements + BlurNotifier, FocusNotifier, TextChangeNotifier, Vaadin6Component { + + /** + * Null representation. + */ + private String nullRepresentation = "null"; + /** + * Is setting to null from non-null value allowed by setting with null + * representation . + */ + private boolean nullSettingAllowed = false; + /** + * The text content when the last messages to the server was sent. Cleared + * when value is changed. + */ + private String lastKnownTextContent; + + /** + * The position of the cursor when the last message to the server was sent. + */ + private int lastKnownCursorPosition; + + /** + * Flag indicating that a text change event is pending to be triggered. + * Cleared by {@link #setInternalValue(Object)} and when the event is fired. + */ + private boolean textChangeEventPending; + + private boolean isFiringTextChangeEvent = false; + + private TextChangeEventMode textChangeEventMode = TextChangeEventMode.LAZY; + + private final int DEFAULT_TEXTCHANGE_TIMEOUT = 400; + + private int textChangeEventTimeout = DEFAULT_TEXTCHANGE_TIMEOUT; + + /** + * Temporarily holds the new selection position. Cleared on paint. + */ + private int selectionPosition = -1; + + /** + * Temporarily holds the new selection length. + */ + private int selectionLength; + + /** + * Flag used to determine whether we are currently handling a state change + * triggered by a user. Used to properly fire text change event before value + * change event triggered by the client side. + */ + private boolean changingVariables; + + protected AbstractTextField() { + super(); + } + + @Override + public AbstractTextFieldState getState() { + return (AbstractTextFieldState) super.getState(); + } + + @Override + public void updateState() { + super.updateState(); + + String value = getValue(); + if (value == null) { + value = getNullRepresentation(); + } + getState().setText(value); + } + + @Override + public void paintContent(PaintTarget target) throws PaintException { + + if (selectionPosition != -1) { + target.addAttribute("selpos", selectionPosition); + target.addAttribute("sellen", selectionLength); + selectionPosition = -1; + } + + if (hasListeners(TextChangeEvent.class)) { + target.addAttribute(VTextField.ATTR_TEXTCHANGE_EVENTMODE, + getTextChangeEventMode().toString()); + target.addAttribute(VTextField.ATTR_TEXTCHANGE_TIMEOUT, + getTextChangeTimeout()); + if (lastKnownTextContent != null) { + /* + * The field has be repainted for some reason (e.g. caption, + * size, stylename), but the value has not been changed since + * the last text change event. Let the client side know about + * the value the server side knows. Client side may then ignore + * the actual value, depending on its state. + */ + target.addAttribute( + VTextField.ATTR_NO_VALUE_CHANGE_BETWEEN_PAINTS, true); + } + } + + } + + @Override + public void changeVariables(Object source, Map<String, Object> variables) { + changingVariables = true; + + try { + + if (variables.containsKey(VTextField.VAR_CURSOR)) { + Integer object = (Integer) variables.get(VTextField.VAR_CURSOR); + lastKnownCursorPosition = object.intValue(); + } + + if (variables.containsKey(VTextField.VAR_CUR_TEXT)) { + /* + * NOTE, we might want to develop this further so that on a + * value change event the whole text content don't need to be + * sent from the client to server. Just "commit" the value from + * currentText to the value. + */ + handleInputEventTextChange(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"); + + // server side check for max length + if (getMaxLength() != -1 && newValue.length() > getMaxLength()) { + newValue = newValue.substring(0, getMaxLength()); + } + 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, or if we have a + // formatter, repaint is needed after all. + if (wasModified != isModified()) { + requestRepaint(); + } + } + } + firePendingTextChangeEvent(); + + if (variables.containsKey(FocusEvent.EVENT_ID)) { + fireEvent(new FocusEvent(this)); + } + if (variables.containsKey(BlurEvent.EVENT_ID)) { + fireEvent(new BlurEvent(this)); + } + } finally { + changingVariables = false; + + } + + } + + @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; + requestRepaint(); + } + + /** + * 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 representation always be converted to + * null-values. + * @see TextField#getNullRepresentation() + */ + public void setNullSettingAllowed(boolean nullSettingAllowed) { + this.nullSettingAllowed = nullSettingAllowed; + requestRepaint(); + } + + @Override + protected boolean isEmpty() { + return super.isEmpty() || getValue().length() == 0; + } + + /** + * Returns the maximum number of characters in the field. Value -1 is + * considered unlimited. Terminal may however have some technical limits. + * + * @return the maxLength + */ + public int getMaxLength() { + return getState().getMaxLength(); + } + + /** + * Sets the maximum number of characters in the field. Value -1 is + * considered unlimited. Terminal may however have some technical limits. + * + * @param maxLength + * the maxLength to set + */ + public void setMaxLength(int maxLength) { + getState().setMaxLength(maxLength); + requestRepaint(); + } + + /** + * Gets the number of columns in the editor. If the number of columns is set + * 0, the actual number of displayed columns is determined implicitly by the + * adapter. + * + * @return the number of columns in the editor. + */ + public int getColumns() { + return getState().getColumns(); + } + + /** + * Sets the number of columns in the editor. If the number of columns is set + * 0, the actual number of displayed columns is determined implicitly by the + * adapter. + * + * @param columns + * the number of columns to set. + */ + public void setColumns(int columns) { + if (columns < 0) { + columns = 0; + } + getState().setColumns(columns); + requestRepaint(); + } + + /** + * Gets the current input prompt. + * + * @see #setInputPrompt(String) + * @return the current input prompt, or null if not enabled + */ + public String getInputPrompt() { + return getState().getInputPrompt(); + } + + /** + * Sets the input prompt - a textual prompt that is displayed when the field + * would otherwise be empty, to prompt the user for input. + * + * @param inputPrompt + */ + public void setInputPrompt(String inputPrompt) { + getState().setInputPrompt(inputPrompt); + requestRepaint(); + } + + /* ** Text Change Events ** */ + + private void firePendingTextChangeEvent() { + if (textChangeEventPending && !isFiringTextChangeEvent) { + isFiringTextChangeEvent = true; + textChangeEventPending = false; + try { + fireEvent(new TextChangeEventImpl(this)); + } finally { + isFiringTextChangeEvent = false; + } + } + } + + @Override + protected void setInternalValue(String newValue) { + if (changingVariables && !textChangeEventPending) { + + /* + * TODO check for possible (minor?) issue (not tested) + * + * -field with e.g. PropertyFormatter. + * + * -TextChangeListener and it changes value. + * + * -if formatter again changes the value, do we get an extra + * simulated text change event ? + */ + + /* + * Fire a "simulated" text change event before value change event if + * change is coming from the client side. + * + * Iff there is both value change and textChangeEvent in same + * variable burst, it is a text field in non immediate mode and the + * text change event "flushed" queued value change event. In this + * case textChangeEventPending flag is already on and text change + * event will be fired after the value change event. + */ + if (newValue == null && lastKnownTextContent != null + && !lastKnownTextContent.equals(getNullRepresentation())) { + // Value was changed from something to null representation + lastKnownTextContent = getNullRepresentation(); + textChangeEventPending = true; + } else if (newValue != null + && !newValue.toString().equals(lastKnownTextContent)) { + // Value was changed to something else than null representation + lastKnownTextContent = newValue.toString(); + textChangeEventPending = true; + } + firePendingTextChangeEvent(); + } + + super.setInternalValue(newValue); + } + + @Override + public void setValue(Object newValue) throws ReadOnlyException { + super.setValue(newValue); + /* + * Make sure w reset lastKnownTextContent field on value change. The + * clearing must happen here as well because TextChangeListener can + * revert the original value. Client must respect the value in this + * case. AbstractField optimizes value change if the existing value is + * reset. Also we need to force repaint if the flag is on. + */ + if (lastKnownTextContent != null) { + lastKnownTextContent = null; + requestRepaint(); + } + } + + private void handleInputEventTextChange(Map<String, Object> variables) { + /* + * TODO we could vastly optimize the communication of values by using + * some sort of diffs instead of always sending the whole text content. + * Also on value change events we could use the mechanism. + */ + String object = (String) variables.get(VTextField.VAR_CUR_TEXT); + lastKnownTextContent = object; + textChangeEventPending = true; + } + + /** + * Sets the mode how the TextField triggers {@link TextChangeEvent}s. + * + * @param inputEventMode + * the new mode + * + * @see TextChangeEventMode + */ + public void setTextChangeEventMode(TextChangeEventMode inputEventMode) { + textChangeEventMode = inputEventMode; + requestRepaint(); + } + + /** + * @return the mode used to trigger {@link TextChangeEvent}s. + */ + public TextChangeEventMode getTextChangeEventMode() { + return textChangeEventMode; + } + + /** + * Different modes how the TextField can trigger {@link TextChangeEvent}s. + */ + public enum TextChangeEventMode { + + /** + * An event is triggered on each text content change, most commonly key + * press events. + */ + EAGER, + /** + * Each text change event in the UI causes the event to be communicated + * to the application after a timeout. The length of the timeout can be + * controlled with {@link TextField#setInputEventTimeout(int)}. Only the + * last input event is reported to the server side if several text + * change events happen during the timeout. + * <p> + * In case of a {@link ValueChangeEvent} the schedule is not kept + * strictly. Before a {@link ValueChangeEvent} a {@link TextChangeEvent} + * is triggered if the text content has changed since the previous + * TextChangeEvent regardless of the schedule. + */ + TIMEOUT, + /** + * An event is triggered when there is a pause of text modifications. + * The length of the pause can be modified with + * {@link TextField#setInputEventTimeout(int)}. Like with the + * {@link #TIMEOUT} mode, an event is forced before + * {@link ValueChangeEvent}s, even if the user did not keep a pause + * while entering the text. + * <p> + * This is the default mode. + */ + LAZY + } + + @Override + public void addListener(TextChangeListener listener) { + addListener(TextChangeListener.EVENT_ID, TextChangeEvent.class, + listener, TextChangeListener.EVENT_METHOD); + } + + @Override + public void removeListener(TextChangeListener listener) { + removeListener(TextChangeListener.EVENT_ID, TextChangeEvent.class, + listener); + } + + /** + * The text change timeout modifies how often text change events are + * communicated to the application when {@link #getTextChangeEventMode()} is + * {@link TextChangeEventMode#LAZY} or {@link TextChangeEventMode#TIMEOUT}. + * + * + * @see #getTextChangeEventMode() + * + * @param timeout + * the timeout in milliseconds + */ + public void setTextChangeTimeout(int timeout) { + textChangeEventTimeout = timeout; + requestRepaint(); + } + + /** + * Gets the timeout used to fire {@link TextChangeEvent}s when the + * {@link #getTextChangeEventMode()} is {@link TextChangeEventMode#LAZY} or + * {@link TextChangeEventMode#TIMEOUT}. + * + * @return the timeout value in milliseconds + */ + public int getTextChangeTimeout() { + return textChangeEventTimeout; + } + + public class TextChangeEventImpl extends TextChangeEvent { + private String curText; + private int cursorPosition; + + private TextChangeEventImpl(final AbstractTextField tf) { + super(tf); + curText = tf.getCurrentTextContent(); + cursorPosition = tf.getCursorPosition(); + } + + @Override + public AbstractTextField getComponent() { + return (AbstractTextField) super.getComponent(); + } + + @Override + public String getText() { + return curText; + } + + @Override + public int getCursorPosition() { + return cursorPosition; + } + + } + + /** + * Gets the current (or the last known) text content in the field. + * <p> + * Note the text returned by this method is not necessary the same that is + * returned by the {@link #getValue()} method. The value is updated when the + * terminal fires a value change event via e.g. blurring the field or by + * pressing enter. The value returned by this method is updated also on + * {@link TextChangeEvent}s. Due to this high dependency to the terminal + * implementation this method is (at least at this point) not published. + * + * @return the text which is currently displayed in the field. + */ + private String getCurrentTextContent() { + if (lastKnownTextContent != null) { + return lastKnownTextContent; + } else { + Object text = getValue(); + if (text == null) { + return getNullRepresentation(); + } + return text.toString(); + } + } + + /** + * Selects all text in the field. + * + * @since 6.4 + */ + public void selectAll() { + String text = getValue() == null ? "" : getValue().toString(); + setSelectionRange(0, text.length()); + } + + /** + * Sets the range of text to be selected. + * + * As a side effect the field will become focused. + * + * @since 6.4 + * + * @param pos + * the position of the first character to be selected + * @param length + * the number of characters to be selected + */ + public void setSelectionRange(int pos, int length) { + selectionPosition = pos; + selectionLength = length; + focus(); + requestRepaint(); + } + + /** + * Sets the cursor position in the field. As a side effect the field will + * become focused. + * + * @since 6.4 + * + * @param pos + * the position for the cursor + * */ + public void setCursorPosition(int pos) { + setSelectionRange(pos, 0); + lastKnownCursorPosition = pos; + } + + /** + * Returns the last known cursor position of the field. + * + * <p> + * Note that due to the client server nature or the GWT terminal, Vaadin + * cannot provide the exact value of the cursor position in most situations. + * The value is updated only when the client side terminal communicates to + * TextField, like on {@link ValueChangeEvent}s and {@link TextChangeEvent} + * s. This may change later if a deep push integration is built to Vaadin. + * + * @return the cursor position + */ + public int getCursorPosition() { + return lastKnownCursorPosition; + } + + @Override + public void addListener(FocusListener listener) { + addListener(FocusEvent.EVENT_ID, FocusEvent.class, listener, + FocusListener.focusMethod); + } + + @Override + public void removeListener(FocusListener listener) { + removeListener(FocusEvent.EVENT_ID, FocusEvent.class, listener); + } + + @Override + public void addListener(BlurListener listener) { + addListener(BlurEvent.EVENT_ID, BlurEvent.class, listener, + BlurListener.blurMethod); + } + + @Override + public void removeListener(BlurListener listener) { + removeListener(BlurEvent.EVENT_ID, BlurEvent.class, listener); + } + +} diff --git a/server/src/com/vaadin/ui/Accordion.java b/server/src/com/vaadin/ui/Accordion.java new file mode 100644 index 0000000000..b937c7bc2b --- /dev/null +++ b/server/src/com/vaadin/ui/Accordion.java @@ -0,0 +1,19 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.ui; + +/** + * An accordion is a component similar to a {@link TabSheet}, but with a + * vertical orientation and the selected component presented between tabs. + * + * Closable tabs are not supported by the accordion. + * + * The {@link Accordion} can be styled with the .v-accordion, .v-accordion-item, + * .v-accordion-item-first and .v-accordion-item-caption styles. + * + * @see TabSheet + */ +public class Accordion extends TabSheet { + +} diff --git a/server/src/com/vaadin/ui/Alignment.java b/server/src/com/vaadin/ui/Alignment.java new file mode 100644 index 0000000000..0d73da8504 --- /dev/null +++ b/server/src/com/vaadin/ui/Alignment.java @@ -0,0 +1,158 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.ui; + +import java.io.Serializable; + +import com.vaadin.shared.ui.AlignmentInfo.Bits; + +/** + * Class containing information about alignment of a component. Use the + * pre-instantiated classes. + */ +@SuppressWarnings("serial") +public final class Alignment implements Serializable { + + public static final Alignment TOP_RIGHT = new Alignment(Bits.ALIGNMENT_TOP + + Bits.ALIGNMENT_RIGHT); + public static final Alignment TOP_LEFT = new Alignment(Bits.ALIGNMENT_TOP + + Bits.ALIGNMENT_LEFT); + public static final Alignment TOP_CENTER = new Alignment(Bits.ALIGNMENT_TOP + + Bits.ALIGNMENT_HORIZONTAL_CENTER); + public static final Alignment MIDDLE_RIGHT = new Alignment( + Bits.ALIGNMENT_VERTICAL_CENTER + Bits.ALIGNMENT_RIGHT); + public static final Alignment MIDDLE_LEFT = new Alignment( + Bits.ALIGNMENT_VERTICAL_CENTER + Bits.ALIGNMENT_LEFT); + public static final Alignment MIDDLE_CENTER = new Alignment( + Bits.ALIGNMENT_VERTICAL_CENTER + Bits.ALIGNMENT_HORIZONTAL_CENTER); + public static final Alignment BOTTOM_RIGHT = new Alignment( + Bits.ALIGNMENT_BOTTOM + Bits.ALIGNMENT_RIGHT); + public static final Alignment BOTTOM_LEFT = new Alignment( + Bits.ALIGNMENT_BOTTOM + Bits.ALIGNMENT_LEFT); + public static final Alignment BOTTOM_CENTER = new Alignment( + Bits.ALIGNMENT_BOTTOM + Bits.ALIGNMENT_HORIZONTAL_CENTER); + + private final int bitMask; + + public Alignment(int bitMask) { + this.bitMask = bitMask; + } + + /** + * Returns a bitmask representation of the alignment value. Used internally + * by terminal. + * + * @return the bitmask representation of the alignment value + */ + public int getBitMask() { + return bitMask; + } + + /** + * Checks if component is aligned to the top of the available space. + * + * @return true if aligned top + */ + public boolean isTop() { + return (bitMask & Bits.ALIGNMENT_TOP) == Bits.ALIGNMENT_TOP; + } + + /** + * Checks if component is aligned to the bottom of the available space. + * + * @return true if aligned bottom + */ + public boolean isBottom() { + return (bitMask & Bits.ALIGNMENT_BOTTOM) == Bits.ALIGNMENT_BOTTOM; + } + + /** + * Checks if component is aligned to the left of the available space. + * + * @return true if aligned left + */ + public boolean isLeft() { + return (bitMask & Bits.ALIGNMENT_LEFT) == Bits.ALIGNMENT_LEFT; + } + + /** + * Checks if component is aligned to the right of the available space. + * + * @return true if aligned right + */ + public boolean isRight() { + return (bitMask & Bits.ALIGNMENT_RIGHT) == Bits.ALIGNMENT_RIGHT; + } + + /** + * Checks if component is aligned middle (vertically center) of the + * available space. + * + * @return true if aligned bottom + */ + public boolean isMiddle() { + return (bitMask & Bits.ALIGNMENT_VERTICAL_CENTER) == Bits.ALIGNMENT_VERTICAL_CENTER; + } + + /** + * Checks if component is aligned center (horizontally) of the available + * space. + * + * @return true if aligned center + */ + public boolean isCenter() { + return (bitMask & Bits.ALIGNMENT_HORIZONTAL_CENTER) == Bits.ALIGNMENT_HORIZONTAL_CENTER; + } + + /** + * Returns string representation of vertical alignment. + * + * @return vertical alignment as CSS value + */ + public String getVerticalAlignment() { + if (isBottom()) { + return "bottom"; + } else if (isMiddle()) { + return "middle"; + } + return "top"; + } + + /** + * Returns string representation of horizontal alignment. + * + * @return horizontal alignment as CSS value + */ + public String getHorizontalAlignment() { + if (isRight()) { + return "right"; + } else if (isCenter()) { + return "center"; + } + return "left"; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if ((obj == null) || (obj.getClass() != this.getClass())) { + return false; + } + Alignment a = (Alignment) obj; + return bitMask == a.bitMask; + } + + @Override + public int hashCode() { + return bitMask; + } + + @Override + public String toString() { + return String.valueOf(bitMask); + } + +} diff --git a/server/src/com/vaadin/ui/Audio.java b/server/src/com/vaadin/ui/Audio.java new file mode 100644 index 0000000000..ac2ee869a6 --- /dev/null +++ b/server/src/com/vaadin/ui/Audio.java @@ -0,0 +1,55 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import com.vaadin.terminal.Resource; + +/** + * The Audio component translates into an HTML5 <audio> element and as + * such is only supported in browsers that support HTML5 media markup. Browsers + * that do not support HTML5 display the text or HTML set by calling + * {@link #setAltText(String)}. + * + * A flash-player fallback can be implemented by setting HTML content allowed ( + * {@link #setHtmlContentAllowed(boolean)} and calling + * {@link #setAltText(String)} with the flash player markup. An example of flash + * fallback can be found at the <a href= + * "https://developer.mozilla.org/En/Using_audio_and_video_in_Firefox#Using_Flash" + * >Mozilla Developer Network</a>. + * + * Multiple sources can be specified. Which of the sources is used is selected + * by the browser depending on which file formats it supports. See <a + * href="http://en.wikipedia.org/wiki/HTML5_video#Table">wikipedia</a> for a + * table of formats supported by different browsers. + * + * @author Vaadin Ltd + * @since 6.7.0 + */ +public class Audio extends AbstractMedia { + + public Audio() { + this("", null); + } + + /** + * @param caption + * The caption of the audio component. + */ + public Audio(String caption) { + this(caption, null); + } + + /** + * @param caption + * The caption of the audio component + * @param source + * The audio file to play. + */ + public Audio(String caption, Resource source) { + setCaption(caption); + setSource(source); + setShowControls(true); + } +} diff --git a/server/src/com/vaadin/ui/Button.java b/server/src/com/vaadin/ui/Button.java new file mode 100644 index 0000000000..0cb667d527 --- /dev/null +++ b/server/src/com/vaadin/ui/Button.java @@ -0,0 +1,539 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.io.Serializable; +import java.lang.reflect.Method; + +import com.vaadin.event.Action; +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.event.ShortcutAction; +import com.vaadin.event.ShortcutAction.KeyCode; +import com.vaadin.event.ShortcutAction.ModifierKey; +import com.vaadin.event.ShortcutListener; +import com.vaadin.shared.MouseEventDetails; +import com.vaadin.shared.ui.button.ButtonServerRpc; +import com.vaadin.shared.ui.button.ButtonState; +import com.vaadin.tools.ReflectTools; +import com.vaadin.ui.Component.Focusable; + +/** + * A generic button component. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +@SuppressWarnings("serial") +public class Button extends AbstractComponent implements + FieldEvents.BlurNotifier, FieldEvents.FocusNotifier, Focusable, + Action.ShortcutNotifier { + + private ButtonServerRpc rpc = new ButtonServerRpc() { + + @Override + public void click(MouseEventDetails mouseEventDetails) { + fireClick(mouseEventDetails); + } + + @Override + public void disableOnClick() { + // Could be optimized so the button is not repainted because of + // this (client side has already disabled the button) + setEnabled(false); + } + }; + + FocusAndBlurServerRpcImpl focusBlurRpc = new FocusAndBlurServerRpcImpl(this) { + + @Override + protected void fireEvent(Event event) { + Button.this.fireEvent(event); + } + }; + + /** + * Creates a new push button. + */ + public Button() { + registerRpc(rpc); + registerRpc(focusBlurRpc); + } + + /** + * Creates a new push button with the given caption. + * + * @param caption + * the Button caption. + */ + public Button(String caption) { + this(); + setCaption(caption); + } + + /** + * Creates a new push button with a click listener. + * + * @param caption + * the Button caption. + * @param listener + * the Button click listener. + */ + public Button(String caption, ClickListener listener) { + this(caption); + addListener(listener); + } + + /** + * Click event. This event is thrown, when the button is clicked. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ + public class ClickEvent extends Component.Event { + + private final MouseEventDetails details; + + /** + * New instance of text change event. + * + * @param source + * the Source of the event. + */ + public ClickEvent(Component source) { + super(source); + details = null; + } + + /** + * Constructor with mouse details + * + * @param source + * The source where the click took place + * @param details + * Details about the mouse click + */ + public ClickEvent(Component source, MouseEventDetails details) { + super(source); + this.details = details; + } + + /** + * Gets the Button where the event occurred. + * + * @return the Source of the event. + */ + public Button getButton() { + return (Button) getSource(); + } + + /** + * Returns the mouse position (x coordinate) when the click took place. + * The position is relative to the browser client area. + * + * @return The mouse cursor x position or -1 if unknown + */ + public int getClientX() { + if (null != details) { + return details.getClientX(); + } else { + return -1; + } + } + + /** + * Returns the mouse position (y coordinate) when the click took place. + * The position is relative to the browser client area. + * + * @return The mouse cursor y position or -1 if unknown + */ + public int getClientY() { + if (null != details) { + return details.getClientY(); + } else { + return -1; + } + } + + /** + * Returns the relative mouse position (x coordinate) when the click + * took place. The position is relative to the clicked component. + * + * @return The mouse cursor x position relative to the clicked layout + * component or -1 if no x coordinate available + */ + public int getRelativeX() { + if (null != details) { + return details.getRelativeX(); + } else { + return -1; + } + } + + /** + * Returns the relative mouse position (y coordinate) when the click + * took place. The position is relative to the clicked component. + * + * @return The mouse cursor y position relative to the clicked layout + * component or -1 if no y coordinate available + */ + public int getRelativeY() { + if (null != details) { + return details.getRelativeY(); + } else { + return -1; + } + } + + /** + * Checks if the Alt key was down when the mouse event took place. + * + * @return true if Alt was down when the event occured, false otherwise + * or if unknown + */ + public boolean isAltKey() { + if (null != details) { + return details.isAltKey(); + } else { + return false; + } + } + + /** + * Checks if the Ctrl key was down when the mouse event took place. + * + * @return true if Ctrl was pressed when the event occured, false + * otherwise or if unknown + */ + public boolean isCtrlKey() { + if (null != details) { + return details.isCtrlKey(); + } else { + return false; + } + } + + /** + * Checks if the Meta key was down when the mouse event took place. + * + * @return true if Meta was pressed when the event occured, false + * otherwise or if unknown + */ + public boolean isMetaKey() { + if (null != details) { + return details.isMetaKey(); + } else { + return false; + } + } + + /** + * Checks if the Shift key was down when the mouse event took place. + * + * @return true if Shift was pressed when the event occured, false + * otherwise or if unknown + */ + public boolean isShiftKey() { + if (null != details) { + return details.isShiftKey(); + } else { + return false; + } + } + } + + /** + * Interface for listening for a {@link ClickEvent} fired by a + * {@link Component}. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ + public interface ClickListener extends Serializable { + + public static final Method BUTTON_CLICK_METHOD = ReflectTools + .findMethod(ClickListener.class, "buttonClick", + ClickEvent.class); + + /** + * Called when a {@link Button} has been clicked. A reference to the + * button is given by {@link ClickEvent#getButton()}. + * + * @param event + * An event containing information about the click. + */ + public void buttonClick(ClickEvent event); + + } + + /** + * Adds the button click listener. + * + * @param listener + * the Listener to be added. + */ + public void addListener(ClickListener listener) { + addListener(ClickEvent.class, listener, + ClickListener.BUTTON_CLICK_METHOD); + } + + /** + * Removes the button click listener. + * + * @param listener + * the Listener to be removed. + */ + public void removeListener(ClickListener listener) { + removeListener(ClickEvent.class, listener, + ClickListener.BUTTON_CLICK_METHOD); + } + + /** + * Simulates a button click, notifying all server-side listeners. + * + * No action is taken is the button is disabled. + */ + public void click() { + if (isEnabled() && !isReadOnly()) { + fireClick(); + } + } + + /** + * Fires a click event to all listeners without any event details. + * + * In subclasses, override {@link #fireClick(MouseEventDetails)} instead of + * this method. + */ + protected void fireClick() { + fireEvent(new Button.ClickEvent(this)); + } + + /** + * Fires a click event to all listeners. + * + * @param details + * MouseEventDetails from which keyboard modifiers and other + * information about the mouse click can be obtained. If the + * button was clicked by a keyboard event, some of the fields may + * be empty/undefined. + */ + protected void fireClick(MouseEventDetails details) { + fireEvent(new Button.ClickEvent(this, details)); + } + + @Override + public void addListener(BlurListener listener) { + addListener(BlurEvent.EVENT_ID, BlurEvent.class, listener, + BlurListener.blurMethod); + } + + @Override + public void removeListener(BlurListener listener) { + removeListener(BlurEvent.EVENT_ID, BlurEvent.class, listener); + } + + @Override + public void addListener(FocusListener listener) { + addListener(FocusEvent.EVENT_ID, FocusEvent.class, listener, + FocusListener.focusMethod); + } + + @Override + public void removeListener(FocusListener listener) { + removeListener(FocusEvent.EVENT_ID, FocusEvent.class, listener); + + } + + /* + * Actions + */ + + protected ClickShortcut clickShortcut; + + /** + * Makes it possible to invoke a click on this button by pressing the given + * {@link KeyCode} and (optional) {@link ModifierKey}s.<br/> + * The shortcut is global (bound to the containing Window). + * + * @param keyCode + * the keycode for invoking the shortcut + * @param modifiers + * the (optional) modifiers for invoking the shortcut, null for + * none + */ + public void setClickShortcut(int keyCode, int... modifiers) { + if (clickShortcut != null) { + removeShortcutListener(clickShortcut); + } + clickShortcut = new ClickShortcut(this, keyCode, modifiers); + addShortcutListener(clickShortcut); + getState().setClickShortcutKeyCode(clickShortcut.getKeyCode()); + } + + /** + * Removes the keyboard shortcut previously set with + * {@link #setClickShortcut(int, int...)}. + */ + public void removeClickShortcut() { + if (clickShortcut != null) { + removeShortcutListener(clickShortcut); + clickShortcut = null; + getState().setClickShortcutKeyCode(0); + } + } + + /** + * A {@link ShortcutListener} specifically made to define a keyboard + * shortcut that invokes a click on the given button. + * + */ + public static class ClickShortcut extends ShortcutListener { + protected Button button; + + /** + * Creates a keyboard shortcut for clicking the given button using the + * shorthand notation defined in {@link ShortcutAction}. + * + * @param button + * to be clicked when the shortcut is invoked + * @param shorthandCaption + * the caption with shortcut keycode and modifiers indicated + */ + public ClickShortcut(Button button, String shorthandCaption) { + super(shorthandCaption); + this.button = button; + } + + /** + * Creates a keyboard shortcut for clicking the given button using the + * given {@link KeyCode} and {@link ModifierKey}s. + * + * @param button + * to be clicked when the shortcut is invoked + * @param keyCode + * KeyCode to react to + * @param modifiers + * optional modifiers for shortcut + */ + public ClickShortcut(Button button, int keyCode, int... modifiers) { + super(null, keyCode, modifiers); + this.button = button; + } + + /** + * Creates a keyboard shortcut for clicking the given button using the + * given {@link KeyCode}. + * + * @param button + * to be clicked when the shortcut is invoked + * @param keyCode + * KeyCode to react to + */ + public ClickShortcut(Button button, int keyCode) { + this(button, keyCode, null); + } + + @Override + public void handleAction(Object sender, Object target) { + button.click(); + } + } + + /** + * Determines if a button is automatically disabled when clicked. See + * {@link #setDisableOnClick(boolean)} for details. + * + * @return true if the button is disabled when clicked, false otherwise + */ + public boolean isDisableOnClick() { + return getState().isDisableOnClick(); + } + + /** + * Determines if a button is automatically disabled when clicked. If this is + * set to true the button will be automatically disabled when clicked, + * typically to prevent (accidental) extra clicks on a button. + * + * @param disableOnClick + * true to disable button when it is clicked, false otherwise + */ + public void setDisableOnClick(boolean disableOnClick) { + getState().setDisableOnClick(disableOnClick); + requestRepaint(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.Component.Focusable#getTabIndex() + */ + @Override + public int getTabIndex() { + return getState().getTabIndex(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.Component.Focusable#setTabIndex(int) + */ + @Override + public void setTabIndex(int tabIndex) { + getState().setTabIndex(tabIndex); + requestRepaint(); + } + + @Override + public void focus() { + // Overridden only to make public + super.focus(); + } + + @Override + public ButtonState getState() { + return (ButtonState) super.getState(); + } + + /** + * Set whether the caption text is rendered as HTML or not. You might need + * to retheme button 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 + */ + public void setHtmlContentAllowed(boolean htmlContentAllowed) { + if (getState().isHtmlContentAllowed() != htmlContentAllowed) { + getState().setHtmlContentAllowed(htmlContentAllowed); + requestRepaint(); + } + } + + /** + * Return HTML rendering setting + * + * @return <code>true</code> if the caption text is to be rendered as HTML, + * <code>false</code> otherwise + */ + public boolean isHtmlContentAllowed() { + return getState().isHtmlContentAllowed(); + } + +} diff --git a/server/src/com/vaadin/ui/CheckBox.java b/server/src/com/vaadin/ui/CheckBox.java new file mode 100644 index 0000000000..30ac9b4626 --- /dev/null +++ b/server/src/com/vaadin/ui/CheckBox.java @@ -0,0 +1,141 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import com.vaadin.data.Property; +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.shared.MouseEventDetails; +import com.vaadin.shared.ui.checkbox.CheckBoxServerRpc; +import com.vaadin.shared.ui.checkbox.CheckBoxState; + +public class CheckBox extends AbstractField<Boolean> { + + private CheckBoxServerRpc rpc = new CheckBoxServerRpc() { + + @Override + public void setChecked(boolean checked, + MouseEventDetails mouseEventDetails) { + if (isReadOnly()) { + return; + } + + final Boolean oldValue = getValue(); + final Boolean newValue = checked; + + if (!newValue.equals(oldValue)) { + // The event is only sent if the switch state is changed + setValue(newValue); + } + + } + }; + + FocusAndBlurServerRpcImpl focusBlurRpc = new FocusAndBlurServerRpcImpl(this) { + @Override + protected void fireEvent(Event event) { + CheckBox.this.fireEvent(event); + } + }; + + /** + * Creates a new checkbox. + */ + public CheckBox() { + registerRpc(rpc); + registerRpc(focusBlurRpc); + setValue(Boolean.FALSE); + } + + /** + * Creates a new checkbox with a set caption. + * + * @param caption + * the Checkbox caption. + */ + public CheckBox(String caption) { + this(); + setCaption(caption); + } + + /** + * Creates a new checkbox with a caption and a set initial state. + * + * @param caption + * the caption of the checkbox + * @param initialState + * the initial state of the checkbox + */ + public CheckBox(String caption, boolean initialState) { + this(caption); + setValue(initialState); + } + + /** + * Creates a new checkbox that is connected to a boolean property. + * + * @param state + * the Initial state of the switch-button. + * @param dataSource + */ + public CheckBox(String caption, Property<?> dataSource) { + this(caption); + setPropertyDataSource(dataSource); + } + + @Override + public Class<Boolean> getType() { + return Boolean.class; + } + + @Override + public CheckBoxState getState() { + return (CheckBoxState) super.getState(); + } + + @Override + protected void setInternalValue(Boolean newValue) { + super.setInternalValue(newValue); + if (newValue == null) { + newValue = false; + } + getState().setChecked(newValue); + } + + public void addListener(BlurListener listener) { + addListener(BlurEvent.EVENT_ID, BlurEvent.class, listener, + BlurListener.blurMethod); + } + + public void removeListener(BlurListener listener) { + removeListener(BlurEvent.EVENT_ID, BlurEvent.class, listener); + } + + public void addListener(FocusListener listener) { + addListener(FocusEvent.EVENT_ID, FocusEvent.class, listener, + FocusListener.focusMethod); + } + + public void removeListener(FocusListener listener) { + removeListener(FocusEvent.EVENT_ID, FocusEvent.class, listener); + } + + /** + * Get the boolean value of the button state. + * + * @return True iff the button is pressed down or checked. + * + * @deprecated Use {@link #getValue()} instead and, if needed, handle null + * values. + */ + @Deprecated + public boolean booleanValue() { + Boolean value = getValue(); + return (null == value) ? false : value.booleanValue(); + } +} diff --git a/server/src/com/vaadin/ui/ComboBox.java b/server/src/com/vaadin/ui/ComboBox.java new file mode 100644 index 0000000000..6286dad124 --- /dev/null +++ b/server/src/com/vaadin/ui/ComboBox.java @@ -0,0 +1,116 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.util.Collection; + +import com.vaadin.data.Container; +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.PaintTarget; +import com.vaadin.terminal.gwt.client.ui.combobox.VFilterSelect; + +/** + * 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 Select { + + private String inputPrompt = null; + + /** + * If text input is not allowed, the ComboBox behaves like a pretty + * NativeSelect - the user can not enter any text and clicking the text + * field opens the drop down with options + */ + private boolean textInputAllowed = true; + + public ComboBox() { + setNewItemsAllowed(false); + } + + public ComboBox(String caption, Collection<?> options) { + super(caption, options); + setNewItemsAllowed(false); + } + + public ComboBox(String caption, Container dataSource) { + super(caption, dataSource); + setNewItemsAllowed(false); + } + + public ComboBox(String caption) { + super(caption); + setNewItemsAllowed(false); + } + + /** + * Gets the current input prompt. + * + * @see #setInputPrompt(String) + * @return the current input prompt, or null if not enabled + */ + public String getInputPrompt() { + return 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) { + this.inputPrompt = inputPrompt; + requestRepaint(); + } + + @Override + public void paintContent(PaintTarget target) throws PaintException { + if (inputPrompt != null) { + target.addAttribute("prompt", inputPrompt); + } + super.paintContent(target); + + if (!textInputAllowed) { + target.addAttribute(VFilterSelect.ATTR_NO_TEXT_INPUT, true); + } + } + + /** + * 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) { + this.textInputAllowed = textInputAllowed; + requestRepaint(); + } + + /** + * 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 textInputAllowed; + } + +} diff --git a/server/src/com/vaadin/ui/Component.java b/server/src/com/vaadin/ui/Component.java new file mode 100644 index 0000000000..a2c257ab68 --- /dev/null +++ b/server/src/com/vaadin/ui/Component.java @@ -0,0 +1,1047 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.io.Serializable; +import java.util.EventListener; +import java.util.EventObject; +import java.util.Locale; + +import com.vaadin.Application; +import com.vaadin.event.FieldEvents; +import com.vaadin.shared.ComponentState; +import com.vaadin.terminal.ErrorMessage; +import com.vaadin.terminal.Resource; +import com.vaadin.terminal.Sizeable; +import com.vaadin.terminal.VariableOwner; +import com.vaadin.terminal.gwt.server.ClientConnector; + +/** + * {@code Component} is the top-level interface that is and must be implemented + * by all Vaadin components. {@code Component} is paired with + * {@link AbstractComponent}, which provides a default implementation for all + * the methods defined in this interface. + * + * <p> + * Components are laid out in the user interface hierarchically. The layout is + * managed by layout components, or more generally by components that implement + * the {@link ComponentContainer} interface. Such a container is the + * <i>parent</i> of the contained components. + * </p> + * + * <p> + * The {@link #getParent()} method allows retrieving the parent component of a + * component. While there is a {@link #setParent(Component) setParent()}, you + * rarely need it as you normally add components with the + * {@link ComponentContainer#addComponent(Component) addComponent()} method of + * the layout or other {@code ComponentContainer}, which automatically sets the + * parent. + * </p> + * + * <p> + * A component becomes <i>attached</i> to an application (and the + * {@link #attach()} is called) when it or one of its parents is attached to the + * main window of the application through its containment hierarchy. + * </p> + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +public interface Component extends ClientConnector, Sizeable, Serializable { + + /** + * Gets all user-defined CSS style names of a component. If the component + * has multiple style names defined, the return string is a space-separated + * list of style names. Built-in style names defined in Vaadin or GWT are + * not returned. + * + * <p> + * The style names are returned only in the basic form in which they were + * added; each user-defined style name shows as two CSS style class names in + * the rendered HTML: one as it was given and one prefixed with the + * component-specific style name. Only the former is returned. + * </p> + * + * @return the style name or a space-separated list of user-defined style + * names of the component + * @see #setStyleName(String) + * @see #addStyleName(String) + * @see #removeStyleName(String) + */ + public String getStyleName(); + + /** + * Sets one or more user-defined style names of the component, replacing any + * previous user-defined styles. Multiple styles can be specified as a + * space-separated list of style names. The style names must be valid CSS + * class names and should not conflict with any built-in style names in + * Vaadin or GWT. + * + * <pre> + * Label label = new Label("This text has a lot of style"); + * label.setStyleName("myonestyle myotherstyle"); + * </pre> + * + * <p> + * Each style name will occur in two versions: one as specified and one that + * is prefixed with the style name of the component. For example, if you + * have a {@code Button} component and give it "{@code mystyle}" style, the + * component will have both "{@code mystyle}" and "{@code v-button-mystyle}" + * styles. You could then style the component either with: + * </p> + * + * <pre> + * .myonestyle {background: blue;} + * </pre> + * + * <p> + * or + * </p> + * + * <pre> + * .v-button-myonestyle {background: blue;} + * </pre> + * + * <p> + * It is normally a good practice to use {@link #addStyleName(String) + * addStyleName()} rather than this setter, as different software + * abstraction layers can then add their own styles without accidentally + * removing those defined in other layers. + * </p> + * + * <p> + * This method will trigger a {@link RepaintRequestEvent}. + * </p> + * + * @param style + * the new style or styles of the component as a space-separated + * list + * @see #getStyleName() + * @see #addStyleName(String) + * @see #removeStyleName(String) + */ + public void setStyleName(String style); + + /** + * Adds a style name to component. The style name will be rendered as a HTML + * class name, which can be used in a CSS definition. + * + * <pre> + * Label label = new Label("This text has style"); + * label.addStyleName("mystyle"); + * </pre> + * + * <p> + * Each style name will occur in two versions: one as specified and one that + * is prefixed wil the style name of the component. For example, if you have + * a {@code Button} component and give it "{@code mystyle}" style, the + * component will have both "{@code mystyle}" and "{@code v-button-mystyle}" + * styles. You could then style the component either with: + * </p> + * + * <pre> + * .mystyle {font-style: italic;} + * </pre> + * + * <p> + * or + * </p> + * + * <pre> + * .v-button-mystyle {font-style: italic;} + * </pre> + * + * <p> + * This method will trigger a {@link RepaintRequestEvent}. + * </p> + * + * @param style + * the new style to be added to the component + * @see #getStyleName() + * @see #setStyleName(String) + * @see #removeStyleName(String) + */ + public void addStyleName(String style); + + /** + * Removes one or more style names from component. Multiple styles can be + * specified as a space-separated list of style names. + * + * <p> + * The parameter must be a valid CSS style name. Only user-defined style + * names added with {@link #addStyleName(String) addStyleName()} or + * {@link #setStyleName(String) setStyleName()} can be removed; built-in + * style names defined in Vaadin or GWT can not be removed. + * </p> + * + * * This method will trigger a {@link RepaintRequestEvent}. + * + * @param style + * the style name or style names to be removed + * @see #getStyleName() + * @see #setStyleName(String) + * @see #addStyleName(String) + */ + public void removeStyleName(String style); + + /** + * Tests whether the component is enabled or not. A user can not interact + * with disabled components. Disabled components are rendered in a style + * that indicates the status, usually in gray color. Children of a disabled + * component are also disabled. Components are enabled by default. + * + * <p> + * As a security feature, all updates for disabled components are blocked on + * the server-side. + * </p> + * + * <p> + * Note that this method only returns the status of the component and does + * not take parents into account. Even though this method returns true the + * component can be disabled to the user if a parent is disabled. + * </p> + * + * @return <code>true</code> if the component and its parent are enabled, + * <code>false</code> otherwise. + * @see VariableOwner#isEnabled() + */ + public boolean isEnabled(); + + /** + * Enables or disables the component. The user can not interact disabled + * components, which are shown with a style that indicates the status, + * usually shaded in light gray color. Components are enabled by default. + * + * <pre> + * Button enabled = new Button("Enabled"); + * enabled.setEnabled(true); // The default + * layout.addComponent(enabled); + * + * Button disabled = new Button("Disabled"); + * disabled.setEnabled(false); + * layout.addComponent(disabled); + * </pre> + * + * <p> + * This method will trigger a {@link RepaintRequestEvent} for the component + * and, if it is a {@link ComponentContainer}, for all its children + * recursively. + * </p> + * + * @param enabled + * a boolean value specifying if the component should be enabled + * or not + */ + public void setEnabled(boolean enabled); + + /** + * Tests the <i>visibility</i> property of the component. + * + * <p> + * Visible components are drawn in the user interface, while invisible ones + * are not. The effect is not merely a cosmetic CSS change - no information + * about an invisible component will be sent to the client. The effect is + * thus the same as removing the component from its parent. Making a + * component invisible through this property can alter the positioning of + * other components. + * </p> + * + * <p> + * A component is visible only if all its parents are also visible. This is + * not checked by this method though, so even if this method returns true, + * the component can be hidden from the user because a parent is set to + * invisible. + * </p> + * + * @return <code>true</code> if the component has been set to be visible in + * the user interface, <code>false</code> if not + * @see #setVisible(boolean) + * @see #attach() + */ + public boolean isVisible(); + + /** + * Sets the visibility of the component. + * + * <p> + * Visible components are drawn in the user interface, while invisible ones + * are not. The effect is not merely a cosmetic CSS change - no information + * about an invisible component will be sent to the client. The effect is + * thus the same as removing the component from its parent. + * </p> + * + * <pre> + * TextField readonly = new TextField("Read-Only"); + * readonly.setValue("You can't see this!"); + * readonly.setVisible(false); + * layout.addComponent(readonly); + * </pre> + * + * <p> + * A component is visible only if all of its parents are also visible. If a + * component is explicitly set to be invisible, changes in the visibility of + * its parents will not change the visibility of the component. + * </p> + * + * @param visible + * the boolean value specifying if the component should be + * visible after the call or not. + * @see #isVisible() + */ + public void setVisible(boolean visible); + + /** + * Gets the parent component of the component. + * + * <p> + * Components can be nested but a component can have only one parent. A + * component that contains other components, that is, can be a parent, + * should usually inherit the {@link ComponentContainer} interface. + * </p> + * + * @return the parent component + */ + @Override + public HasComponents getParent(); + + /** + * Tests whether the component is in the read-only mode. The user can not + * change the value of a read-only component. As only {@link Field} + * components normally have a value that can be input or changed by the + * user, this is mostly relevant only to field components, though not + * restricted to them. + * + * <p> + * Notice that the read-only mode only affects whether the user can change + * the <i>value</i> of the component; it is possible to, for example, scroll + * a read-only table. + * </p> + * + * <p> + * The method will return {@code true} if the component or any of its + * parents is in the read-only mode. + * </p> + * + * @return <code>true</code> if the component or any of its parents is in + * read-only mode, <code>false</code> if not. + * @see #setReadOnly(boolean) + */ + public boolean isReadOnly(); + + /** + * Sets the read-only mode of the component to the specified mode. The user + * can not change the value of a read-only component. + * + * <p> + * As only {@link Field} components normally have a value that can be input + * or changed by the user, this is mostly relevant only to field components, + * though not restricted to them. + * </p> + * + * <p> + * Notice that the read-only mode only affects whether the user can change + * the <i>value</i> of the component; it is possible to, for example, scroll + * a read-only table. + * </p> + * + * <p> + * This method will trigger a {@link RepaintRequestEvent}. + * </p> + * + * @param readOnly + * a boolean value specifying whether the component is put + * read-only mode or not + */ + public void setReadOnly(boolean readOnly); + + /** + * Gets the caption of the component. + * + * <p> + * See {@link #setCaption(String)} for a detailed description of the + * caption. + * </p> + * + * @return the caption of the component or {@code null} if the caption is + * not set. + * @see #setCaption(String) + */ + public String getCaption(); + + /** + * Sets the caption of the component. + * + * <p> + * A <i>caption</i> is an explanatory textual label accompanying a user + * interface component, usually shown above, left of, or inside the + * component. <i>Icon</i> (see {@link #setIcon(Resource) setIcon()} is + * closely related to caption and is usually displayed horizontally before + * or after it, depending on the component and the containing layout. + * </p> + * + * <p> + * The caption can usually also be given as the first parameter to a + * constructor, though some components do not support it. + * </p> + * + * <pre> + * RichTextArea area = new RichTextArea(); + * area.setCaption("You can edit stuff here"); + * area.setValue("<h1>Helpful Heading</h1>" + * + "<p>All this is for you to edit.</p>"); + * </pre> + * + * <p> + * The contents of a caption are automatically quoted, so no raw XHTML can + * be rendered in a caption. The validity of the used character encoding, + * usually UTF-8, is not checked. + * </p> + * + * <p> + * The caption of a component is, by default, managed and displayed by the + * layout component or component container in which the component is placed. + * For example, the {@link VerticalLayout} component shows the captions + * left-aligned above the contained components, while the {@link FormLayout} + * component shows the captions on the left side of the vertically laid + * components, with the captions and their associated components + * left-aligned in their own columns. The {@link CustomComponent} does not + * manage the caption of its composition root, so if the root component has + * a caption, it will not be rendered. Some components, such as + * {@link Button} and {@link Panel}, manage the caption themselves and + * display it inside the component. + * </p> + * + * <p> + * This method will trigger a {@link RepaintRequestEvent}. A + * reimplementation should call the superclass implementation. + * </p> + * + * @param caption + * the new caption for the component. If the caption is + * {@code null}, no caption is shown and it does not normally + * take any space + */ + public void setCaption(String caption); + + /** + * Gets the icon resource of the component. + * + * <p> + * See {@link #setIcon(Resource)} for a detailed description of the icon. + * </p> + * + * @return the icon resource of the component or {@code null} if the + * component has no icon + * @see #setIcon(Resource) + */ + public Resource getIcon(); + + /** + * Sets the icon of the component. + * + * <p> + * An icon is an explanatory graphical label accompanying a user interface + * component, usually shown above, left of, or inside the component. Icon is + * closely related to caption (see {@link #setCaption(String) setCaption()}) + * and is usually displayed horizontally before or after it, depending on + * the component and the containing layout. + * </p> + * + * <p> + * The image is loaded by the browser from a resource, typically a + * {@link com.vaadin.terminal.ThemeResource}. + * </p> + * + * <pre> + * // Component with an icon from a custom theme + * TextField name = new TextField("Name"); + * name.setIcon(new ThemeResource("icons/user.png")); + * layout.addComponent(name); + * + * // Component with an icon from another theme ('runo') + * Button ok = new Button("OK"); + * ok.setIcon(new ThemeResource("../runo/icons/16/ok.png")); + * layout.addComponent(ok); + * </pre> + * + * <p> + * The icon of a component is, by default, managed and displayed by the + * layout component or component container in which the component is placed. + * For example, the {@link VerticalLayout} component shows the icons + * left-aligned above the contained components, while the {@link FormLayout} + * component shows the icons on the left side of the vertically laid + * components, with the icons and their associated components left-aligned + * in their own columns. The {@link CustomComponent} does not manage the + * icon of its composition root, so if the root component has an icon, it + * will not be rendered. + * </p> + * + * <p> + * An icon will be rendered inside an HTML element that has the + * {@code v-icon} CSS style class. The containing layout may enclose an icon + * and a caption inside elements related to the caption, such as + * {@code v-caption} . + * </p> + * + * This method will trigger a {@link RepaintRequestEvent}. + * + * @param icon + * the icon of the component. If null, no icon is shown and it + * does not normally take any space. + * @see #getIcon() + * @see #setCaption(String) + */ + public void setIcon(Resource icon); + + /** + * Gets the Root the component is attached to. + * + * <p> + * If the component is not attached to a Root through a component + * containment hierarchy, <code>null</code> is returned. + * </p> + * + * @return the Root of the component or <code>null</code> if it is not + * attached to a Root + */ + @Override + public Root getRoot(); + + /** + * Gets the application object to which the component is attached. + * + * <p> + * The method will return {@code null} if the component is not currently + * attached to an application. + * </p> + * + * <p> + * Getting a null value is often a problem in constructors of regular + * components and in the initializers of custom composite components. A + * standard workaround is to use {@link Application#getCurrent()} to + * retrieve the application instance that the current request relates to. + * Another way is to move the problematic initialization to + * {@link #attach()}, as described in the documentation of the method. + * </p> + * + * @return the parent application of the component or <code>null</code>. + * @see #attach() + */ + public Application getApplication(); + + /** + * {@inheritDoc} + * + * <p> + * Reimplementing the {@code attach()} method is useful for tasks that need + * to get a reference to the parent, window, or application object with the + * {@link #getParent()}, {@link #getRoot()}, and {@link #getApplication()} + * methods. A component does not yet know these objects in the constructor, + * so in such case, the methods will return {@code null}. For example, the + * following is invalid: + * </p> + * + * <pre> + * public class AttachExample extends CustomComponent { + * public AttachExample() { + * // ERROR: We can't access the application object yet. + * ClassResource r = new ClassResource("smiley.jpg", getApplication()); + * Embedded image = new Embedded("Image:", r); + * setCompositionRoot(image); + * } + * } + * </pre> + * + * <p> + * Adding a component to an application triggers calling the + * {@link #attach()} method for the component. Correspondingly, removing a + * component from a container triggers calling the {@link #detach()} method. + * If the parent of an added component is already connected to the + * application, the {@code attach()} is called immediately from + * {@link #setParent(Component)}. + * </p> + * <p> + * This method must call {@link Root#componentAttached(Component)} to let + * the Root know that a new Component has been attached. + * </p> + * + * + * <pre> + * public class AttachExample extends CustomComponent { + * public AttachExample() { + * } + * + * @Override + * public void attach() { + * super.attach(); // Must call. + * + * // Now we know who ultimately owns us. + * ClassResource r = new ClassResource("smiley.jpg", getApplication()); + * Embedded image = new Embedded("Image:", r); + * setCompositionRoot(image); + * } + * } + * </pre> + */ + @Override + public void attach(); + + /** + * Gets the locale of the component. + * + * <p> + * If a component does not have a locale set, the locale of its parent is + * returned, and so on. Eventually, if no parent has locale set, the locale + * of the application is returned. If the application does not have a locale + * set, it is determined by <code>Locale.getDefault()</code>. + * </p> + * + * <p> + * As the component must be attached before its locale can be acquired, + * using this method in the internationalization of component captions, etc. + * is generally not feasible. For such use case, we recommend using an + * otherwise acquired reference to the application locale. + * </p> + * + * @return Locale of this component or {@code null} if the component and + * none of its parents has a locale set and the component is not yet + * attached to an application. + */ + public Locale getLocale(); + + /** + * Returns the current shared state bean for the component. The state (or + * changes to it) is communicated from the server to the client. + * + * Subclasses can use a more specific return type for this method. + * + * @return The state object for the component + * + * @since 7.0 + */ + @Override + public ComponentState getState(); + + /** + * Called before the shared state is sent to the client. Gives the component + * an opportunity to set computed/dynamic state values e.g. state values + * that depend on other component features. + * <p> + * This method must not alter the component hierarchy in any way. + * </p> + * + * @since 7.0 + */ + public void updateState(); + + /** + * Adds an unique id for component that get's transferred to terminal for + * testing purposes. Keeping identifiers unique is the responsibility of the + * programmer. + * + * @param id + * An alphanumeric id + */ + public void setDebugId(String id); + + /** + * Get's currently set debug identifier + * + * @return current debug id, null if not set + */ + public String getDebugId(); + + /* Component event framework */ + + /** + * Superclass of all component originated events. + * + * <p> + * Events are the basis of all user interaction handling in Vaadin. To + * handle events, you provide a listener object that receives the events of + * the particular event type. + * </p> + * + * <pre> + * Button button = new Button("Click Me!"); + * button.addListener(new Button.ClickListener() { + * public void buttonClick(ClickEvent event) { + * getWindow().showNotification("Thank You!"); + * } + * }); + * layout.addComponent(button); + * </pre> + * + * <p> + * Notice that while each of the event types have their corresponding + * listener types; the listener interfaces are not required to inherit the + * {@code Component.Listener} interface. + * </p> + * + * @see Component.Listener + */ + @SuppressWarnings("serial") + public class Event extends EventObject { + + /** + * Constructs a new event with the specified source component. + * + * @param source + * the source component of the event + */ + public Event(Component source) { + super(source); + } + + /** + * Gets the component where the event occurred. + * + * @return the source component of the event + */ + public Component getComponent() { + return (Component) getSource(); + } + } + + /** + * Listener interface for receiving <code>Component.Event</code>s. + * + * <p> + * Listener interfaces are the basis of all user interaction handling in + * Vaadin. You have or create a listener object that receives the events. + * All event types have their corresponding listener types; they are not, + * however, required to inherit the {@code Component.Listener} interface, + * and they rarely do so. + * </p> + * + * <p> + * This generic listener interface is useful typically when you wish to + * handle events from different component types in a single listener method + * ({@code componentEvent()}. If you handle component events in an anonymous + * listener class, you normally use the component specific listener class, + * such as {@link com.vaadin.ui.Button.ClickEvent}. + * </p> + * + * <pre> + * class Listening extends CustomComponent implements Listener { + * Button ok; // Stored for determining the source of an event + * + * Label status; // For displaying info about the event + * + * public Listening() { + * VerticalLayout layout = new VerticalLayout(); + * + * // Some miscellaneous component + * TextField name = new TextField("Say it all here"); + * name.addListener(this); + * name.setImmediate(true); + * layout.addComponent(name); + * + * // Handle button clicks as generic events instead + * // of Button.ClickEvent events + * ok = new Button("OK"); + * ok.addListener(this); + * layout.addComponent(ok); + * + * // For displaying information about an event + * status = new Label(""); + * layout.addComponent(status); + * + * setCompositionRoot(layout); + * } + * + * public void componentEvent(Event event) { + * // Act according to the source of the event + * if (event.getSource() == ok + * && event.getClass() == Button.ClickEvent.class) + * getWindow().showNotification("Click!"); + * + * // Display source component and event class names + * status.setValue("Event from " + event.getSource().getClass().getName() + * + ": " + event.getClass().getName()); + * } + * } + * + * Listening listening = new Listening(); + * layout.addComponent(listening); + * </pre> + * + * @see Component#addListener(Listener) + */ + public interface Listener extends EventListener, Serializable { + + /** + * Notifies the listener of a component event. + * + * <p> + * As the event can typically come from one of many source components, + * you may need to differentiate between the event source by component + * reference, class, etc. + * </p> + * + * <pre> + * public void componentEvent(Event event) { + * // Act according to the source of the event + * if (event.getSource() == ok && event.getClass() == Button.ClickEvent.class) + * getWindow().showNotification("Click!"); + * + * // Display source component and event class names + * status.setValue("Event from " + event.getSource().getClass().getName() + * + ": " + event.getClass().getName()); + * } + * </pre> + * + * @param event + * the event that has occured. + */ + public void componentEvent(Component.Event event); + } + + /** + * Registers a new (generic) component event listener for the component. + * + * <pre> + * class Listening extends CustomComponent implements Listener { + * // Stored for determining the source of an event + * Button ok; + * + * Label status; // For displaying info about the event + * + * public Listening() { + * VerticalLayout layout = new VerticalLayout(); + * + * // Some miscellaneous component + * TextField name = new TextField("Say it all here"); + * name.addListener(this); + * name.setImmediate(true); + * layout.addComponent(name); + * + * // Handle button clicks as generic events instead + * // of Button.ClickEvent events + * ok = new Button("OK"); + * ok.addListener(this); + * layout.addComponent(ok); + * + * // For displaying information about an event + * status = new Label(""); + * layout.addComponent(status); + * + * setCompositionRoot(layout); + * } + * + * public void componentEvent(Event event) { + * // Act according to the source of the event + * if (event.getSource() == ok) + * getWindow().showNotification("Click!"); + * + * status.setValue("Event from " + event.getSource().getClass().getName() + * + ": " + event.getClass().getName()); + * } + * } + * + * Listening listening = new Listening(); + * layout.addComponent(listening); + * </pre> + * + * @param listener + * the new Listener to be registered. + * @see Component.Event + * @see #removeListener(Listener) + */ + public void addListener(Component.Listener listener); + + /** + * Removes a previously registered component event listener from this + * component. + * + * @param listener + * the listener to be removed. + * @see #addListener(Listener) + */ + public void removeListener(Component.Listener listener); + + /** + * Class of all component originated error events. + * + * <p> + * The component error event is normally fired by + * {@link AbstractComponent#setComponentError(ErrorMessage)}. The component + * errors are set by the framework in some situations and can be set by user + * code. They are indicated in a component with an error indicator. + * </p> + */ + @SuppressWarnings("serial") + public class ErrorEvent extends Event { + + private final ErrorMessage message; + + /** + * Constructs a new event with a specified source component. + * + * @param message + * the error message. + * @param component + * the source component. + */ + public ErrorEvent(ErrorMessage message, Component component) { + super(component); + this.message = message; + } + + /** + * Gets the error message. + * + * @return the error message. + */ + public ErrorMessage getErrorMessage() { + return message; + } + } + + /** + * Listener interface for receiving <code>Component.Errors</code>s. + */ + public interface ErrorListener extends EventListener, Serializable { + + /** + * Notifies the listener of a component error. + * + * @param event + * the event that has occured. + */ + public void componentError(Component.ErrorEvent event); + } + + /** + * A sub-interface implemented by components that can obtain input focus. + * This includes all {@link Field} components as well as some other + * components, such as {@link Upload}. + * + * <p> + * Focus can be set with {@link #focus()}. This interface does not provide + * an accessor that would allow finding out the currently focused component; + * focus information can be acquired for some (but not all) {@link Field} + * components through the {@link com.vaadin.event.FieldEvents.FocusListener} + * and {@link com.vaadin.event.FieldEvents.BlurListener} interfaces. + * </p> + * + * @see FieldEvents + */ + public interface Focusable extends Component { + + /** + * Sets the focus to this component. + * + * <pre> + * Form loginBox = new Form(); + * loginBox.setCaption("Login"); + * layout.addComponent(loginBox); + * + * // Create the first field which will be focused + * TextField username = new TextField("User name"); + * loginBox.addField("username", username); + * + * // Set focus to the user name + * username.focus(); + * + * TextField password = new TextField("Password"); + * loginBox.addField("password", password); + * + * Button login = new Button("Login"); + * loginBox.getFooter().addComponent(login); + * </pre> + * + * <p> + * Notice that this interface does not provide an accessor that would + * allow finding out the currently focused component. Focus information + * can be acquired for some (but not all) {@link Field} components + * through the {@link com.vaadin.event.FieldEvents.FocusListener} and + * {@link com.vaadin.event.FieldEvents.BlurListener} interfaces. + * </p> + * + * @see com.vaadin.event.FieldEvents + * @see com.vaadin.event.FieldEvents.FocusEvent + * @see com.vaadin.event.FieldEvents.FocusListener + * @see com.vaadin.event.FieldEvents.BlurEvent + * @see com.vaadin.event.FieldEvents.BlurListener + */ + public void focus(); + + /** + * Gets the <i>tabulator index</i> of the {@code Focusable} component. + * + * @return tab index set for the {@code Focusable} component + * @see #setTabIndex(int) + */ + public int getTabIndex(); + + /** + * Sets the <i>tabulator index</i> of the {@code Focusable} component. + * The tab index property is used to specify the order in which the + * fields are focused when the user presses the Tab key. Components with + * a defined tab index are focused sequentially first, and then the + * components with no tab index. + * + * <pre> + * Form loginBox = new Form(); + * loginBox.setCaption("Login"); + * layout.addComponent(loginBox); + * + * // Create the first field which will be focused + * TextField username = new TextField("User name"); + * loginBox.addField("username", username); + * + * // Set focus to the user name + * username.focus(); + * + * TextField password = new TextField("Password"); + * loginBox.addField("password", password); + * + * Button login = new Button("Login"); + * loginBox.getFooter().addComponent(login); + * + * // An additional component which natural focus order would + * // be after the button. + * CheckBox remember = new CheckBox("Remember me"); + * loginBox.getFooter().addComponent(remember); + * + * username.setTabIndex(1); + * password.setTabIndex(2); + * remember.setTabIndex(3); // Different than natural place + * login.setTabIndex(4); + * </pre> + * + * <p> + * After all focusable user interface components are done, the browser + * can begin again from the component with the smallest tab index, or it + * can take the focus out of the page, for example, to the location bar. + * </p> + * + * <p> + * If the tab index is not set (is set to zero), the default tab order + * is used. The order is somewhat browser-dependent, but generally + * follows the HTML structure of the page. + * </p> + * + * <p> + * A negative value means that the component is completely removed from + * the tabulation order and can not be reached by pressing the Tab key + * at all. + * </p> + * + * @param tabIndex + * the tab order of this component. Indexes usually start + * from 1. Zero means that default tab order should be used. + * A negative value means that the field should not be + * included in the tabbing sequence. + * @see #getTabIndex() + */ + public void setTabIndex(int tabIndex); + + } + +} diff --git a/server/src/com/vaadin/ui/ComponentContainer.java b/server/src/com/vaadin/ui/ComponentContainer.java new file mode 100644 index 0000000000..8182d54b56 --- /dev/null +++ b/server/src/com/vaadin/ui/ComponentContainer.java @@ -0,0 +1,222 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.io.Serializable; + +/** + * Extension to the {@link Component} interface which adds to it the capacity to + * contain other components. All UI elements that can have child elements + * implement this interface. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +public interface ComponentContainer extends HasComponents { + + /** + * Adds the component into this container. + * + * @param c + * the component to be added. + */ + public void addComponent(Component c); + + /** + * Removes the component from this container. + * + * @param c + * the component to be removed. + */ + public void removeComponent(Component c); + + /** + * Removes all components from this container. + */ + public void removeAllComponents(); + + /** + * Replaces the component in the container with another one without changing + * position. + * + * <p> + * This method replaces component with another one is such way that the new + * component overtakes the position of the old component. If the old + * component is not in the container, the new component is added to the + * container. If the both component are already in the container, their + * positions are swapped. Component attach and detach events should be taken + * care as with add and remove. + * </p> + * + * @param oldComponent + * the old component that will be replaced. + * @param newComponent + * the new component to be replaced. + */ + public void replaceComponent(Component oldComponent, Component newComponent); + + /** + * Gets the number of children this {@link ComponentContainer} has. This + * must be symmetric with what {@link #getComponentIterator()} returns. + * + * @return The number of child components this container has. + * @since 7.0.0 + */ + public int getComponentCount(); + + /** + * Moves all components from an another container into this container. The + * components are removed from <code>source</code>. + * + * @param source + * the container which contains the components that are to be + * moved to this container. + */ + public void moveComponentsFrom(ComponentContainer source); + + /** + * Listens the component attach events. + * + * @param listener + * the listener to add. + */ + public void addListener(ComponentAttachListener listener); + + /** + * Stops the listening component attach events. + * + * @param listener + * the listener to removed. + */ + public void removeListener(ComponentAttachListener listener); + + /** + * Listens the component detach events. + */ + public void addListener(ComponentDetachListener listener); + + /** + * Stops the listening component detach events. + */ + public void removeListener(ComponentDetachListener listener); + + /** + * Component attach listener interface. + */ + public interface ComponentAttachListener extends Serializable { + + /** + * A new component is attached to container. + * + * @param event + * the component attach event. + */ + public void componentAttachedToContainer(ComponentAttachEvent event); + } + + /** + * Component detach listener interface. + */ + public interface ComponentDetachListener extends Serializable { + + /** + * A component has been detached from container. + * + * @param event + * the component detach event. + */ + public void componentDetachedFromContainer(ComponentDetachEvent event); + } + + /** + * Component attach event sent when a component is attached to container. + */ + @SuppressWarnings("serial") + public class ComponentAttachEvent extends Component.Event { + + private final Component component; + + /** + * Creates a new attach event. + * + * @param container + * the component container the component has been detached + * to. + * @param attachedComponent + * the component that has been attached. + */ + public ComponentAttachEvent(ComponentContainer container, + Component attachedComponent) { + super(container); + component = attachedComponent; + } + + /** + * Gets the component container. + * + * @param the + * component container. + */ + public ComponentContainer getContainer() { + return (ComponentContainer) getSource(); + } + + /** + * Gets the attached component. + * + * @param the + * attach component. + */ + public Component getAttachedComponent() { + return component; + } + } + + /** + * Component detach event sent when a component is detached from container. + */ + @SuppressWarnings("serial") + public class ComponentDetachEvent extends Component.Event { + + private final Component component; + + /** + * Creates a new detach event. + * + * @param container + * the component container the component has been detached + * from. + * @param detachedComponent + * the component that has been detached. + */ + public ComponentDetachEvent(ComponentContainer container, + Component detachedComponent) { + super(container); + component = detachedComponent; + } + + /** + * Gets the component container. + * + * @param the + * component container. + */ + public ComponentContainer getContainer() { + return (ComponentContainer) getSource(); + } + + /** + * Gets the detached component. + * + * @return the detached component. + */ + public Component getDetachedComponent() { + return component; + } + } + +} diff --git a/server/src/com/vaadin/ui/ConnectorTracker.java b/server/src/com/vaadin/ui/ConnectorTracker.java new file mode 100644 index 0000000000..e3d1bf86db --- /dev/null +++ b/server/src/com/vaadin/ui/ConnectorTracker.java @@ -0,0 +1,320 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.ui; + +import java.io.Serializable; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; + +import com.vaadin.terminal.AbstractClientConnector; +import com.vaadin.terminal.gwt.client.ServerConnector; +import com.vaadin.terminal.gwt.server.ClientConnector; + +/** + * A class which takes care of book keeping of {@link ClientConnector}s for a + * Root. + * <p> + * Provides {@link #getConnector(String)} which can be used to lookup a + * connector from its id. This is for framework use only and should not be + * needed in applications. + * </p> + * <p> + * Tracks which {@link ClientConnector}s are dirty so they can be updated to the + * client when the following response is sent. A connector is dirty when an + * operation has been performed on it on the server and as a result of this + * operation new information needs to be sent to its {@link ServerConnector}. + * </p> + * + * @author Vaadin Ltd + * @version @VERSION@ + * @since 7.0.0 + * + */ +public class ConnectorTracker implements Serializable { + + private final HashMap<String, ClientConnector> connectorIdToConnector = new HashMap<String, ClientConnector>(); + private Set<ClientConnector> dirtyConnectors = new HashSet<ClientConnector>(); + + private Root root; + + /** + * Gets a logger for this class + * + * @return A logger instance for logging within this class + * + */ + public static Logger getLogger() { + return Logger.getLogger(ConnectorTracker.class.getName()); + } + + /** + * Creates a new ConnectorTracker for the given root. A tracker is always + * attached to a root and the root cannot be changed during the lifetime of + * a {@link ConnectorTracker}. + * + * @param root + * The root to attach to. Cannot be null. + */ + public ConnectorTracker(Root root) { + this.root = root; + } + + /** + * Register the given connector. + * <p> + * The lookup method {@link #getConnector(String)} only returns registered + * connectors. + * </p> + * + * @param connector + * The connector to register. + */ + public void registerConnector(ClientConnector connector) { + String connectorId = connector.getConnectorId(); + ClientConnector previouslyRegistered = connectorIdToConnector + .get(connectorId); + if (previouslyRegistered == null) { + connectorIdToConnector.put(connectorId, connector); + getLogger().fine( + "Registered " + connector.getClass().getSimpleName() + " (" + + connectorId + ")"); + } else if (previouslyRegistered != connector) { + throw new RuntimeException("A connector with id " + connectorId + + " is already registered!"); + } else { + getLogger().warning( + "An already registered connector was registered again: " + + connector.getClass().getSimpleName() + " (" + + connectorId + ")"); + } + + } + + /** + * Unregister the given connector. + * + * <p> + * The lookup method {@link #getConnector(String)} only returns registered + * connectors. + * </p> + * + * @param connector + * The connector to unregister + */ + public void unregisterConnector(ClientConnector connector) { + String connectorId = connector.getConnectorId(); + if (!connectorIdToConnector.containsKey(connectorId)) { + getLogger().warning( + "Tried to unregister " + + connector.getClass().getSimpleName() + " (" + + connectorId + ") which is not registered"); + return; + } + if (connectorIdToConnector.get(connectorId) != connector) { + throw new RuntimeException("The given connector with id " + + connectorId + + " is not the one that was registered for that id"); + } + + getLogger().fine( + "Unregistered " + connector.getClass().getSimpleName() + " (" + + connectorId + ")"); + connectorIdToConnector.remove(connectorId); + } + + /** + * Gets a connector by its id. + * + * @param connectorId + * The connector id to look for + * @return The connector with the given id or null if no connector has the + * given id + */ + public ClientConnector getConnector(String connectorId) { + return connectorIdToConnector.get(connectorId); + } + + /** + * Cleans the connector map from all connectors that are no longer attached + * to the application. This should only be called by the framework. + */ + public void cleanConnectorMap() { + // remove detached components from paintableIdMap so they + // can be GC'ed + Iterator<String> iterator = connectorIdToConnector.keySet().iterator(); + + while (iterator.hasNext()) { + String connectorId = iterator.next(); + ClientConnector connector = connectorIdToConnector.get(connectorId); + if (getRootForConnector(connector) != root) { + // If connector is no longer part of this root, + // remove it from the map. If it is re-attached to the + // application at some point it will be re-added through + // registerConnector(connector) + + // This code should never be called as cleanup should take place + // in detach() + getLogger() + .warning( + "cleanConnectorMap unregistered connector " + + getConnectorAndParentInfo(connector) + + "). This should have been done when the connector was detached."); + iterator.remove(); + } + } + + } + + /** + * Finds the root that the connector is attached to. + * + * @param connector + * The connector to lookup + * @return The root the connector is attached to or null if it is not + * attached to any root. + */ + private Root getRootForConnector(ClientConnector connector) { + if (connector == null) { + return null; + } + if (connector instanceof Component) { + return ((Component) connector).getRoot(); + } + + return getRootForConnector(connector.getParent()); + } + + /** + * Mark the connector as dirty. + * + * @see #getDirtyConnectors() + * + * @param connector + * The connector that should be marked clean. + */ + public void markDirty(ClientConnector connector) { + if (getLogger().isLoggable(Level.FINE)) { + if (!dirtyConnectors.contains(connector)) { + getLogger().fine( + getConnectorAndParentInfo(connector) + " " + + "is now dirty"); + } + } + + dirtyConnectors.add(connector); + } + + /** + * Mark the connector as clean. + * + * @param connector + * The connector that should be marked clean. + */ + public void markClean(ClientConnector connector) { + if (getLogger().isLoggable(Level.FINE)) { + if (dirtyConnectors.contains(connector)) { + getLogger().fine( + getConnectorAndParentInfo(connector) + " " + + "is no longer dirty"); + } + } + + dirtyConnectors.remove(connector); + } + + /** + * Returns {@link #getConnectorString(ClientConnector)} for the connector + * and its parent (if it has a parent). + * + * @param connector + * The connector + * @return A string describing the connector and its parent + */ + private String getConnectorAndParentInfo(ClientConnector connector) { + String message = getConnectorString(connector); + if (connector.getParent() != null) { + message += " (parent: " + getConnectorString(connector.getParent()) + + ")"; + } + return message; + } + + /** + * Returns a string with the connector name and id. Useful mostly for + * debugging and logging. + * + * @param connector + * The connector + * @return A string that describes the connector + */ + private String getConnectorString(ClientConnector connector) { + if (connector == null) { + return "(null)"; + } + + String connectorId; + try { + connectorId = connector.getConnectorId(); + } catch (RuntimeException e) { + // This happens if the connector is not attached to the application. + // SHOULD not happen in this case but theoretically can. + connectorId = "@" + Integer.toHexString(connector.hashCode()); + } + return connector.getClass().getName() + "(" + connectorId + ")"; + } + + /** + * Mark all connectors in this root as dirty. + */ + public void markAllConnectorsDirty() { + markConnectorsDirtyRecursively(root); + getLogger().fine("All connectors are now dirty"); + } + + /** + * Mark all connectors in this root as clean. + */ + public void markAllConnectorsClean() { + dirtyConnectors.clear(); + getLogger().fine("All connectors are now clean"); + } + + /** + * Marks all visible connectors dirty, starting from the given connector and + * going downwards in the hierarchy. + * + * @param c + * The component to start iterating downwards from + */ + private void markConnectorsDirtyRecursively(ClientConnector c) { + if (c instanceof Component && !((Component) c).isVisible()) { + return; + } + markDirty(c); + for (ClientConnector child : AbstractClientConnector + .getAllChildrenIterable(c)) { + markConnectorsDirtyRecursively(child); + } + } + + /** + * Returns a collection of all connectors which have been marked as dirty. + * <p> + * The state and pending RPC calls for dirty connectors are sent to the + * client in the following request. + * </p> + * + * @return A collection of all dirty connectors for this root. This list may + * contain invisible connectors. + */ + public Collection<ClientConnector> getDirtyConnectors() { + return dirtyConnectors; + } + +} diff --git a/server/src/com/vaadin/ui/CssLayout.java b/server/src/com/vaadin/ui/CssLayout.java new file mode 100644 index 0000000000..356f0a3843 --- /dev/null +++ b/server/src/com/vaadin/ui/CssLayout.java @@ -0,0 +1,308 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.ui; + +import java.util.Iterator; +import java.util.LinkedList; + +import com.vaadin.event.LayoutEvents.LayoutClickEvent; +import com.vaadin.event.LayoutEvents.LayoutClickListener; +import com.vaadin.event.LayoutEvents.LayoutClickNotifier; +import com.vaadin.shared.Connector; +import com.vaadin.shared.MouseEventDetails; +import com.vaadin.shared.ui.csslayout.CssLayoutServerRpc; +import com.vaadin.shared.ui.csslayout.CssLayoutState; +import com.vaadin.terminal.gwt.client.ui.LayoutClickEventHandler; + +/** + * CssLayout is a layout component that can be used in browser environment only. + * It simply renders components and their captions into a same div element. + * Component layout can then be adjusted with css. + * <p> + * In comparison to {@link HorizontalLayout} and {@link VerticalLayout} + * <ul> + * <li>rather similar server side api + * <li>no spacing, alignment or expand ratios + * <li>much simpler DOM that can be styled by skilled web developer + * <li>no abstraction of browser differences (developer must ensure that the + * result works properly on each browser) + * <li>different kind of handling for relative sizes (that are set from server + * side) (*) + * <li>noticeably faster rendering time in some situations as we rely more on + * the browser's rendering engine. + * </ul> + * <p> + * With {@link CustomLayout} one can often achieve similar results (good looking + * layouts with web technologies), but with CustomLayout developer needs to work + * with fixed templates. + * <p> + * By extending CssLayout one can also inject some css rules straight to child + * components using {@link #getCss(Component)}. + * + * <p> + * (*) Relative sizes (set from server side) are treated bit differently than in + * other layouts in Vaadin. In cssLayout the size is calculated relatively to + * CSS layouts content area which is pretty much as in html and css. In other + * layouts the size of component is calculated relatively to the "slot" given by + * layout. + * <p> + * Also note that client side framework in Vaadin modifies inline style + * properties width and height. This happens on each update to component. If one + * wants to set component sizes with CSS, component must have undefined size on + * server side (which is not the default for all components) and the size must + * be defined with class styles - not by directly injecting width and height. + * + * @since 6.1 brought in from "FastLayouts" incubator project + * + */ +public class CssLayout extends AbstractLayout implements LayoutClickNotifier { + + private CssLayoutServerRpc rpc = new CssLayoutServerRpc() { + + @Override + public void layoutClick(MouseEventDetails mouseDetails, + Connector clickedConnector) { + fireEvent(LayoutClickEvent.createEvent(CssLayout.this, + mouseDetails, clickedConnector)); + } + }; + /** + * Custom layout slots containing the components. + */ + protected LinkedList<Component> components = new LinkedList<Component>(); + + public CssLayout() { + registerRpc(rpc); + } + + /** + * Add a component into this container. The component is added to the right + * or under the previous component. + * + * @param c + * the component to be added. + */ + @Override + public void addComponent(Component c) { + // Add to components before calling super.addComponent + // so that it is available to AttachListeners + components.add(c); + try { + super.addComponent(c); + requestRepaint(); + } catch (IllegalArgumentException e) { + components.remove(c); + throw e; + } + } + + /** + * Adds a component into this container. The component is added to the left + * or on top of the other components. + * + * @param c + * the component to be added. + */ + public void addComponentAsFirst(Component c) { + // If c is already in this, we must remove it before proceeding + // see ticket #7668 + if (c.getParent() == this) { + removeComponent(c); + } + components.addFirst(c); + try { + super.addComponent(c); + requestRepaint(); + } catch (IllegalArgumentException e) { + components.remove(c); + throw e; + } + } + + /** + * Adds a component into indexed position in this container. + * + * @param c + * the component to be added. + * @param index + * the index of the component position. The components currently + * in and after the position are shifted forwards. + */ + public void addComponent(Component c, int index) { + // If c is already in this, we must remove it before proceeding + // see ticket #7668 + if (c.getParent() == this) { + // When c is removed, all components after it are shifted down + if (index > getComponentIndex(c)) { + index--; + } + removeComponent(c); + } + components.add(index, c); + try { + super.addComponent(c); + requestRepaint(); + } catch (IllegalArgumentException e) { + components.remove(c); + throw e; + } + } + + /** + * Removes the component from this container. + * + * @param c + * the component to be removed. + */ + @Override + public void removeComponent(Component c) { + components.remove(c); + super.removeComponent(c); + requestRepaint(); + } + + /** + * Gets the component container iterator for going trough all the components + * in the container. + * + * @return the Iterator of the components inside the container. + */ + @Override + public Iterator<Component> getComponentIterator() { + return components.iterator(); + } + + /** + * Gets the number of contained components. Consistent with the iterator + * returned by {@link #getComponentIterator()}. + * + * @return the number of contained components + */ + @Override + public int getComponentCount() { + return components.size(); + } + + @Override + public void updateState() { + super.updateState(); + getState().getChildCss().clear(); + for (Iterator<Component> ci = getComponentIterator(); ci.hasNext();) { + Component child = ci.next(); + String componentCssString = getCss(child); + if (componentCssString != null) { + getState().getChildCss().put(child, componentCssString); + } + + } + } + + @Override + public CssLayoutState getState() { + return (CssLayoutState) super.getState(); + } + + /** + * Returns styles to be applied to given component. Override this method to + * inject custom style rules to components. + * + * <p> + * Note that styles are injected over previous styles before actual child + * rendering. Previous styles are not cleared, but overridden. + * + * <p> + * Note that one most often achieves better code style, by separating + * styling to theme (with custom theme and {@link #addStyleName(String)}. + * With own custom styles it is also very easy to break browser + * compatibility. + * + * @param c + * the component + * @return css rules to be applied to component + */ + protected String getCss(Component c) { + return null; + } + + /* Documented in superclass */ + @Override + public void replaceComponent(Component oldComponent, Component newComponent) { + + // Gets the locations + int oldLocation = -1; + int newLocation = -1; + int location = 0; + for (final Iterator<Component> i = components.iterator(); i.hasNext();) { + final Component component = i.next(); + + if (component == oldComponent) { + oldLocation = location; + } + if (component == newComponent) { + newLocation = location; + } + + location++; + } + + if (oldLocation == -1) { + addComponent(newComponent); + } else if (newLocation == -1) { + removeComponent(oldComponent); + addComponent(newComponent, oldLocation); + } else { + if (oldLocation > newLocation) { + components.remove(oldComponent); + components.add(newLocation, oldComponent); + components.remove(newComponent); + components.add(oldLocation, newComponent); + } else { + components.remove(newComponent); + components.add(oldLocation, newComponent); + components.remove(oldComponent); + components.add(newLocation, oldComponent); + } + + requestRepaint(); + } + } + + @Override + public void addListener(LayoutClickListener listener) { + addListener(LayoutClickEventHandler.LAYOUT_CLICK_EVENT_IDENTIFIER, + LayoutClickEvent.class, listener, + LayoutClickListener.clickMethod); + } + + @Override + public void removeListener(LayoutClickListener listener) { + removeListener(LayoutClickEventHandler.LAYOUT_CLICK_EVENT_IDENTIFIER, + LayoutClickEvent.class, listener); + } + + /** + * Returns the index of the given component. + * + * @param component + * The component to look up. + * @return The index of the component or -1 if the component is not a child. + */ + public int getComponentIndex(Component component) { + return components.indexOf(component); + } + + /** + * Returns the component at the given position. + * + * @param index + * The position of the component. + * @return The component at the given index. + * @throws IndexOutOfBoundsException + * If the index is out of range. + */ + public Component getComponent(int index) throws IndexOutOfBoundsException { + return components.get(index); + } + +} diff --git a/server/src/com/vaadin/ui/CustomComponent.java b/server/src/com/vaadin/ui/CustomComponent.java new file mode 100644 index 0000000000..40b5dcd636 --- /dev/null +++ b/server/src/com/vaadin/ui/CustomComponent.java @@ -0,0 +1,189 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.io.Serializable; +import java.util.Iterator; + +/** + * Custom component provides simple implementation of Component interface for + * creation of new UI components by composition of existing components. + * <p> + * The component is used by inheriting the CustomComponent class and setting + * composite root inside the Custom component. The composite root itself can + * contain more components, but their interfaces are hidden from the users. + * </p> + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +@SuppressWarnings("serial") +public class CustomComponent extends AbstractComponentContainer { + + /** + * The root component implementing the custom component. + */ + private Component root = null; + + /** + * Constructs a new custom component. + * + * <p> + * The component is implemented by wrapping the methods of the composition + * root component given as parameter. The composition root must be set + * before the component can be used. + * </p> + */ + public CustomComponent() { + // expand horizontally by default + setWidth(100, UNITS_PERCENTAGE); + } + + /** + * Constructs a new custom component. + * + * <p> + * The component is implemented by wrapping the methods of the composition + * root component given as parameter. The composition root must not be null + * and can not be changed after the composition. + * </p> + * + * @param compositionRoot + * the root of the composition component tree. + */ + public CustomComponent(Component compositionRoot) { + this(); + setCompositionRoot(compositionRoot); + } + + /** + * Returns the composition root. + * + * @return the Component Composition root. + */ + protected Component getCompositionRoot() { + return root; + } + + /** + * Sets the compositions root. + * <p> + * The composition root must be set to non-null value before the component + * can be used. The composition root can only be set once. + * </p> + * + * @param compositionRoot + * the root of the composition component tree. + */ + protected void setCompositionRoot(Component compositionRoot) { + if (compositionRoot != root) { + if (root != null) { + // remove old component + super.removeComponent(root); + } + if (compositionRoot != null) { + // set new component + super.addComponent(compositionRoot); + } + root = compositionRoot; + requestRepaint(); + } + } + + /* Basic component features ------------------------------------------ */ + + private class ComponentIterator implements Iterator<Component>, + Serializable { + boolean first = getCompositionRoot() != null; + + @Override + public boolean hasNext() { + return first; + } + + @Override + public Component next() { + first = false; + return root; + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + } + + @Override + public Iterator<Component> getComponentIterator() { + return new ComponentIterator(); + } + + /** + * Gets the number of contained components. Consistent with the iterator + * returned by {@link #getComponentIterator()}. + * + * @return the number of contained components (zero or one) + */ + @Override + public int getComponentCount() { + return (root != null ? 1 : 0); + } + + /** + * This method is not supported by CustomComponent. + * + * @see com.vaadin.ui.ComponentContainer#replaceComponent(com.vaadin.ui.Component, + * com.vaadin.ui.Component) + */ + @Override + public void replaceComponent(Component oldComponent, Component newComponent) { + throw new UnsupportedOperationException(); + } + + /** + * This method is not supported by CustomComponent. Use + * {@link CustomComponent#setCompositionRoot(Component)} to set + * CustomComponents "child". + * + * @see com.vaadin.ui.AbstractComponentContainer#addComponent(com.vaadin.ui.Component) + */ + @Override + public void addComponent(Component c) { + throw new UnsupportedOperationException(); + } + + /** + * This method is not supported by CustomComponent. + * + * @see com.vaadin.ui.AbstractComponentContainer#moveComponentsFrom(com.vaadin.ui.ComponentContainer) + */ + @Override + public void moveComponentsFrom(ComponentContainer source) { + throw new UnsupportedOperationException(); + } + + /** + * This method is not supported by CustomComponent. + * + * @see com.vaadin.ui.AbstractComponentContainer#removeAllComponents() + */ + @Override + public void removeAllComponents() { + throw new UnsupportedOperationException(); + } + + /** + * This method is not supported by CustomComponent. + * + * @see com.vaadin.ui.AbstractComponentContainer#removeComponent(com.vaadin.ui.Component) + */ + @Override + public void removeComponent(Component c) { + throw new UnsupportedOperationException(); + } + +} diff --git a/server/src/com/vaadin/ui/CustomField.java b/server/src/com/vaadin/ui/CustomField.java new file mode 100644 index 0000000000..ab3797a58c --- /dev/null +++ b/server/src/com/vaadin/ui/CustomField.java @@ -0,0 +1,237 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.io.Serializable; +import java.lang.reflect.Method; +import java.util.Iterator; + +import com.vaadin.data.Property; + +/** + * A {@link Field} whose UI content can be constructed by the user, enabling the + * creation of e.g. form fields by composing Vaadin components. Customization of + * both the visual presentation and the logic of the field is possible. + * + * Subclasses must implement {@link #getType()} and {@link #initContent()}. + * + * Most custom fields can simply compose a user interface that calls the methods + * {@link #setInternalValue(Object)} and {@link #getInternalValue()} when + * necessary. + * + * It is also possible to override {@link #validate()}, + * {@link #setInternalValue(Object)}, {@link #commit()}, + * {@link #setPropertyDataSource(Property)}, {@link #isEmpty()} and other logic + * of the field. Methods overriding {@link #setInternalValue(Object)} should + * also call the corresponding superclass method. + * + * @param <T> + * field value type + * + * @since 7.0 + */ +public abstract class CustomField<T> extends AbstractField<T> implements + ComponentContainer { + + /** + * The root component implementing the custom component. + */ + private Component root = null; + + /** + * Constructs a new custom field. + * + * <p> + * The component is implemented by wrapping the methods of the composition + * root component given as parameter. The composition root must be set + * before the component can be used. + * </p> + */ + public CustomField() { + // expand horizontally by default + setWidth(100, Unit.PERCENTAGE); + } + + /** + * Constructs the content and notifies it that the {@link CustomField} is + * attached to a window. + * + * @see com.vaadin.ui.Component#attach() + */ + @Override + public void attach() { + // First call super attach to notify all children (none if content has + // not yet been created) + super.attach(); + + // If the content has not yet been created, we create and attach it at + // this point. + if (root == null) { + // Ensure content is created and its parent is set. + // The getContent() call creates the content and attaches the + // content + fireComponentAttachEvent(getContent()); + } + } + + /** + * Returns the content (UI) of the custom component. + * + * @return Component + */ + protected Component getContent() { + if (null == root) { + root = initContent(); + root.setParent(this); + } + return root; + } + + /** + * Create the content component or layout for the field. Subclasses of + * {@link CustomField} should implement this method. + * + * Note that this method is called when the CustomField is attached to a + * layout or when {@link #getContent()} is called explicitly for the first + * time. It is only called once for a {@link CustomField}. + * + * @return {@link Component} representing the UI of the CustomField + */ + protected abstract Component initContent(); + + // Size related methods + // TODO might not be necessary to override but following the pattern from + // AbstractComponentContainer + + @Override + public void setHeight(float height, Unit unit) { + super.setHeight(height, unit); + requestRepaintAll(); + } + + @Override + public void setWidth(float height, Unit unit) { + super.setWidth(height, unit); + requestRepaintAll(); + } + + // ComponentContainer methods + + private class ComponentIterator implements Iterator<Component>, + Serializable { + boolean first = (root != null); + + @Override + public boolean hasNext() { + return first; + } + + @Override + public Component next() { + first = false; + return getContent(); + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + } + + @Override + public Iterator<Component> getComponentIterator() { + return new ComponentIterator(); + } + + @Override + public Iterator<Component> iterator() { + return getComponentIterator(); + } + + @Override + public int getComponentCount() { + return (null != getContent()) ? 1 : 0; + } + + /** + * Fires the component attached event. This should be called by the + * addComponent methods after the component have been added to this + * container. + * + * @param component + * the component that has been added to this container. + */ + protected void fireComponentAttachEvent(Component component) { + fireEvent(new ComponentAttachEvent(this, component)); + } + + // TODO remove these methods when ComponentContainer interface is cleaned up + + @Override + public void addComponent(Component c) { + throw new UnsupportedOperationException(); + } + + @Override + public void removeComponent(Component c) { + throw new UnsupportedOperationException(); + } + + @Override + public void removeAllComponents() { + throw new UnsupportedOperationException(); + } + + @Override + public void replaceComponent(Component oldComponent, Component newComponent) { + throw new UnsupportedOperationException(); + } + + @Override + public void moveComponentsFrom(ComponentContainer source) { + throw new UnsupportedOperationException(); + } + + private static final Method COMPONENT_ATTACHED_METHOD; + + static { + try { + COMPONENT_ATTACHED_METHOD = ComponentAttachListener.class + .getDeclaredMethod("componentAttachedToContainer", + new Class[] { ComponentAttachEvent.class }); + } catch (final java.lang.NoSuchMethodException e) { + // This should never happen + throw new java.lang.RuntimeException( + "Internal error finding methods in CustomField"); + } + } + + @Override + public void addListener(ComponentAttachListener listener) { + addListener(ComponentContainer.ComponentAttachEvent.class, listener, + COMPONENT_ATTACHED_METHOD); + } + + @Override + public void removeListener(ComponentAttachListener listener) { + removeListener(ComponentContainer.ComponentAttachEvent.class, listener, + COMPONENT_ATTACHED_METHOD); + } + + @Override + public void addListener(ComponentDetachListener listener) { + // content never detached + } + + @Override + public void removeListener(ComponentDetachListener listener) { + // content never detached + } + + @Override + public boolean isComponentVisible(Component childComponent) { + return true; + } +} diff --git a/server/src/com/vaadin/ui/CustomLayout.java b/server/src/com/vaadin/ui/CustomLayout.java new file mode 100644 index 0000000000..d7830603f0 --- /dev/null +++ b/server/src/com/vaadin/ui/CustomLayout.java @@ -0,0 +1,329 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; + +import com.vaadin.shared.ui.customlayout.CustomLayoutState; +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.PaintTarget; +import com.vaadin.terminal.Vaadin6Component; +import com.vaadin.terminal.gwt.server.JsonPaintTarget; + +/** + * <p> + * A container component with freely designed layout and style. The layout + * consists of items with textually represented locations. Each item contains + * one sub-component, which can be any Vaadin component, such as a layout. The + * adapter and theme are responsible for rendering the layout with a given style + * by placing the items in the defined locations. + * </p> + * + * <p> + * The placement of the locations is not fixed - different themes can define the + * locations in a way that is suitable for them. One typical example would be to + * create visual design for a web site as a custom layout: the visual design + * would define locations for "menu", "body", and "title", for example. The + * layout would then be implemented as an XHTML template for each theme. + * </p> + * + * <p> + * The default theme handles the styles that are not defined by drawing the + * subcomponents just as in OrderedLayout. + * </p> + * + * @author Vaadin Ltd. + * @author Duy B. Vo (<a + * href="mailto:devduy@gmail.com?subject=Vaadin">devduy@gmail.com</a>) + * @version + * @VERSION@ + * @since 3.0 + */ +@SuppressWarnings("serial") +public class CustomLayout extends AbstractLayout implements Vaadin6Component { + + private static final int BUFFER_SIZE = 10000; + + /** + * Custom layout slots containing the components. + */ + private final HashMap<String, Component> slots = new HashMap<String, Component>(); + + /** + * Default constructor only used by subclasses. Subclasses are responsible + * for setting the appropriate fields. Either + * {@link #setTemplateName(String)}, that makes layout fetch the template + * from theme, or {@link #setTemplateContents(String)}. + */ + protected CustomLayout() { + setWidth(100, UNITS_PERCENTAGE); + } + + /** + * Constructs a custom layout with the template given in the stream. + * + * @param templateStream + * Stream containing template data. Must be using UTF-8 encoding. + * To use a String as a template use for instance new + * ByteArrayInputStream("<template>".getBytes()). + * @param streamLength + * Length of the templateStream + * @throws IOException + */ + public CustomLayout(InputStream templateStream) throws IOException { + this(); + initTemplateContentsFromInputStream(templateStream); + } + + /** + * Constructor for custom layout with given template name. Template file is + * fetched from "<theme>/layout/<templateName>". + */ + public CustomLayout(String template) { + this(); + setTemplateName(template); + } + + protected void initTemplateContentsFromInputStream( + InputStream templateStream) throws IOException { + InputStreamReader reader = new InputStreamReader(templateStream, + "UTF-8"); + StringBuilder b = new StringBuilder(BUFFER_SIZE); + + char[] cbuf = new char[BUFFER_SIZE]; + int offset = 0; + + while (true) { + int nrRead = reader.read(cbuf, offset, BUFFER_SIZE); + b.append(cbuf, 0, nrRead); + if (nrRead < BUFFER_SIZE) { + break; + } + } + + setTemplateContents(b.toString()); + } + + @Override + public CustomLayoutState getState() { + return (CustomLayoutState) super.getState(); + } + + /** + * Adds the component into this container to given location. If the location + * is already populated, the old component is removed. + * + * @param c + * the component to be added. + * @param location + * the location of the component. + */ + public void addComponent(Component c, String location) { + final Component old = slots.get(location); + if (old != null) { + removeComponent(old); + } + slots.put(location, c); + getState().getChildLocations().put(c, location); + c.setParent(this); + fireComponentAttachEvent(c); + requestRepaint(); + } + + /** + * Adds the component into this container. The component is added without + * specifying the location (empty string is then used as location). Only one + * component can be added to the default "" location and adding more + * components into that location overwrites the old components. + * + * @param c + * the component to be added. + */ + @Override + public void addComponent(Component c) { + this.addComponent(c, ""); + } + + /** + * Removes the component from this container. + * + * @param c + * the component to be removed. + */ + @Override + public void removeComponent(Component c) { + if (c == null) { + return; + } + slots.values().remove(c); + getState().getChildLocations().remove(c); + super.removeComponent(c); + requestRepaint(); + } + + /** + * Removes the component from this container from given location. + * + * @param location + * the Location identifier of the component. + */ + public void removeComponent(String location) { + this.removeComponent(slots.get(location)); + } + + /** + * Gets the component container iterator for going trough all the components + * in the container. + * + * @return the Iterator of the components inside the container. + */ + @Override + public Iterator<Component> getComponentIterator() { + return slots.values().iterator(); + } + + /** + * Gets the number of contained components. Consistent with the iterator + * returned by {@link #getComponentIterator()}. + * + * @return the number of contained components + */ + @Override + public int getComponentCount() { + return slots.values().size(); + } + + /** + * Gets the child-component by its location. + * + * @param location + * the name of the location where the requested component + * resides. + * @return the Component in the given location or null if not found. + */ + public Component getComponent(String location) { + return slots.get(location); + } + + /* Documented in superclass */ + @Override + public void replaceComponent(Component oldComponent, Component newComponent) { + + // Gets the locations + String oldLocation = null; + String newLocation = null; + for (final Iterator<String> i = slots.keySet().iterator(); i.hasNext();) { + final String location = i.next(); + final Component component = slots.get(location); + if (component == oldComponent) { + oldLocation = location; + } + if (component == newComponent) { + newLocation = location; + } + } + + if (oldLocation == null) { + addComponent(newComponent); + } else if (newLocation == null) { + removeComponent(oldLocation); + addComponent(newComponent, oldLocation); + } else { + slots.put(newLocation, oldComponent); + slots.put(oldLocation, newComponent); + getState().getChildLocations().put(newComponent, oldLocation); + getState().getChildLocations().put(oldComponent, newLocation); + requestRepaint(); + } + } + + /** Get the name of the template */ + public String getTemplateName() { + return getState().getTemplateName(); + } + + /** Get the contents of the template */ + public String getTemplateContents() { + return getState().getTemplateContents(); + } + + /** + * Set the name of the template used to draw custom layout. + * + * With GWT-adapter, the template with name 'templatename' is loaded from + * VAADIN/themes/themename/layouts/templatename.html. If the theme has not + * been set (with Application.setTheme()), themename is 'default'. + * + * @param templateName + */ + public void setTemplateName(String templateName) { + getState().setTemplateName(templateName); + getState().setTemplateContents(null); + requestRepaint(); + } + + /** + * Set the contents of the template used to draw the custom layout. + * + * @param templateContents + */ + public void setTemplateContents(String templateContents) { + getState().setTemplateContents(templateContents); + getState().setTemplateName(null); + requestRepaint(); + } + + /** + * Although most layouts support margins, CustomLayout does not. The + * behaviour of this layout is determined almost completely by the actual + * template. + * + * @throws UnsupportedOperationException + */ + @Override + public void setMargin(boolean enabled) { + throw new UnsupportedOperationException( + "CustomLayout does not support margins."); + } + + /** + * Although most layouts support margins, CustomLayout does not. The + * behaviour of this layout is determined almost completely by the actual + * template. + * + * @throws UnsupportedOperationException + */ + @Override + public void setMargin(boolean topEnabled, boolean rightEnabled, + boolean bottomEnabled, boolean leftEnabled) { + throw new UnsupportedOperationException( + "CustomLayout does not support margins."); + } + + @Override + public void changeVariables(Object source, Map<String, Object> variables) { + // Nothing to see here + } + + @Override + public void paintContent(PaintTarget target) throws PaintException { + // Workaround to make the CommunicationManager read the template file + // and send it to the client + String templateName = getState().getTemplateName(); + if (templateName != null && templateName.length() != 0) { + Set<Object> usedResources = ((JsonPaintTarget) target) + .getUsedResources(); + String resourceName = "layouts/" + templateName + ".html"; + usedResources.add(resourceName); + } + } + +} diff --git a/server/src/com/vaadin/ui/DateField.java b/server/src/com/vaadin/ui/DateField.java new file mode 100644 index 0000000000..d0a22f3c29 --- /dev/null +++ b/server/src/com/vaadin/ui/DateField.java @@ -0,0 +1,869 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Collection; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.TimeZone; + +import com.vaadin.data.Property; +import com.vaadin.data.Validator; +import com.vaadin.data.Validator.InvalidValueException; +import com.vaadin.data.util.converter.Converter; +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.terminal.PaintException; +import com.vaadin.terminal.PaintTarget; +import com.vaadin.terminal.Vaadin6Component; +import com.vaadin.terminal.gwt.client.ui.datefield.VDateField; + +/** + * <p> + * A date editor component that can be bound to any {@link Property} that is + * compatible with <code>java.util.Date</code>. + * </p> + * <p> + * Since <code>DateField</code> extends <code>AbstractField</code> it implements + * the {@link com.vaadin.data.Buffered}interface. + * </p> + * <p> + * A <code>DateField</code> is in write-through mode by default, so + * {@link com.vaadin.ui.AbstractField#setWriteThrough(boolean)}must be called to + * enable buffering. + * </p> + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +@SuppressWarnings("serial") +public class DateField extends AbstractField<Date> implements + FieldEvents.BlurNotifier, FieldEvents.FocusNotifier, Vaadin6Component { + + /** + * Resolutions for DateFields + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 7.0 + */ + public enum Resolution { + SECOND(Calendar.SECOND), MINUTE(Calendar.MINUTE), HOUR( + Calendar.HOUR_OF_DAY), DAY(Calendar.DAY_OF_MONTH), MONTH( + Calendar.MONTH), YEAR(Calendar.YEAR); + + private int calendarField; + + private Resolution(int calendarField) { + this.calendarField = calendarField; + } + + /** + * Returns the field in {@link Calendar} that corresponds to this + * resolution. + * + * @return one of the field numbers used by Calendar + */ + public int getCalendarField() { + return calendarField; + } + + /** + * Returns the resolutions that are higher or equal to the given + * resolution, starting from the given resolution. In other words + * passing DAY to this methods returns DAY,MONTH,YEAR + * + * @param r + * The resolution to start from + * @return An iterable for the resolutions higher or equal to r + */ + public static Iterable<Resolution> getResolutionsHigherOrEqualTo( + Resolution r) { + List<Resolution> resolutions = new ArrayList<DateField.Resolution>(); + Resolution[] values = Resolution.values(); + for (int i = r.ordinal(); i < values.length; i++) { + resolutions.add(values[i]); + } + return resolutions; + } + + /** + * Returns the resolutions that are lower than the given resolution, + * starting from the given resolution. In other words passing DAY to + * this methods returns HOUR,MINUTE,SECOND. + * + * @param r + * The resolution to start from + * @return An iterable for the resolutions lower than r + */ + public static List<Resolution> getResolutionsLowerThan(Resolution r) { + List<Resolution> resolutions = new ArrayList<DateField.Resolution>(); + Resolution[] values = Resolution.values(); + for (int i = r.ordinal() - 1; i >= 0; i--) { + resolutions.add(values[i]); + } + return resolutions; + } + }; + + /** + * Resolution identifier: seconds. + * + * @deprecated Use {@link Resolution#SECOND} + */ + @Deprecated + public static final Resolution RESOLUTION_SEC = Resolution.SECOND; + + /** + * Resolution identifier: minutes. + * + * @deprecated Use {@link Resolution#MINUTE} + */ + @Deprecated + public static final Resolution RESOLUTION_MIN = Resolution.MINUTE; + + /** + * Resolution identifier: hours. + * + * @deprecated Use {@link Resolution#HOUR} + */ + @Deprecated + public static final Resolution RESOLUTION_HOUR = Resolution.HOUR; + + /** + * Resolution identifier: days. + * + * @deprecated Use {@link Resolution#DAY} + */ + @Deprecated + public static final Resolution RESOLUTION_DAY = Resolution.DAY; + + /** + * Resolution identifier: months. + * + * @deprecated Use {@link Resolution#MONTH} + */ + @Deprecated + public static final Resolution RESOLUTION_MONTH = Resolution.MONTH; + + /** + * Resolution identifier: years. + * + * @deprecated Use {@link Resolution#YEAR} + */ + @Deprecated + public static final Resolution RESOLUTION_YEAR = Resolution.YEAR; + + /** + * Specified smallest modifiable unit for the date field. + */ + private Resolution resolution = Resolution.DAY; + + /** + * The internal calendar to be used in java.utl.Date conversions. + */ + private transient Calendar calendar; + + /** + * Overridden format string + */ + private String dateFormat; + + private boolean lenient = false; + + private String dateString = null; + + /** + * Was the last entered string parsable? If this flag is false, datefields + * internal validator does not pass. + */ + private boolean uiHasValidDateString = true; + + /** + * Determines if week numbers are shown in the date selector. + */ + private boolean showISOWeekNumbers = false; + + private String currentParseErrorMessage; + + private String defaultParseErrorMessage = "Date format not recognized"; + + private TimeZone timeZone = null; + + private static Map<Resolution, String> variableNameForResolution = new HashMap<DateField.Resolution, String>(); + { + variableNameForResolution.put(Resolution.SECOND, "sec"); + variableNameForResolution.put(Resolution.MINUTE, "min"); + variableNameForResolution.put(Resolution.HOUR, "hour"); + variableNameForResolution.put(Resolution.DAY, "day"); + variableNameForResolution.put(Resolution.MONTH, "month"); + variableNameForResolution.put(Resolution.YEAR, "year"); + } + + /* Constructors */ + + /** + * Constructs an empty <code>DateField</code> with no caption. + */ + public DateField() { + } + + /** + * Constructs an empty <code>DateField</code> with caption. + * + * @param caption + * the caption of the datefield. + */ + public DateField(String caption) { + setCaption(caption); + } + + /** + * Constructs a new <code>DateField</code> that's bound to the specified + * <code>Property</code> and has the given caption <code>String</code>. + * + * @param caption + * the caption <code>String</code> for the editor. + * @param dataSource + * the Property to be edited with this editor. + */ + public DateField(String caption, Property dataSource) { + this(dataSource); + setCaption(caption); + } + + /** + * Constructs a new <code>DateField</code> that's bound to the specified + * <code>Property</code> and has no caption. + * + * @param dataSource + * the Property to be edited with this editor. + */ + public DateField(Property dataSource) throws IllegalArgumentException { + if (!Date.class.isAssignableFrom(dataSource.getType())) { + throw new IllegalArgumentException("Can't use " + + dataSource.getType().getName() + + " typed property as datasource"); + } + + setPropertyDataSource(dataSource); + } + + /** + * 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)} + * is called to bind it. + * + * @param caption + * the caption <code>String</code> for the editor. + * @param value + * the Date value. + */ + public DateField(String caption, Date value) { + setValue(value); + setCaption(caption); + } + + /* Component basic features */ + + /* + * Paints this component. Don't add a JavaDoc comment here, we use the + * default documentation from implemented interface. + */ + @Override + public void paintContent(PaintTarget target) throws PaintException { + + // Adds the locale as attribute + final Locale l = getLocale(); + if (l != null) { + target.addAttribute("locale", l.toString()); + } + + if (getDateFormat() != null) { + target.addAttribute("format", dateFormat); + } + + if (!isLenient()) { + target.addAttribute("strict", true); + } + + target.addAttribute(VDateField.WEEK_NUMBERS, isShowISOWeekNumbers()); + target.addAttribute("parsable", uiHasValidDateString); + /* + * TODO communicate back the invalid date string? E.g. returning back to + * app or refresh. + */ + + // Gets the calendar + final Calendar calendar = getCalendar(); + final Date currentDate = getValue(); + + // Only paint variables for the resolution and up, e.g. Resolution DAY + // paints DAY,MONTH,YEAR + for (Resolution res : Resolution + .getResolutionsHigherOrEqualTo(resolution)) { + int value = -1; + if (currentDate != null) { + value = calendar.get(res.getCalendarField()); + if (res == Resolution.MONTH) { + // Calendar month is zero based + value++; + } + } + target.addVariable(this, variableNameForResolution.get(res), value); + } + } + + @Override + protected boolean shouldHideErrors() { + return super.shouldHideErrors() && uiHasValidDateString; + } + + /* + * Invoked when a variable of the component changes. Don't add a JavaDoc + * comment here, we use the default documentation from implemented + * interface. + */ + @Override + public void changeVariables(Object source, Map<String, Object> variables) { + + if (!isReadOnly() + && (variables.containsKey("year") + || variables.containsKey("month") + || variables.containsKey("day") + || variables.containsKey("hour") + || variables.containsKey("min") + || variables.containsKey("sec") + || variables.containsKey("msec") || variables + .containsKey("dateString"))) { + + // Old and new dates + final Date oldDate = getValue(); + Date newDate = null; + + // this enables analyzing invalid input on the server + final String newDateString = (String) variables.get("dateString"); + dateString = newDateString; + + // Gets the new date in parts + boolean hasChanges = false; + Map<Resolution, Integer> calendarFieldChanges = new HashMap<DateField.Resolution, Integer>(); + + for (Resolution r : Resolution + .getResolutionsHigherOrEqualTo(resolution)) { + // Only handle what the client is allowed to send. The same + // resolutions that are painted + String variableName = variableNameForResolution.get(r); + + if (variables.containsKey(variableName)) { + Integer value = (Integer) variables.get(variableName); + if (r == Resolution.MONTH) { + // Calendar MONTH is zero based + value--; + } + if (value >= 0) { + hasChanges = true; + calendarFieldChanges.put(r, value); + } + } + } + + // If no new variable values were received, use the previous value + if (!hasChanges) { + newDate = null; + } else { + // Clone the calendar for date operation + final Calendar cal = getCalendar(); + + // Update the value based on the received info + // Must set in this order to avoid invalid dates (or wrong + // dates if lenient is true) in calendar + for (int r = Resolution.YEAR.ordinal(); r >= 0; r--) { + Resolution res = Resolution.values()[r]; + if (calendarFieldChanges.containsKey(res)) { + + // Field resolution should be included. Others are + // skipped so that client can not make unexpected + // changes (e.g. day change even though resolution is + // year). + Integer newValue = calendarFieldChanges.get(res); + cal.set(res.getCalendarField(), newValue); + } + } + newDate = cal.getTime(); + } + + if (newDate == null && dateString != null && !"".equals(dateString)) { + try { + Date parsedDate = handleUnparsableDateString(dateString); + setValue(parsedDate, true); + + /* + * Ensure the value is sent to the client if the value is + * set to the same as the previous (#4304). Does not repaint + * if handleUnparsableDateString throws an exception. In + * this case the invalid text remains in the DateField. + */ + requestRepaint(); + } catch (Converter.ConversionException e) { + + /* + * Datefield now contains some text that could't be parsed + * into date. + */ + if (oldDate != null) { + /* + * Set the logic value to null. + */ + setValue(null); + /* + * Reset the dateString (overridden to null by setValue) + */ + dateString = newDateString; + } + + /* + * Saves the localized message of parse error. This can be + * overridden in handleUnparsableDateString. The message + * will later be used to show a validation error. + */ + currentParseErrorMessage = e.getLocalizedMessage(); + + /* + * The value of the DateField should be null if an invalid + * value has been given. Not using setValue() since we do + * not want to cause the client side value to change. + */ + uiHasValidDateString = false; + + /* + * Because of our custom implementation of isValid(), that + * also checks the parsingSucceeded flag, we must also + * notify the form (if this is used in one) that the + * validity of this field has changed. + * + * Normally fields validity doesn't change without value + * change and form depends on this implementation detail. + */ + notifyFormOfValidityChange(); + requestRepaint(); + } + } else if (newDate != oldDate + && (newDate == null || !newDate.equals(oldDate))) { + setValue(newDate, true); // Don't require a repaint, client + // updates itself + } else if (!uiHasValidDateString) { // oldDate == + // newDate == null + // Empty value set, previously contained unparsable date string, + // clear related internal fields + setValue(null); + } + } + + if (variables.containsKey(FocusEvent.EVENT_ID)) { + fireEvent(new FocusEvent(this)); + } + + if (variables.containsKey(BlurEvent.EVENT_ID)) { + fireEvent(new BlurEvent(this)); + } + } + + /** + * This method is called to handle a non-empty date string from the client + * if the client could not parse it as a Date. + * + * By default, a Converter.ConversionException is thrown, and the current + * value is not modified. + * + * This can be overridden to handle conversions, to return null (equivalent + * to empty input), to throw an exception or to fire an event. + * + * @param dateString + * @return parsed Date + * @throws Converter.ConversionException + * to keep the old value and indicate an error + */ + protected Date handleUnparsableDateString(String dateString) + throws Converter.ConversionException { + currentParseErrorMessage = null; + throw new Converter.ConversionException(getParseErrorMessage()); + } + + /* Property features */ + + /* + * Gets the edited property's type. Don't add a JavaDoc comment here, we use + * the default documentation from implemented interface. + */ + @Override + public Class<Date> getType() { + return Date.class; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.AbstractField#setValue(java.lang.Object, boolean) + */ + @Override + protected void setValue(Date newValue, boolean repaintIsNotNeeded) + throws Property.ReadOnlyException { + + /* + * First handle special case when the client side component have a date + * string but value is null (e.g. unparsable date string typed in by the + * user). No value changes should happen, but we need to do some + * internal housekeeping. + */ + if (newValue == null && !uiHasValidDateString) { + /* + * Side-effects of setInternalValue clears possible previous strings + * and flags about invalid input. + */ + setInternalValue(null); + + /* + * Due to DateField's special implementation of isValid(), + * datefields validity may change although the logical value does + * not change. This is an issue for Form which expects that validity + * of Fields cannot change unless actual value changes. + * + * So we check if this field is inside a form and the form has + * registered this as a field. In this case we repaint the form. + * Without this hacky solution the form might not be able to clean + * validation errors etc. We could avoid this by firing an extra + * value change event, but feels like at least as bad solution as + * this. + */ + notifyFormOfValidityChange(); + requestRepaint(); + return; + } + + super.setValue(newValue, repaintIsNotNeeded); + } + + /** + * Detects if this field is used in a Form (logically) and if so, notifies + * it (by repainting it) that the validity of this field might have changed. + */ + private void notifyFormOfValidityChange() { + Component parenOfDateField = getParent(); + boolean formFound = false; + while (parenOfDateField != null || formFound) { + if (parenOfDateField instanceof Form) { + Form f = (Form) parenOfDateField; + Collection<?> visibleItemProperties = f.getItemPropertyIds(); + for (Object fieldId : visibleItemProperties) { + Field<?> field = f.getField(fieldId); + if (field == this) { + /* + * this datefield is logically in a form. Do the same + * thing as form does in its value change listener that + * it registers to all fields. + */ + f.requestRepaint(); + formFound = true; + break; + } + } + } + if (formFound) { + break; + } + parenOfDateField = parenOfDateField.getParent(); + } + } + + @Override + protected void setInternalValue(Date newValue) { + // Also set the internal dateString + if (newValue != null) { + dateString = newValue.toString(); + } else { + dateString = null; + } + + if (!uiHasValidDateString) { + // clear component error and parsing flag + setComponentError(null); + uiHasValidDateString = true; + currentParseErrorMessage = null; + } + + super.setInternalValue(newValue); + } + + /** + * Gets the resolution. + * + * @return int + */ + public Resolution getResolution() { + return resolution; + } + + /** + * Sets the resolution of the DateField. + * + * The default resolution is {@link Resolution#DAY} since Vaadin 7.0. + * + * @param resolution + * the resolution to set. + */ + public void setResolution(Resolution resolution) { + this.resolution = resolution; + requestRepaint(); + } + + /** + * Returns new instance calendar used in Date conversions. + * + * Returns new clone of the calendar object initialized using the the + * current date (if available) + * + * If this is no calendar is assigned the <code>Calendar.getInstance</code> + * is used. + * + * @return the Calendar. + * @see #setCalendar(Calendar) + */ + private Calendar getCalendar() { + + // Makes sure we have an calendar instance + if (calendar == null) { + calendar = Calendar.getInstance(); + // Start by a zeroed calendar to avoid having values for lower + // resolution variables e.g. time when resolution is day + for (Resolution r : Resolution.getResolutionsLowerThan(resolution)) { + calendar.set(r.getCalendarField(), 0); + } + calendar.set(Calendar.MILLISECOND, 0); + } + + // Clone the instance + final Calendar newCal = (Calendar) calendar.clone(); + + // Assigns the current time tom calendar. + final Date currentDate = getValue(); + if (currentDate != null) { + newCal.setTime(currentDate); + } + + final TimeZone currentTimeZone = getTimeZone(); + if (currentTimeZone != null) { + newCal.setTimeZone(currentTimeZone); + } + + return newCal; + } + + /** + * Sets formatting used by some component implementations. See + * {@link SimpleDateFormat} for format details. + * + * By default it is encouraged to used default formatting defined by Locale, + * but due some JVM bugs it is sometimes necessary to use this method to + * override formatting. See Vaadin issue #2200. + * + * @param dateFormat + * the dateFormat to set + * + * @see com.vaadin.ui.AbstractComponent#setLocale(Locale)) + */ + public void setDateFormat(String dateFormat) { + this.dateFormat = dateFormat; + requestRepaint(); + } + + /** + * Returns a format string used to format date value on client side or null + * if default formatting from {@link Component#getLocale()} is used. + * + * @return the dateFormat + */ + public String getDateFormat() { + return dateFormat; + } + + /** + * Specifies whether or not date/time interpretation in component is to be + * lenient. + * + * @see Calendar#setLenient(boolean) + * @see #isLenient() + * + * @param lenient + * true if the lenient mode is to be turned on; false if it is to + * be turned off. + */ + public void setLenient(boolean lenient) { + this.lenient = lenient; + requestRepaint(); + } + + /** + * Returns whether date/time interpretation is to be lenient. + * + * @see #setLenient(boolean) + * + * @return true if the interpretation mode of this calendar is lenient; + * false otherwise. + */ + public boolean isLenient() { + return lenient; + } + + @Override + public void addListener(FocusListener listener) { + addListener(FocusEvent.EVENT_ID, FocusEvent.class, listener, + FocusListener.focusMethod); + } + + @Override + public void removeListener(FocusListener listener) { + removeListener(FocusEvent.EVENT_ID, FocusEvent.class, listener); + } + + @Override + public void addListener(BlurListener listener) { + addListener(BlurEvent.EVENT_ID, BlurEvent.class, listener, + BlurListener.blurMethod); + } + + @Override + public void removeListener(BlurListener listener) { + removeListener(BlurEvent.EVENT_ID, BlurEvent.class, listener); + } + + /** + * Checks whether ISO 8601 week numbers are shown in the date selector. + * + * @return true if week numbers are shown, false otherwise. + */ + public boolean isShowISOWeekNumbers() { + return showISOWeekNumbers; + } + + /** + * Sets the visibility of ISO 8601 week numbers in the date selector. ISO + * 8601 defines that a week always starts with a Monday so the week numbers + * are only shown if this is the case. + * + * @param showWeekNumbers + * true if week numbers should be shown, false otherwise. + */ + public void setShowISOWeekNumbers(boolean showWeekNumbers) { + showISOWeekNumbers = showWeekNumbers; + requestRepaint(); + } + + /** + * Validates the current value against registered validators if the field is + * not empty. Note that DateField is considered empty (value == null) and + * invalid if it contains text typed in by the user that couldn't be parsed + * into a Date value. + * + * @see com.vaadin.ui.AbstractField#validate() + */ + @Override + public void validate() throws InvalidValueException { + /* + * To work properly in form we must throw exception if there is + * currently a parsing error in the datefield. Parsing error is kind of + * an internal validator. + */ + if (!uiHasValidDateString) { + throw new UnparsableDateString(currentParseErrorMessage); + } + super.validate(); + } + + /** + * Return the error message that is shown if the user inputted value can't + * be parsed into a Date object. If + * {@link #handleUnparsableDateString(String)} is overridden and it throws a + * custom exception, the message returned by + * {@link Exception#getLocalizedMessage()} will be used instead of the value + * returned by this method. + * + * @see #setParseErrorMessage(String) + * + * @return the error message that the DateField uses when it can't parse the + * textual input from user to a Date object + */ + public String getParseErrorMessage() { + return defaultParseErrorMessage; + } + + /** + * Sets the default error message used if the DateField cannot parse the + * text input by user to a Date field. Note that if the + * {@link #handleUnparsableDateString(String)} method is overridden, the + * localized message from its exception is used. + * + * @see #getParseErrorMessage() + * @see #handleUnparsableDateString(String) + * @param parsingErrorMessage + */ + public void setParseErrorMessage(String parsingErrorMessage) { + defaultParseErrorMessage = parsingErrorMessage; + } + + /** + * Sets the time zone used by this date field. The time zone is used to + * convert the absolute time in a Date object to a logical time displayed in + * the selector and to convert the select time back to a Date object. + * + * If no time zone has been set, the current default time zone returned by + * {@code TimeZone.getDefault()} is used. + * + * @see #getTimeZone() + * @param timeZone + * the time zone to use for time calculations. + */ + public void setTimeZone(TimeZone timeZone) { + this.timeZone = timeZone; + requestRepaint(); + } + + /** + * Gets the time zone used by this field. The time zone is used to convert + * the absolute time in a Date object to a logical time displayed in the + * selector and to convert the select time back to a Date object. + * + * If {@code null} is returned, the current default time zone returned by + * {@code TimeZone.getDefault()} is used. + * + * @return the current time zone + */ + public TimeZone getTimeZone() { + return timeZone; + } + + public static class UnparsableDateString extends + Validator.InvalidValueException { + + public UnparsableDateString(String message) { + super(message); + } + + } +} diff --git a/server/src/com/vaadin/ui/DefaultFieldFactory.java b/server/src/com/vaadin/ui/DefaultFieldFactory.java new file mode 100644 index 0000000000..e17f08c1c6 --- /dev/null +++ b/server/src/com/vaadin/ui/DefaultFieldFactory.java @@ -0,0 +1,146 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.ui; + +import java.util.Date; + +import com.vaadin.data.Container; +import com.vaadin.data.Item; +import com.vaadin.data.Property; + +/** + * This class contains a basic implementation for both {@link FormFieldFactory} + * and {@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 FormFieldFactory, 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(Item item, Object propertyId, + Component uiContext) { + Class<?> type = item.getItemProperty(propertyId).getType(); + Field<?> field = createFieldByPropertyType(type); + field.setCaption(createCaptionByPropertyId(propertyId)); + return field; + } + + @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) { + String name = propertyId.toString(); + if (name.length() > 0) { + + int dotLocation = name.lastIndexOf('.'); + if (dotLocation > 0 && dotLocation < name.length() - 1) { + name = name.substring(dotLocation + 1); + } + if (name.indexOf(' ') < 0 + && name.charAt(0) == Character.toLowerCase(name.charAt(0)) + && name.charAt(0) != Character.toUpperCase(name.charAt(0))) { + StringBuffer out = new StringBuffer(); + out.append(Character.toUpperCase(name.charAt(0))); + int i = 1; + + while (i < name.length()) { + int j = i; + for (; j < name.length(); j++) { + char c = name.charAt(j); + if (Character.toLowerCase(c) != c + && Character.toUpperCase(c) == c) { + break; + } + } + if (j == name.length()) { + out.append(name.substring(i)); + } else { + out.append(name.substring(i, j)); + out.append(" " + name.charAt(j)); + } + i = j + 1; + } + + name = out.toString(); + } + } + return name; + } + + /** + * 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; + } + + // Item field + if (Item.class.isAssignableFrom(type)) { + return new Form(); + } + + // 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/server/src/com/vaadin/ui/DragAndDropWrapper.java b/server/src/com/vaadin/ui/DragAndDropWrapper.java new file mode 100644 index 0000000000..67229a45fe --- /dev/null +++ b/server/src/com/vaadin/ui/DragAndDropWrapper.java @@ -0,0 +1,407 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.ui; + +import java.io.OutputStream; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Map.Entry; + +import com.vaadin.event.Transferable; +import com.vaadin.event.TransferableImpl; +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.TargetDetailsImpl; +import com.vaadin.shared.MouseEventDetails; +import com.vaadin.shared.ui.dd.HorizontalDropLocation; +import com.vaadin.shared.ui.dd.VerticalDropLocation; +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.PaintTarget; +import com.vaadin.terminal.StreamVariable; +import com.vaadin.terminal.Vaadin6Component; +import com.vaadin.terminal.gwt.client.ui.draganddropwrapper.VDragAndDropWrapper; + +@SuppressWarnings("serial") +public class DragAndDropWrapper extends CustomComponent implements DropTarget, + DragSource, Vaadin6Component { + + public class WrapperTransferable extends TransferableImpl { + + private Html5File[] files; + + public WrapperTransferable(Component sourceComponent, + Map<String, Object> rawVariables) { + super(sourceComponent, rawVariables); + Integer fc = (Integer) rawVariables.get("filecount"); + if (fc != null) { + files = new Html5File[fc]; + for (int i = 0; i < fc; i++) { + Html5File file = new Html5File( + (String) rawVariables.get("fn" + i), // name + (Integer) rawVariables.get("fs" + i), // size + (String) rawVariables.get("ft" + i)); // mime + String id = (String) rawVariables.get("fi" + i); + files[i] = file; + receivers.put(id, file); + requestRepaint(); // paint Receivers + } + } + } + + /** + * The component in wrapper that is being dragged or null if the + * transferable is not a component (most likely an html5 drag). + * + * @return + */ + public Component getDraggedComponent() { + Component object = (Component) getData("component"); + return object; + } + + /** + * @return the mouse down event that started the drag and drop operation + */ + public MouseEventDetails getMouseDownEvent() { + return MouseEventDetails.deSerialize((String) getData("mouseDown")); + } + + public Html5File[] getFiles() { + return files; + } + + public String getText() { + String data = (String) getData("Text"); // IE, html5 + if (data == null) { + // check for "text/plain" (webkit) + data = (String) getData("text/plain"); + } + return data; + } + + public String getHtml() { + String data = (String) getData("Html"); // IE, html5 + if (data == null) { + // check for "text/plain" (webkit) + data = (String) getData("text/html"); + } + return data; + } + + } + + private Map<String, Html5File> receivers = new HashMap<String, Html5File>(); + + public class WrapperTargetDetails extends TargetDetailsImpl { + + public WrapperTargetDetails(Map<String, Object> rawDropData) { + super(rawDropData, DragAndDropWrapper.this); + } + + /** + * @return the absolute position of wrapper on the page + */ + public Integer getAbsoluteLeft() { + return (Integer) getData("absoluteLeft"); + } + + /** + * + * @return the absolute position of wrapper on the page + */ + public Integer getAbsoluteTop() { + return (Integer) getData("absoluteTop"); + } + + /** + * @return details about the actual event that caused the event details. + * Practically mouse move or mouse up. + */ + public MouseEventDetails getMouseEvent() { + return MouseEventDetails + .deSerialize((String) getData("mouseEvent")); + } + + /** + * @return a detail about the drags vertical position over the wrapper. + */ + public VerticalDropLocation getVerticalDropLocation() { + return VerticalDropLocation + .valueOf((String) getData("verticalLocation")); + } + + /** + * @return a detail about the drags horizontal position over the + * wrapper. + */ + public HorizontalDropLocation getHorizontalDropLocation() { + return HorizontalDropLocation + .valueOf((String) getData("horizontalLocation")); + } + + /** + * @deprecated use {@link #getVerticalDropLocation()} instead + */ + @Deprecated + public VerticalDropLocation verticalDropLocation() { + return getVerticalDropLocation(); + } + + /** + * @deprecated use {@link #getHorizontalDropLocation()} instead + */ + @Deprecated + public HorizontalDropLocation horizontalDropLocation() { + return getHorizontalDropLocation(); + } + + } + + public enum DragStartMode { + /** + * {@link DragAndDropWrapper} does not start drag events at all + */ + NONE, + /** + * The component on which the drag started will be shown as drag image. + */ + COMPONENT, + /** + * The whole wrapper is used as a drag image when dragging. + */ + WRAPPER, + /** + * The whole wrapper is used to start an HTML5 drag. + * + * NOTE: In Internet Explorer 6 to 8, this prevents user interactions + * with the wrapper's contents. For example, clicking a button inside + * the wrapper will no longer work. + */ + HTML5, + } + + private final Map<String, Object> html5DataFlavors = new LinkedHashMap<String, Object>(); + private DragStartMode dragStartMode = DragStartMode.NONE; + + /** + * Wraps given component in a {@link DragAndDropWrapper}. + * + * @param root + * the component to be wrapped + */ + public DragAndDropWrapper(Component root) { + super(root); + } + + /** + * Sets data flavors available in the DragAndDropWrapper is used to start an + * HTML5 style drags. Most commonly the "Text" flavor should be set. + * Multiple data types can be set. + * + * @param type + * the string identifier of the drag "payload". E.g. "Text" or + * "text/html" + * @param value + * the value + */ + public void setHTML5DataFlavor(String type, Object value) { + html5DataFlavors.put(type, value); + requestRepaint(); + } + + @Override + public void changeVariables(Object source, Map<String, Object> variables) { + // TODO Remove once Vaadin6Component is no longer implemented + } + + @Override + public void paintContent(PaintTarget target) throws PaintException { + target.addAttribute(VDragAndDropWrapper.DRAG_START_MODE, + dragStartMode.ordinal()); + if (getDropHandler() != null) { + getDropHandler().getAcceptCriterion().paint(target); + } + if (receivers != null && receivers.size() > 0) { + for (Iterator<Entry<String, Html5File>> it = receivers.entrySet() + .iterator(); it.hasNext();) { + Entry<String, com.vaadin.ui.Html5File> entry = it.next(); + String id = entry.getKey(); + Html5File html5File = entry.getValue(); + if (html5File.getStreamVariable() != null) { + target.addVariable(this, "rec-" + id, new ProxyReceiver( + html5File)); + // these are cleaned from receivers once the upload has + // started + } else { + // instructs the client side not to send the file + target.addVariable(this, "rec-" + id, (String) null); + // forget the file from subsequent paints + it.remove(); + } + } + } + target.addAttribute(VDragAndDropWrapper.HTML5_DATA_FLAVORS, + html5DataFlavors); + } + + private DropHandler dropHandler; + + @Override + public DropHandler getDropHandler() { + return dropHandler; + } + + public void setDropHandler(DropHandler dropHandler) { + this.dropHandler = dropHandler; + requestRepaint(); + } + + @Override + public TargetDetails translateDropTargetDetails( + Map<String, Object> clientVariables) { + return new WrapperTargetDetails(clientVariables); + } + + @Override + public Transferable getTransferable(final Map<String, Object> rawVariables) { + return new WrapperTransferable(this, rawVariables); + } + + public void setDragStartMode(DragStartMode dragStartMode) { + this.dragStartMode = dragStartMode; + requestRepaint(); + } + + public DragStartMode getDragStartMode() { + return dragStartMode; + } + + final class ProxyReceiver implements StreamVariable { + + private Html5File file; + + public ProxyReceiver(Html5File file) { + this.file = file; + } + + private boolean listenProgressOfUploadedFile; + + @Override + public OutputStream getOutputStream() { + if (file.getStreamVariable() == null) { + return null; + } + return file.getStreamVariable().getOutputStream(); + } + + @Override + public boolean listenProgress() { + return file.getStreamVariable().listenProgress(); + } + + @Override + public void onProgress(StreamingProgressEvent event) { + file.getStreamVariable().onProgress( + new ReceivingEventWrapper(event)); + } + + @Override + public void streamingStarted(StreamingStartEvent event) { + listenProgressOfUploadedFile = file.getStreamVariable() != null; + if (listenProgressOfUploadedFile) { + file.getStreamVariable().streamingStarted( + new ReceivingEventWrapper(event)); + } + // no need tell to the client about this receiver on next paint + receivers.remove(file); + // let the terminal GC the streamvariable and not to accept other + // file uploads to this variable + event.disposeStreamVariable(); + } + + @Override + public void streamingFinished(StreamingEndEvent event) { + if (listenProgressOfUploadedFile) { + file.getStreamVariable().streamingFinished( + new ReceivingEventWrapper(event)); + } + } + + @Override + public void streamingFailed(final StreamingErrorEvent event) { + if (listenProgressOfUploadedFile) { + file.getStreamVariable().streamingFailed( + new ReceivingEventWrapper(event)); + } + } + + @Override + public boolean isInterrupted() { + return file.getStreamVariable().isInterrupted(); + } + + /* + * With XHR2 file posts we can't provide as much information from the + * terminal as with multipart request. This helper class wraps the + * terminal event and provides the lacking information from the + * Html5File. + */ + class ReceivingEventWrapper implements StreamingErrorEvent, + StreamingEndEvent, StreamingStartEvent, StreamingProgressEvent { + + private StreamingEvent wrappedEvent; + + ReceivingEventWrapper(StreamingEvent e) { + wrappedEvent = e; + } + + @Override + public String getMimeType() { + return file.getType(); + } + + @Override + public String getFileName() { + return file.getFileName(); + } + + @Override + public long getContentLength() { + return file.getFileSize(); + } + + public StreamVariable getReceiver() { + return ProxyReceiver.this; + } + + @Override + public Exception getException() { + if (wrappedEvent instanceof StreamingErrorEvent) { + return ((StreamingErrorEvent) wrappedEvent).getException(); + } + return null; + } + + @Override + public long getBytesReceived() { + return wrappedEvent.getBytesReceived(); + } + + /** + * Calling this method has no effect. DD files are receive only once + * anyway. + */ + @Override + public void disposeStreamVariable() { + + } + } + + } + +} diff --git a/server/src/com/vaadin/ui/Embedded.java b/server/src/com/vaadin/ui/Embedded.java new file mode 100644 index 0000000000..6088c5aa66 --- /dev/null +++ b/server/src/com/vaadin/ui/Embedded.java @@ -0,0 +1,531 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +import com.vaadin.event.MouseEvents.ClickEvent; +import com.vaadin.event.MouseEvents.ClickListener; +import com.vaadin.shared.MouseEventDetails; +import com.vaadin.shared.ui.embedded.EmbeddedServerRpc; +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.PaintTarget; +import com.vaadin.terminal.Resource; +import com.vaadin.terminal.Vaadin6Component; +import com.vaadin.terminal.gwt.client.ui.ClickEventHandler; +import com.vaadin.terminal.gwt.client.ui.embedded.EmbeddedConnector; + +/** + * Component for embedding external objects. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +@SuppressWarnings("serial") +public class Embedded extends AbstractComponent implements Vaadin6Component { + + /** + * General object type. + */ + public static final int TYPE_OBJECT = 0; + + /** + * Image types. + */ + public static final int TYPE_IMAGE = 1; + + /** + * Browser ("iframe") type. + */ + public static final int TYPE_BROWSER = 2; + + /** + * Type of the object. + */ + private int type = TYPE_OBJECT; + + /** + * Source of the embedded object. + */ + private Resource source = null; + + /** + * Generic object attributes. + */ + private String mimeType = null; + + private String standby = null; + + /** + * Hash of object parameters. + */ + private final Map<String, String> parameters = new HashMap<String, String>(); + + /** + * Applet or other client side runnable properties. + */ + private String codebase = null; + + private String codetype = null; + + private String classId = null; + + private String archive = null; + + private String altText; + + private EmbeddedServerRpc rpc = new EmbeddedServerRpc() { + @Override + public void click(MouseEventDetails mouseDetails) { + fireEvent(new ClickEvent(Embedded.this, mouseDetails)); + } + }; + + /** + * Creates a new empty Embedded object. + */ + public Embedded() { + registerRpc(rpc); + } + + /** + * Creates a new empty Embedded object with caption. + * + * @param caption + */ + public Embedded(String caption) { + this(); + setCaption(caption); + } + + /** + * Creates a new Embedded object whose contents is loaded from given + * resource. The dimensions are assumed if possible. The type is guessed + * from resource. + * + * @param caption + * @param source + * the Source of the embedded object. + */ + public Embedded(String caption, Resource source) { + this(caption); + setSource(source); + } + + /** + * Invoked when the component state should be painted. + */ + @Override + public void paintContent(PaintTarget target) throws PaintException { + + switch (type) { + case TYPE_IMAGE: + target.addAttribute("type", "image"); + break; + case TYPE_BROWSER: + target.addAttribute("type", "browser"); + break; + default: + break; + } + + if (getSource() != null) { + target.addAttribute("src", getSource()); + } + + if (mimeType != null && !"".equals(mimeType)) { + target.addAttribute("mimetype", mimeType); + } + if (classId != null && !"".equals(classId)) { + target.addAttribute("classid", classId); + } + if (codebase != null && !"".equals(codebase)) { + target.addAttribute("codebase", codebase); + } + if (codetype != null && !"".equals(codetype)) { + target.addAttribute("codetype", codetype); + } + if (standby != null && !"".equals(standby)) { + target.addAttribute("standby", standby); + } + if (archive != null && !"".equals(archive)) { + target.addAttribute("archive", archive); + } + if (altText != null && !"".equals(altText)) { + target.addAttribute(EmbeddedConnector.ALTERNATE_TEXT, altText); + } + + // Params + for (final Iterator<String> i = getParameterNames(); i.hasNext();) { + target.startTag("embeddedparam"); + final String key = i.next(); + target.addAttribute("name", key); + target.addAttribute("value", getParameter(key)); + target.endTag("embeddedparam"); + } + } + + /** + * Sets this component's "alt-text", that is, an alternate text that can be + * presented instead of this component's normal content, for accessibility + * purposes. Does not work when {@link #setType(int)} has been called with + * {@link #TYPE_BROWSER}. + * + * @param altText + * A short, human-readable description of this component's + * content. + * @since 6.8 + */ + public void setAlternateText(String altText) { + if (altText != this.altText + || (altText != null && !altText.equals(this.altText))) { + this.altText = altText; + requestRepaint(); + } + } + + /** + * Gets this component's "alt-text". + * + * @see #setAlternateText(String) + */ + public String getAlternateText() { + return altText; + } + + /** + * Sets an object parameter. Parameters are optional information, and they + * are passed to the instantiated object. Parameters are are stored as name + * value pairs. This overrides the previous value assigned to this + * parameter. + * + * @param name + * the name of the parameter. + * @param value + * the value of the parameter. + */ + public void setParameter(String name, String value) { + parameters.put(name, value); + requestRepaint(); + } + + /** + * Gets the value of an object parameter. Parameters are optional + * information, and they are passed to the instantiated object. Parameters + * are are stored as name value pairs. + * + * @return the Value of parameter or null if not found. + */ + public String getParameter(String name) { + return parameters.get(name); + } + + /** + * Removes an object parameter from the list. + * + * @param name + * the name of the parameter to remove. + */ + public void removeParameter(String name) { + parameters.remove(name); + requestRepaint(); + } + + /** + * Gets the embedded object parameter names. + * + * @return the Iterator of parameters names. + */ + public Iterator<String> getParameterNames() { + return parameters.keySet().iterator(); + } + + /** + * This attribute specifies the base path used to resolve relative URIs + * specified by the classid, data, and archive attributes. When absent, its + * default value is the base URI of the current document. + * + * @return the code base. + */ + public String getCodebase() { + return codebase; + } + + /** + * Gets the MIME-Type of the code. + * + * @return the MIME-Type of the code. + */ + public String getCodetype() { + return codetype; + } + + /** + * Gets the MIME-Type of the object. + * + * @return the MIME-Type of the object. + */ + public String getMimeType() { + return mimeType; + } + + /** + * This attribute specifies a message that a user agent may render while + * loading the object's implementation and data. + * + * @return The text displayed when loading + */ + public String getStandby() { + return standby; + } + + /** + * This attribute specifies the base path used to resolve relative URIs + * specified by the classid, data, and archive attributes. When absent, its + * default value is the base URI of the current document. + * + * @param codebase + * The base path + */ + public void setCodebase(String codebase) { + if (codebase != this.codebase + || (codebase != null && !codebase.equals(this.codebase))) { + this.codebase = codebase; + requestRepaint(); + } + } + + /** + * This attribute specifies the content type of data expected when + * downloading the object specified by classid. This attribute is optional + * but recommended when classid is specified since it allows the user agent + * to avoid loading information for unsupported content types. When absent, + * it defaults to the value of the type attribute. + * + * @param codetype + * the codetype to set. + */ + public void setCodetype(String codetype) { + if (codetype != this.codetype + || (codetype != null && !codetype.equals(this.codetype))) { + this.codetype = codetype; + requestRepaint(); + } + } + + /** + * Sets the mimeType, the MIME-Type of the object. + * + * @param mimeType + * the mimeType to set. + */ + public void setMimeType(String mimeType) { + if (mimeType != this.mimeType + || (mimeType != null && !mimeType.equals(this.mimeType))) { + this.mimeType = mimeType; + if ("application/x-shockwave-flash".equals(mimeType)) { + /* + * Automatically add wmode transparent as we use lots of + * floating layers in Vaadin. If developers need better flash + * performance, they can override this value programmatically + * back to "window" (the defautl). + */ + if (getParameter("wmode") == null) { + setParameter("wmode", "transparent"); + } + } + requestRepaint(); + } + } + + /** + * This attribute specifies a message that a user agent may render while + * loading the object's implementation and data. + * + * @param standby + * The text to display while loading + */ + public void setStandby(String standby) { + if (standby != this.standby + || (standby != null && !standby.equals(this.standby))) { + this.standby = standby; + requestRepaint(); + } + } + + /** + * This attribute may be used to specify the location of an object's + * implementation via a URI. + * + * @return the classid. + */ + public String getClassId() { + return classId; + } + + /** + * This attribute may be used to specify the location of an object's + * implementation via a URI. + * + * @param classId + * the classId to set. + */ + public void setClassId(String classId) { + if (classId != this.classId + || (classId != null && !classId.equals(this.classId))) { + this.classId = classId; + requestRepaint(); + } + } + + /** + * Gets the resource contained in the embedded object. + * + * @return the Resource + */ + public Resource getSource() { + return source; + } + + /** + * Gets the type of the embedded object. + * <p> + * This can be one of the following: + * <ul> + * <li>TYPE_OBJECT <i>(This is the default)</i> + * <li>TYPE_IMAGE + * </ul> + * </p> + * + * @return the type. + */ + public int getType() { + return type; + } + + /** + * Sets the object source resource. The dimensions are assumed if possible. + * The type is guessed from resource. + * + * @param source + * the source to set. + */ + public void setSource(Resource source) { + if (source != null && !source.equals(this.source)) { + this.source = source; + final String mt = source.getMIMEType(); + + if (mimeType == null) { + mimeType = mt; + } + + if (mt.equals("image/svg+xml")) { + type = TYPE_OBJECT; + } else if ((mt.substring(0, mt.indexOf("/")) + .equalsIgnoreCase("image"))) { + type = TYPE_IMAGE; + } else { + // Keep previous type + } + requestRepaint(); + } + } + + /** + * Sets the object type. + * <p> + * This can be one of the following: + * <ul> + * <li>TYPE_OBJECT <i>(This is the default)</i> + * <li>TYPE_IMAGE + * <li>TYPE_BROWSER + * </ul> + * </p> + * + * @param type + * the type to set. + */ + public void setType(int type) { + if (type != TYPE_OBJECT && type != TYPE_IMAGE && type != TYPE_BROWSER) { + throw new IllegalArgumentException("Unsupported type"); + } + if (type != this.type) { + this.type = type; + requestRepaint(); + } + } + + /** + * This attribute may be used to specify a space-separated list of URIs for + * archives containing resources relevant to the object, which may include + * the resources specified by the classid and data attributes. Preloading + * archives will generally result in reduced load times for objects. + * Archives specified as relative URIs should be interpreted relative to the + * codebase attribute. + * + * @return Space-separated list of URIs with resources relevant to the + * object + */ + public String getArchive() { + return archive; + } + + /** + * This attribute may be used to specify a space-separated list of URIs for + * archives containing resources relevant to the object, which may include + * the resources specified by the classid and data attributes. Preloading + * archives will generally result in reduced load times for objects. + * Archives specified as relative URIs should be interpreted relative to the + * codebase attribute. + * + * @param archive + * Space-separated list of URIs with resources relevant to the + * object + */ + public void setArchive(String archive) { + if (archive != this.archive + || (archive != null && !archive.equals(this.archive))) { + this.archive = archive; + requestRepaint(); + } + } + + /** + * Add a click listener to the component. The listener is called whenever + * the user clicks inside the component. Depending on the content the event + * may be blocked and in that case no event is fired. + * + * Use {@link #removeListener(ClickListener)} to remove the listener. + * + * @param listener + * The listener to add + */ + public void addListener(ClickListener listener) { + addListener(ClickEventHandler.CLICK_EVENT_IDENTIFIER, ClickEvent.class, + listener, ClickListener.clickMethod); + } + + /** + * Remove a click listener from the component. The listener should earlier + * have been added using {@link #addListener(ClickListener)}. + * + * @param listener + * The listener to remove + */ + public void removeListener(ClickListener listener) { + removeListener(ClickEventHandler.CLICK_EVENT_IDENTIFIER, + ClickEvent.class, listener); + } + + @Override + public void changeVariables(Object source, Map<String, Object> variables) { + // TODO Remove once Vaadin6Component is no longer implemented + } + +} diff --git a/server/src/com/vaadin/ui/Field.java b/server/src/com/vaadin/ui/Field.java new file mode 100644 index 0000000000..6dc40d192f --- /dev/null +++ b/server/src/com/vaadin/ui/Field.java @@ -0,0 +1,97 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import com.vaadin.data.BufferedValidatable; +import com.vaadin.data.Property; +import com.vaadin.ui.Component.Focusable; + +/** + * TODO document + * + * @author Vaadin Ltd. + * + * @param T + * the type of values in the field, which might not be the same type + * as that of the data source if converters are used + * + * @author IT Mill Ltd. + */ +public interface Field<T> extends Component, BufferedValidatable, Property<T>, + Property.ValueChangeNotifier, Property.ValueChangeListener, + Property.Editor, Focusable { + + /** + * Is this field required. + * + * Required fields must filled by the user. + * + * @return <code>true</code> if the field is required,otherwise + * <code>false</code>. + * @since 3.1 + */ + public boolean isRequired(); + + /** + * Sets the field required. Required fields must filled by the user. + * + * @param required + * Is the field required. + * @since 3.1 + */ + public void setRequired(boolean required); + + /** + * Sets the error message to be displayed if a required field is empty. + * + * @param requiredMessage + * Error message. + * @since 5.2.6 + */ + public void setRequiredError(String requiredMessage); + + /** + * Gets the error message that is to be displayed if a required field is + * empty. + * + * @return Error message. + * @since 5.2.6 + */ + public String getRequiredError(); + + /** + * An <code>Event</code> object specifying the Field whose value has been + * changed. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ + @SuppressWarnings("serial") + public static class ValueChangeEvent extends Component.Event implements + Property.ValueChangeEvent { + + /** + * Constructs a new event object with the specified source field object. + * + * @param source + * the field that caused the event. + */ + public ValueChangeEvent(Field source) { + super(source); + } + + /** + * Gets the Property which triggered the event. + * + * @return the Source Property of the event. + */ + @Override + public Property getProperty() { + return (Property) getSource(); + } + } +} diff --git a/server/src/com/vaadin/ui/Form.java b/server/src/com/vaadin/ui/Form.java new file mode 100644 index 0000000000..fbc4d5a8e6 --- /dev/null +++ b/server/src/com/vaadin/ui/Form.java @@ -0,0 +1,1420 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.io.Serializable; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.Map; + +import com.vaadin.data.Buffered; +import com.vaadin.data.Item; +import com.vaadin.data.Property; +import com.vaadin.data.Validatable; +import com.vaadin.data.Validator; +import com.vaadin.data.Validator.InvalidValueException; +import com.vaadin.data.fieldgroup.FieldGroup; +import com.vaadin.data.util.BeanItem; +import com.vaadin.event.Action; +import com.vaadin.event.Action.Handler; +import com.vaadin.event.Action.ShortcutNotifier; +import com.vaadin.event.ActionManager; +import com.vaadin.shared.ui.form.FormState; +import com.vaadin.terminal.AbstractErrorMessage; +import com.vaadin.terminal.CompositeErrorMessage; +import com.vaadin.terminal.ErrorMessage; +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.PaintTarget; +import com.vaadin.terminal.UserError; +import com.vaadin.terminal.Vaadin6Component; + +/** + * Form component provides easy way of creating and managing sets fields. + * + * <p> + * <code>Form</code> is a container for fields implementing {@link Field} + * interface. It provides support for any layouts and provides buffering + * interface for easy connection of commit and discard buttons. All the form + * fields can be customized by adding validators, setting captions and icons, + * setting immediateness, etc. Also direct mechanism for replacing existing + * fields with selections is given. + * </p> + * + * <p> + * <code>Form</code> provides customizable editor for classes implementing + * {@link com.vaadin.data.Item} interface. Also the form itself implements this + * interface for easier connectivity to other items. To use the form as editor + * for an item, just connect the item to form with + * {@link Form#setItemDataSource(Item)}. If only a part of the item needs to be + * edited, {@link Form#setItemDataSource(Item,Collection)} can be used instead. + * After the item has been connected to the form, the automatically created + * fields can be customized and new fields can be added. If you need to connect + * a class that does not implement {@link com.vaadin.data.Item} interface, most + * properties of any class following bean pattern, can be accessed trough + * {@link com.vaadin.data.util.BeanItem}. + * </p> + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + * @deprecated Use {@link FieldGroup} instead of {@link Form} for more + * flexibility. + */ +@Deprecated +public class Form extends AbstractField<Object> implements Item.Editor, + Buffered, Item, Validatable, Action.Notifier, HasComponents, + Vaadin6Component { + + private Object propertyValue; + + /** + * Item connected to this form as datasource. + */ + private Item itemDatasource; + + /** + * Ordered list of property ids in this editor. + */ + private final LinkedList<Object> propertyIds = new LinkedList<Object>(); + + /** + * Current buffered source exception. + */ + private Buffered.SourceException currentBufferedSourceException = null; + + /** + * Is the form in write trough mode. + */ + private boolean writeThrough = true; + + /** + * Is the form in read trough mode. + */ + private boolean readThrough = true; + + /** + * Mapping from propertyName to corresponding field. + */ + private final HashMap<Object, Field<?>> fields = new HashMap<Object, Field<?>>(); + + /** + * Form may act as an Item, its own properties are stored here. + */ + private final HashMap<Object, Property<?>> ownProperties = new HashMap<Object, Property<?>>(); + + /** + * Field factory for this form. + */ + private FormFieldFactory fieldFactory; + + /** + * Visible item properties. + */ + private Collection<?> visibleItemProperties; + + /** + * Form needs to repaint itself if child fields value changes due possible + * change in form validity. + * + * TODO introduce ValidityChangeEvent (#6239) and start using it instead. + * See e.g. DateField#notifyFormOfValidityChange(). + */ + private final ValueChangeListener fieldValueChangeListener = new ValueChangeListener() { + @Override + public void valueChange(com.vaadin.data.Property.ValueChangeEvent event) { + requestRepaint(); + } + }; + + /** + * If this is true, commit implicitly calls setValidationVisible(true). + */ + private boolean validationVisibleOnCommit = true; + + // special handling for gridlayout; remember initial cursor pos + private int gridlayoutCursorX = -1; + private int gridlayoutCursorY = -1; + + /** + * Keeps track of the Actions added to this component, and manages the + * painting and handling as well. Note that the extended AbstractField is a + * {@link ShortcutNotifier} and has a actionManager that delegates actions + * to the containing window. This one does not delegate. + */ + private ActionManager ownActionManager = new ActionManager(this); + + /** + * Constructs a new form with default layout. + * + * <p> + * By default the form uses {@link FormLayout}. + * </p> + */ + public Form() { + this(null); + setValidationVisible(false); + } + + /** + * Constructs a new form with given {@link Layout}. + * + * @param formLayout + * the layout of the form. + */ + public Form(Layout formLayout) { + this(formLayout, DefaultFieldFactory.get()); + } + + /** + * Constructs a new form with given {@link Layout} and + * {@link FormFieldFactory}. + * + * @param formLayout + * the layout of the form. + * @param fieldFactory + * the FieldFactory of the form. + */ + public Form(Layout formLayout, FormFieldFactory fieldFactory) { + super(); + setLayout(formLayout); + setFooter(null); + setFormFieldFactory(fieldFactory); + setValidationVisible(false); + setWidth(100, UNITS_PERCENTAGE); + } + + @Override + public FormState getState() { + return (FormState) super.getState(); + } + + /* Documented in interface */ + @Override + public void paintContent(PaintTarget target) throws PaintException { + if (ownActionManager != null) { + ownActionManager.paintActions(null, target); + } + } + + @Override + public void changeVariables(Object source, Map<String, Object> variables) { + // Actions + if (ownActionManager != null) { + ownActionManager.handleActions(variables, this); + } + } + + /** + * The error message of a Form is the error of the first field with a + * non-empty error. + * + * Empty error messages of the contained fields are skipped, because an + * empty error indicator would be confusing to the user, especially if there + * are errors that have something to display. This is also the reason why + * the calculation of the error message is separate from validation, because + * validation fails also on empty errors. + */ + @Override + public ErrorMessage getErrorMessage() { + + // Reimplement the checking of validation error by using + // getErrorMessage() recursively instead of validate(). + ErrorMessage validationError = null; + if (isValidationVisible()) { + for (final Iterator<Object> i = propertyIds.iterator(); i.hasNext();) { + Object f = fields.get(i.next()); + if (f instanceof AbstractComponent) { + AbstractComponent field = (AbstractComponent) f; + + validationError = field.getErrorMessage(); + if (validationError != null) { + // Show caption as error for fields with empty errors + if ("".equals(validationError.toString())) { + validationError = new UserError(field.getCaption()); + } + break; + } else if (f instanceof Field && !((Field<?>) f).isValid()) { + // Something is wrong with the field, but no proper + // error is given. Generate one. + validationError = new UserError(field.getCaption()); + break; + } + } + } + } + + // Return if there are no errors at all + if (getComponentError() == null && validationError == null + && currentBufferedSourceException == null) { + return null; + } + + // Throw combination of the error types + return new CompositeErrorMessage( + new ErrorMessage[] { + getComponentError(), + validationError, + AbstractErrorMessage + .getErrorMessageForException(currentBufferedSourceException) }); + } + + /** + * Controls the making validation visible implicitly on commit. + * + * Having commit() call setValidationVisible(true) implicitly is the default + * behaviour. You can disable the implicit setting by setting this property + * as false. + * + * It is useful, because you usually want to start with the form free of + * errors and only display them after the user clicks Ok. You can disable + * the implicit setting by setting this property as false. + * + * @param makeVisible + * If true (default), validation is made visible when commit() is + * called. If false, the visibility is left as it is. + */ + public void setValidationVisibleOnCommit(boolean makeVisible) { + validationVisibleOnCommit = makeVisible; + } + + /** + * Is validation made automatically visible on commit? + * + * See setValidationVisibleOnCommit(). + * + * @return true if validation is made automatically visible on commit. + */ + public boolean isValidationVisibleOnCommit() { + return validationVisibleOnCommit; + } + + /* + * Commit changes to the data source Don't add a JavaDoc comment here, we + * use the default one from the interface. + */ + @Override + public void commit() throws Buffered.SourceException, InvalidValueException { + + LinkedList<SourceException> problems = null; + + // Only commit on valid state if so requested + if (!isInvalidCommitted() && !isValid()) { + /* + * The values are not ok and we are told not to commit invalid + * values + */ + if (validationVisibleOnCommit) { + setValidationVisible(true); + } + + // Find the first invalid value and throw the exception + validate(); + } + + // Try to commit all + for (final Iterator<Object> i = propertyIds.iterator(); i.hasNext();) { + try { + final Field<?> f = (fields.get(i.next())); + // Commit only non-readonly fields. + if (!f.isReadOnly()) { + f.commit(); + } + } catch (final Buffered.SourceException e) { + if (problems == null) { + problems = new LinkedList<SourceException>(); + } + problems.add(e); + } + } + + // No problems occurred + if (problems == null) { + if (currentBufferedSourceException != null) { + currentBufferedSourceException = null; + requestRepaint(); + } + return; + } + + // Commit problems + final Throwable[] causes = new Throwable[problems.size()]; + int index = 0; + for (final Iterator<SourceException> i = problems.iterator(); i + .hasNext();) { + causes[index++] = i.next(); + } + final Buffered.SourceException e = new Buffered.SourceException(this, + causes); + currentBufferedSourceException = e; + requestRepaint(); + throw e; + } + + /* + * Discards local changes and refresh values from the data source Don't add + * a JavaDoc comment here, we use the default one from the interface. + */ + @Override + public void discard() throws Buffered.SourceException { + + LinkedList<SourceException> problems = null; + + // Try to discard all changes + for (final Iterator<Object> i = propertyIds.iterator(); i.hasNext();) { + try { + (fields.get(i.next())).discard(); + } catch (final Buffered.SourceException e) { + if (problems == null) { + problems = new LinkedList<SourceException>(); + } + problems.add(e); + } + } + + // No problems occurred + if (problems == null) { + if (currentBufferedSourceException != null) { + currentBufferedSourceException = null; + requestRepaint(); + } + return; + } + + // Discards problems occurred + final Throwable[] causes = new Throwable[problems.size()]; + int index = 0; + for (final Iterator<SourceException> i = problems.iterator(); i + .hasNext();) { + causes[index++] = i.next(); + } + final Buffered.SourceException e = new Buffered.SourceException(this, + causes); + currentBufferedSourceException = e; + requestRepaint(); + throw e; + } + + /* + * Is the object modified but not committed? Don't add a JavaDoc comment + * here, we use the default one from the interface. + */ + @Override + public boolean isModified() { + for (final Iterator<Object> i = propertyIds.iterator(); i.hasNext();) { + final Field<?> f = fields.get(i.next()); + if (f != null && f.isModified()) { + return true; + } + + } + return false; + } + + /* + * Is the editor in a read-through mode? Don't add a JavaDoc comment here, + * we use the default one from the interface. + */ + @Override + @Deprecated + public boolean isReadThrough() { + return readThrough; + } + + /* + * Is the editor in a write-through mode? Don't add a JavaDoc comment here, + * we use the default one from the interface. + */ + @Override + @Deprecated + public boolean isWriteThrough() { + return writeThrough; + } + + /* + * Sets the editor's read-through mode to the specified status. Don't add a + * JavaDoc comment here, we use the default one from the interface. + */ + @Override + public void setReadThrough(boolean readThrough) { + if (readThrough != this.readThrough) { + this.readThrough = readThrough; + for (final Iterator<Object> i = propertyIds.iterator(); i.hasNext();) { + (fields.get(i.next())).setReadThrough(readThrough); + } + } + } + + /* + * Sets the editor's read-through mode to the specified status. Don't add a + * JavaDoc comment here, we use the default one from the interface. + */ + @Override + public void setWriteThrough(boolean writeThrough) throws SourceException, + InvalidValueException { + if (writeThrough != this.writeThrough) { + this.writeThrough = writeThrough; + for (final Iterator<Object> i = propertyIds.iterator(); i.hasNext();) { + (fields.get(i.next())).setWriteThrough(writeThrough); + } + } + } + + /** + * Adds a new property to form and create corresponding field. + * + * @see com.vaadin.data.Item#addItemProperty(Object, Property) + */ + @Override + public boolean addItemProperty(Object id, Property property) { + + // Checks inputs + if (id == null || property == null) { + throw new NullPointerException("Id and property must be non-null"); + } + + // Checks that the property id is not reserved + if (propertyIds.contains(id)) { + return false; + } + + propertyIds.add(id); + ownProperties.put(id, property); + + // Gets suitable field + final Field<?> field = fieldFactory.createField(this, id, this); + if (field == null) { + return false; + } + + // Configures the field + bindPropertyToField(id, property, field); + + // Register and attach the created field + addField(id, field); + + return true; + } + + /** + * Registers the field with the form and adds the field to the form layout. + * + * <p> + * The property id must not be already used in the form. + * </p> + * + * <p> + * This field is added to the layout using the + * {@link #attachField(Object, Field)} method. + * </p> + * + * @param propertyId + * the Property id the the field. + * @param field + * the field which should be added to the form. + */ + public void addField(Object propertyId, Field<?> field) { + registerField(propertyId, field); + attachField(propertyId, field); + requestRepaint(); + } + + /** + * Register the field with the form. All registered fields are validated + * when the form is validated and also committed when the form is committed. + * + * <p> + * The property id must not be already used in the form. + * </p> + * + * + * @param propertyId + * the Property id of the field. + * @param field + * the Field that should be registered + */ + private void registerField(Object propertyId, Field<?> field) { + if (propertyId == null || field == null) { + return; + } + + fields.put(propertyId, field); + field.addListener(fieldValueChangeListener); + if (!propertyIds.contains(propertyId)) { + // adding a field directly + propertyIds.addLast(propertyId); + } + + // Update the read and write through status and immediate to match the + // form. + // Should this also include invalidCommitted (#3993)? + field.setReadThrough(readThrough); + field.setWriteThrough(writeThrough); + if (isImmediate() && field instanceof AbstractComponent) { + ((AbstractComponent) field).setImmediate(true); + } + } + + /** + * Adds the field to the form layout. + * <p> + * The field is added to the form layout in the default position (the + * position used by {@link Layout#addComponent(Component)}. If the + * underlying layout is a {@link CustomLayout} the field is added to the + * CustomLayout location given by the string representation of the property + * id using {@link CustomLayout#addComponent(Component, String)}. + * </p> + * + * <p> + * Override this method to control how the fields are added to the layout. + * </p> + * + * @param propertyId + * @param field + */ + protected void attachField(Object propertyId, Field field) { + if (propertyId == null || field == null) { + return; + } + + Layout layout = getLayout(); + if (layout instanceof CustomLayout) { + ((CustomLayout) layout).addComponent(field, propertyId.toString()); + } else { + layout.addComponent(field); + } + + } + + /** + * The property identified by the property id. + * + * <p> + * The property data source of the field specified with property id is + * returned. If there is a (with specified property id) having no data + * source, the field is returned instead of the data source. + * </p> + * + * @see com.vaadin.data.Item#getItemProperty(Object) + */ + @Override + public Property<?> getItemProperty(Object id) { + final Field<?> field = fields.get(id); + if (field == null) { + // field does not exist or it is not (yet) created for this property + return ownProperties.get(id); + } + final Property<?> property = field.getPropertyDataSource(); + + if (property != null) { + return property; + } else { + return field; + } + } + + /** + * Gets the field identified by the propertyid. + * + * @param propertyId + * the id of the property. + */ + public Field<?> getField(Object propertyId) { + return fields.get(propertyId); + } + + /* Documented in interface */ + @Override + public Collection<?> getItemPropertyIds() { + return Collections.unmodifiableCollection(propertyIds); + } + + /** + * Removes the property and corresponding field from the form. + * + * @see com.vaadin.data.Item#removeItemProperty(Object) + */ + @Override + public boolean removeItemProperty(Object id) { + ownProperties.remove(id); + + final Field<?> field = fields.get(id); + + if (field != null) { + propertyIds.remove(id); + fields.remove(id); + detachField(field); + field.removeListener(fieldValueChangeListener); + return true; + } + + return false; + } + + /** + * Called when a form field is detached from a Form. Typically when a new + * Item is assigned to Form via {@link #setItemDataSource(Item)}. + * <p> + * Override this method to control how the fields are removed from the + * layout. + * </p> + * + * @param field + * the field to be detached from the forms layout. + */ + protected void detachField(final Field field) { + Component p = field.getParent(); + if (p instanceof ComponentContainer) { + ((ComponentContainer) p).removeComponent(field); + } + } + + /** + * Removes all properties and fields from the form. + * + * @return the Success of the operation. Removal of all fields succeeded if + * (and only if) the return value is <code>true</code>. + */ + public boolean removeAllProperties() { + final Object[] properties = propertyIds.toArray(); + boolean success = true; + + for (int i = 0; i < properties.length; i++) { + if (!removeItemProperty(properties[i])) { + success = false; + } + } + + return success; + } + + /* Documented in the interface */ + @Override + public Item getItemDataSource() { + return itemDatasource; + } + + /** + * Sets the item datasource for the form. + * + * <p> + * Setting item datasource clears any fields, the form might contain and + * adds all the properties as fields to the form. + * </p> + * + * @see com.vaadin.data.Item.Viewer#setItemDataSource(Item) + */ + @Override + public void setItemDataSource(Item newDataSource) { + setItemDataSource(newDataSource, + newDataSource != null ? newDataSource.getItemPropertyIds() + : null); + } + + /** + * Set the item datasource for the form, but limit the form contents to + * specified properties of the item. + * + * <p> + * Setting item datasource clears any fields, the form might contain and + * adds the specified the properties as fields to the form, in the specified + * order. + * </p> + * + * @see com.vaadin.data.Item.Viewer#setItemDataSource(Item) + */ + public void setItemDataSource(Item newDataSource, Collection<?> propertyIds) { + + if (getLayout() instanceof GridLayout) { + GridLayout gl = (GridLayout) getLayout(); + if (gridlayoutCursorX == -1) { + // first setItemDataSource, remember initial cursor + gridlayoutCursorX = gl.getCursorX(); + gridlayoutCursorY = gl.getCursorY(); + } else { + // restore initial cursor + gl.setCursorX(gridlayoutCursorX); + gl.setCursorY(gridlayoutCursorY); + } + } + + // Removes all fields first from the form + removeAllProperties(); + + // Sets the datasource + itemDatasource = newDataSource; + + // If the new datasource is null, just set null datasource + if (itemDatasource == null) { + requestRepaint(); + return; + } + + // Adds all the properties to this form + for (final Iterator<?> i = propertyIds.iterator(); i.hasNext();) { + final Object id = i.next(); + final Property<?> property = itemDatasource.getItemProperty(id); + if (id != null && property != null) { + final Field<?> f = fieldFactory.createField(itemDatasource, id, + this); + if (f != null) { + bindPropertyToField(id, property, f); + addField(id, f); + } + } + } + } + + /** + * Binds an item property to a field. The default behavior is to bind + * property straight to Field. If Property.Viewer type property (e.g. + * PropertyFormatter) is already set for field, the property is bound to + * that Property.Viewer. + * + * @param propertyId + * @param property + * @param field + * @since 6.7.3 + */ + protected void bindPropertyToField(final Object propertyId, + final Property property, final 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); + } + } + + /** + * Gets the layout of the form. + * + * <p> + * By default form uses <code>OrderedLayout</code> with <code>form</code> + * -style. + * </p> + * + * @return the Layout of the form. + */ + public Layout getLayout() { + return (Layout) getState().getLayout(); + } + + /** + * Sets the layout of the form. + * + * <p> + * If set to null then Form uses a FormLayout by default. + * </p> + * + * @param layout + * the layout of the form. + */ + public void setLayout(Layout layout) { + + // Use orderedlayout by default + if (layout == null) { + layout = new FormLayout(); + } + + // reset cursor memory + gridlayoutCursorX = -1; + gridlayoutCursorY = -1; + + // Move fields from previous layout + if (getLayout() != null) { + final Object[] properties = propertyIds.toArray(); + for (int i = 0; i < properties.length; i++) { + Field<?> f = getField(properties[i]); + detachField(f); + if (layout instanceof CustomLayout) { + ((CustomLayout) layout).addComponent(f, + properties[i].toString()); + } else { + layout.addComponent(f); + } + } + + getLayout().setParent(null); + } + + // Replace the previous layout + layout.setParent(this); + getState().setLayout(layout); + + // Hierarchy has changed so we need to repaint (this could be a + // hierarchy repaint only) + requestRepaint(); + } + + /** + * Sets the form field to be selectable from static list of changes. + * + * <p> + * The list values and descriptions are given as array. The value-array must + * contain the current value of the field and the lengths of the arrays must + * match. Null values are not supported. + * </p> + * + * Note: since Vaadin 7.0, returns an {@link AbstractSelect} instead of a + * {@link Select}. + * + * @param propertyId + * the id of the property. + * @param values + * @param descriptions + * @return the select property generated + */ + public AbstractSelect replaceWithSelect(Object propertyId, Object[] values, + Object[] descriptions) { + + // Checks the parameters + if (propertyId == null || values == null || descriptions == null) { + throw new NullPointerException("All parameters must be non-null"); + } + if (values.length != descriptions.length) { + throw new IllegalArgumentException( + "Value and description list are of different size"); + } + + // Gets the old field + final Field<?> oldField = fields.get(propertyId); + if (oldField == null) { + throw new IllegalArgumentException("Field with given propertyid '" + + propertyId.toString() + "' can not be found."); + } + final Object value = oldField.getPropertyDataSource() == null ? oldField + .getValue() : oldField.getPropertyDataSource().getValue(); + + // Checks that the value exists and check if the select should + // be forced in multiselect mode + boolean found = false; + boolean isMultiselect = false; + for (int i = 0; i < values.length && !found; i++) { + if (values[i] == value + || (value != null && value.equals(values[i]))) { + found = true; + } + } + if (value != null && !found) { + if (value instanceof Collection) { + for (final Iterator<?> it = ((Collection<?>) value).iterator(); it + .hasNext();) { + final Object val = it.next(); + found = false; + for (int i = 0; i < values.length && !found; i++) { + if (values[i] == val + || (val != null && val.equals(values[i]))) { + found = true; + } + } + if (!found) { + throw new IllegalArgumentException( + "Currently selected value '" + val + + "' of property '" + + propertyId.toString() + + "' was not found"); + } + } + isMultiselect = true; + } else { + throw new IllegalArgumentException("Current value '" + value + + "' of property '" + propertyId.toString() + + "' was not found"); + } + } + + // Creates the new field matching to old field parameters + final AbstractSelect newField = isMultiselect ? new ListSelect() + : new Select(); + newField.setCaption(oldField.getCaption()); + newField.setReadOnly(oldField.isReadOnly()); + newField.setReadThrough(oldField.isReadThrough()); + newField.setWriteThrough(oldField.isWriteThrough()); + + // Creates the options list + newField.addContainerProperty("desc", String.class, ""); + newField.setItemCaptionPropertyId("desc"); + for (int i = 0; i < values.length; i++) { + Object id = values[i]; + final Item item; + if (id == null) { + id = newField.addItem(); + item = newField.getItem(id); + newField.setNullSelectionItemId(id); + } else { + item = newField.addItem(id); + } + + if (item != null) { + item.getItemProperty("desc").setValue( + descriptions[i].toString()); + } + } + + // Sets the property data source + final Property<?> property = oldField.getPropertyDataSource(); + oldField.setPropertyDataSource(null); + newField.setPropertyDataSource(property); + + // Replaces the old field with new one + getLayout().replaceComponent(oldField, newField); + fields.put(propertyId, newField); + newField.addListener(fieldValueChangeListener); + oldField.removeListener(fieldValueChangeListener); + + return newField; + } + + /** + * Checks the validity of the Form and all of its fields. + * + * @see com.vaadin.data.Validatable#validate() + */ + @Override + public void validate() throws InvalidValueException { + super.validate(); + for (final Iterator<Object> i = propertyIds.iterator(); i.hasNext();) { + (fields.get(i.next())).validate(); + } + } + + /** + * Checks the validabtable object accept invalid values. + * + * @see com.vaadin.data.Validatable#isInvalidAllowed() + */ + @Override + public boolean isInvalidAllowed() { + return true; + } + + /** + * Should the validabtable object accept invalid values. + * + * @see com.vaadin.data.Validatable#setInvalidAllowed(boolean) + */ + @Override + public void setInvalidAllowed(boolean invalidValueAllowed) + throws UnsupportedOperationException { + throw new UnsupportedOperationException(); + } + + /** + * Sets the component's to read-only mode to the specified state. + * + * @see com.vaadin.ui.Component#setReadOnly(boolean) + */ + @Override + public void setReadOnly(boolean readOnly) { + super.setReadOnly(readOnly); + for (final Iterator<?> i = propertyIds.iterator(); i.hasNext();) { + (fields.get(i.next())).setReadOnly(readOnly); + } + } + + /** + * Sets the field factory used by this Form to genarate Fields for + * properties. + * + * {@link FormFieldFactory} is used to create fields for form properties. + * {@link DefaultFieldFactory} is used by default. + * + * @param fieldFactory + * the new factory used to create the fields. + * @see Field + * @see FormFieldFactory + */ + public void setFormFieldFactory(FormFieldFactory fieldFactory) { + this.fieldFactory = fieldFactory; + } + + /** + * Get the field factory of the form. + * + * @return the FormFieldFactory Factory used to create the fields. + */ + public FormFieldFactory getFormFieldFactory() { + return fieldFactory; + } + + /** + * Gets the field type. + * + * @see com.vaadin.ui.AbstractField#getType() + */ + @Override + public Class<?> getType() { + if (getPropertyDataSource() != null) { + return getPropertyDataSource().getType(); + } + return Object.class; + } + + /** + * Sets the internal value. + * + * This is relevant when the Form is used as Field. + * + * @see com.vaadin.ui.AbstractField#setInternalValue(java.lang.Object) + */ + @Override + protected void setInternalValue(Object newValue) { + // Stores the old value + final Object oldValue = propertyValue; + + // Sets the current Value + super.setInternalValue(newValue); + propertyValue = newValue; + + // Ignores form updating if data object has not changed. + if (oldValue != newValue) { + setFormDataSource(newValue, getVisibleItemProperties()); + } + } + + /** + * Gets the first focusable field in form. If there are enabled, + * non-read-only fields, the first one of them is returned. Otherwise, the + * field for the first property (or null if none) is returned. + * + * @return the Field. + */ + private Field<?> getFirstFocusableField() { + if (getItemPropertyIds() != null) { + for (Object id : getItemPropertyIds()) { + if (id != null) { + Field<?> field = getField(id); + if (field.isEnabled() && !field.isReadOnly()) { + return field; + } + } + } + // fallback: first field if none of the fields is enabled and + // writable + Object id = getItemPropertyIds().iterator().next(); + if (id != null) { + return getField(id); + } + } + return null; + } + + /** + * Updates the internal form datasource. + * + * Method setFormDataSource. + * + * @param data + * @param properties + */ + protected void setFormDataSource(Object data, Collection<?> properties) { + + // If data is an item use it. + Item item = null; + if (data instanceof Item) { + item = (Item) data; + } else if (data != null) { + item = new BeanItem<Object>(data); + } + + // Sets the datasource to form + if (item != null && properties != null) { + // Shows only given properties + this.setItemDataSource(item, properties); + } else { + // Shows all properties + this.setItemDataSource(item); + } + } + + /** + * Returns the visibleProperties. + * + * @return the Collection of visible Item properites. + */ + public Collection<?> getVisibleItemProperties() { + return visibleItemProperties; + } + + /** + * Sets the visibleProperties. + * + * @param visibleProperties + * the visibleProperties to set. + */ + public void setVisibleItemProperties(Collection<?> visibleProperties) { + visibleItemProperties = visibleProperties; + Object value = getValue(); + if (value == null) { + value = itemDatasource; + } + setFormDataSource(value, getVisibleItemProperties()); + } + + /** + * Sets the visibleProperties. + * + * @param visibleProperties + * the visibleProperties to set. + */ + public void setVisibleItemProperties(Object[] visibleProperties) { + LinkedList<Object> v = new LinkedList<Object>(); + for (int i = 0; i < visibleProperties.length; i++) { + v.add(visibleProperties[i]); + } + setVisibleItemProperties(v); + } + + /** + * Focuses the first field in the form. + * + * @see com.vaadin.ui.Component.Focusable#focus() + */ + @Override + public void focus() { + final Field<?> f = getFirstFocusableField(); + if (f != null) { + f.focus(); + } + } + + /** + * Sets the Tabulator index of this Focusable component. + * + * @see com.vaadin.ui.Component.Focusable#setTabIndex(int) + */ + @Override + public void setTabIndex(int tabIndex) { + super.setTabIndex(tabIndex); + for (final Iterator<?> i = getItemPropertyIds().iterator(); i.hasNext();) { + (getField(i.next())).setTabIndex(tabIndex); + } + } + + /** + * Setting the form to be immediate also sets all the fields of the form to + * the same state. + */ + @Override + public void setImmediate(boolean immediate) { + super.setImmediate(immediate); + for (Iterator<Field<?>> i = fields.values().iterator(); i.hasNext();) { + Field<?> f = i.next(); + if (f instanceof AbstractComponent) { + ((AbstractComponent) f).setImmediate(immediate); + } + } + } + + /** Form is empty if all of its fields are empty. */ + @Override + protected boolean isEmpty() { + + for (Iterator<Field<?>> i = fields.values().iterator(); i.hasNext();) { + Field<?> f = i.next(); + if (f instanceof AbstractField) { + if (!((AbstractField<?>) f).isEmpty()) { + return false; + } + } + } + + return true; + } + + /** + * Adding validators directly to form is not supported. + * + * Add the validators to form fields instead. + */ + @Override + public void addValidator(Validator validator) { + throw new UnsupportedOperationException(); + } + + /** + * Returns a layout that is rendered below normal form contents. This area + * can be used for example to include buttons related to form contents. + * + * @return layout rendered below normal form contents. + */ + public Layout getFooter() { + return (Layout) getState().getFooter(); + } + + /** + * Sets the layout that is rendered below normal form contents. Setting this + * to null will cause an empty HorizontalLayout to be rendered in the + * footer. + * + * @param footer + * the new footer layout + */ + public void setFooter(Layout footer) { + if (getFooter() != null) { + getFooter().setParent(null); + } + if (footer == null) { + footer = new HorizontalLayout(); + } + + getState().setFooter(footer); + footer.setParent(this); + + // Hierarchy has changed so we need to repaint (this could be a + // hierarchy repaint only) + requestRepaint(); + + } + + @Override + public void setEnabled(boolean enabled) { + super.setEnabled(enabled); + if (getParent() != null && !getParent().isEnabled()) { + // some ancestor still disabled, don't update children + return; + } else { + getLayout().requestRepaintAll(); + } + } + + /* + * ACTIONS + */ + + /** + * Gets the {@link ActionManager} responsible for handling {@link Action}s + * added to this Form.<br/> + * Note that Form has another ActionManager inherited from + * {@link AbstractField}. The ownActionManager handles Actions attached to + * this Form specifically, while the ActionManager in AbstractField + * delegates to the containing Window (i.e global Actions). + * + * @return + */ + protected ActionManager getOwnActionManager() { + if (ownActionManager == null) { + ownActionManager = new ActionManager(this); + } + return ownActionManager; + } + + @Override + public void addActionHandler(Handler actionHandler) { + getOwnActionManager().addActionHandler(actionHandler); + } + + @Override + public void removeActionHandler(Handler actionHandler) { + if (ownActionManager != null) { + ownActionManager.removeActionHandler(actionHandler); + } + } + + /** + * Removes all action handlers + */ + public void removeAllActionHandlers() { + if (ownActionManager != null) { + ownActionManager.removeAllActionHandlers(); + } + } + + @Override + public <T extends Action & com.vaadin.event.Action.Listener> void addAction( + T action) { + getOwnActionManager().addAction(action); + } + + @Override + public <T extends Action & com.vaadin.event.Action.Listener> void removeAction( + T action) { + if (ownActionManager != null) { + ownActionManager.removeAction(action); + } + } + + @Override + public Iterator<Component> iterator() { + return getComponentIterator(); + } + + /** + * Modifiable and Serializable Iterator for the components, used by + * {@link Form#getComponentIterator()}. + */ + private class ComponentIterator implements Iterator<Component>, + Serializable { + + int i = 0; + + @Override + public boolean hasNext() { + if (i < getComponentCount()) { + return true; + } + return false; + } + + @Override + public Component next() { + if (!hasNext()) { + return null; + } + i++; + if (i == 1) { + return getLayout() != null ? getLayout() : getFooter(); + } else if (i == 2) { + return getFooter(); + } + return null; + } + + @Override + public void remove() { + if (i == 1) { + if (getLayout() != null) { + setLayout(null); + i = 0; + } else { + setFooter(null); + } + } else if (i == 2) { + setFooter(null); + } + } + } + + @Override + public Iterator<Component> getComponentIterator() { + return new ComponentIterator(); + } + + public int getComponentCount() { + int count = 0; + if (getLayout() != null) { + count++; + } + if (getFooter() != null) { + count++; + } + + return count; + } + + @Override + public boolean isComponentVisible(Component childComponent) { + return true; + }; +} diff --git a/server/src/com/vaadin/ui/FormFieldFactory.java b/server/src/com/vaadin/ui/FormFieldFactory.java new file mode 100644 index 0000000000..1efa05c5f5 --- /dev/null +++ b/server/src/com/vaadin/ui/FormFieldFactory.java @@ -0,0 +1,41 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.ui; + +import java.io.Serializable; + +import com.vaadin.data.Item; + +/** + * Factory interface for creating new Field-instances based on {@link Item}, + * property id and uiContext (the component responsible for displaying fields). + * Currently this interface is used by {@link Form}, but might later be used by + * some other components for {@link Field} generation. + * + * <p> + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 6.0 + * @see TableFieldFactory + */ +public interface FormFieldFactory extends Serializable { + /** + * Creates a field based on the item, property id and the component (most + * commonly {@link Form}) where the Field will be presented. + * + * @param item + * the item where the property belongs to. + * @param propertyId + * the Id of the property. + * @param uiContext + * the component where the field is presented, most commonly this + * is {@link Form}. uiContext will not necessary be the parent + * component of the field, but the one that is responsible for + * creating it. + * @return Field the field suitable for editing the specified data. + */ + Field<?> createField(Item item, Object propertyId, Component uiContext); +} diff --git a/server/src/com/vaadin/ui/FormLayout.java b/server/src/com/vaadin/ui/FormLayout.java new file mode 100644 index 0000000000..c0be784a7b --- /dev/null +++ b/server/src/com/vaadin/ui/FormLayout.java @@ -0,0 +1,31 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +/** + * FormLayout is used by {@link Form} to layout fields. It may also be used + * separately without {@link Form}. + * + * FormLayout is a close relative to vertical {@link OrderedLayout}, but in + * FormLayout caption is rendered on left side of component. Required and + * validation indicators are between captions and fields. + * + * FormLayout does not currently support some advanced methods from + * OrderedLayout like setExpandRatio and setComponentAlignment. + * + * FormLayout by default has component spacing on. Also margin top and margin + * bottom are by default on. + * + */ +public class FormLayout extends AbstractOrderedLayout { + + public FormLayout() { + super(); + setSpacing(true); + setMargin(true, false, true, false); + setWidth(100, UNITS_PERCENTAGE); + } + +} diff --git a/server/src/com/vaadin/ui/GridLayout.java b/server/src/com/vaadin/ui/GridLayout.java new file mode 100644 index 0000000000..2391a9cd3a --- /dev/null +++ b/server/src/com/vaadin/ui/GridLayout.java @@ -0,0 +1,1415 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.io.Serializable; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.Map; +import java.util.Map.Entry; + +import com.vaadin.event.LayoutEvents.LayoutClickEvent; +import com.vaadin.event.LayoutEvents.LayoutClickListener; +import com.vaadin.event.LayoutEvents.LayoutClickNotifier; +import com.vaadin.shared.Connector; +import com.vaadin.shared.MouseEventDetails; +import com.vaadin.shared.ui.gridlayout.GridLayoutServerRpc; +import com.vaadin.shared.ui.gridlayout.GridLayoutState; +import com.vaadin.terminal.LegacyPaint; +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.PaintTarget; +import com.vaadin.terminal.Vaadin6Component; +import com.vaadin.terminal.gwt.client.ui.LayoutClickEventHandler; + +/** + * A layout where the components are laid out on a grid using cell coordinates. + * + * <p> + * The GridLayout also maintains a cursor for adding components in + * left-to-right, top-to-bottom order. + * </p> + * + * <p> + * Each component in a <code>GridLayout</code> uses a defined + * {@link GridLayout.Area area} (column1,row1,column2,row2) from the grid. The + * components may not overlap with the existing components - if you try to do so + * you will get an {@link OverlapsException}. Adding a component with cursor + * automatically extends the grid by increasing the grid height. + * </p> + * + * <p> + * The grid coordinates, which are specified by a row and column index, always + * start from 0 for the topmost row and the leftmost column. + * </p> + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +@SuppressWarnings("serial") +public class GridLayout extends AbstractLayout implements + Layout.AlignmentHandler, Layout.SpacingHandler, LayoutClickNotifier, + Vaadin6Component { + + private GridLayoutServerRpc rpc = new GridLayoutServerRpc() { + + @Override + public void layoutClick(MouseEventDetails mouseDetails, + Connector clickedConnector) { + fireEvent(LayoutClickEvent.createEvent(GridLayout.this, + mouseDetails, clickedConnector)); + + } + }; + /** + * Cursor X position: this is where the next component with unspecified x,y + * is inserted + */ + private int cursorX = 0; + + /** + * Cursor Y position: this is where the next component with unspecified x,y + * is inserted + */ + private int cursorY = 0; + + /** + * Contains all items that are placed on the grid. These are components with + * grid area definition. + */ + private final LinkedList<Area> areas = new LinkedList<Area>(); + + /** + * Mapping from components to their respective areas. + */ + private final LinkedList<Component> components = new LinkedList<Component>(); + + /** + * Mapping from components to alignments (horizontal + vertical). + */ + private Map<Component, Alignment> componentToAlignment = new HashMap<Component, Alignment>(); + + private static final Alignment ALIGNMENT_DEFAULT = Alignment.TOP_LEFT; + + /** + * Has there been rows inserted or deleted in the middle of the layout since + * the last paint operation. + */ + private boolean structuralChange = false; + + private Map<Integer, Float> columnExpandRatio = new HashMap<Integer, Float>(); + private Map<Integer, Float> rowExpandRatio = new HashMap<Integer, Float>(); + + /** + * Constructor for a grid of given size (number of columns and rows). + * + * The grid may grow or shrink later. Grid grows automatically if you add + * components outside its area. + * + * @param columns + * Number of columns in the grid. + * @param rows + * Number of rows in the grid. + */ + public GridLayout(int columns, int rows) { + setColumns(columns); + setRows(rows); + registerRpc(rpc); + } + + /** + * Constructs an empty (1x1) grid layout that is extended as needed. + */ + public GridLayout() { + this(1, 1); + } + + @Override + public GridLayoutState getState() { + return (GridLayoutState) super.getState(); + } + + /** + * <p> + * Adds a component to the grid in the specified area. The area is defined + * by specifying the upper left corner (column1, row1) and the lower right + * corner (column2, row2) of the area. The coordinates are zero-based. + * </p> + * + * <p> + * If the area overlaps with any of the existing components already present + * in the grid, the operation will fail and an {@link OverlapsException} is + * thrown. + * </p> + * + * @param component + * the component to be added. + * @param column1 + * the column of the upper left corner of the area <code>c</code> + * is supposed to occupy. The leftmost column has index 0. + * @param row1 + * the row of the upper left corner of the area <code>c</code> is + * supposed to occupy. The topmost row has index 0. + * @param column2 + * the column of the lower right corner of the area + * <code>c</code> is supposed to occupy. + * @param row2 + * the row of the lower right corner of the area <code>c</code> + * is supposed to occupy. + * @throws OverlapsException + * if the new component overlaps with any of the components + * already in the grid. + * @throws OutOfBoundsException + * if the cells are outside the grid area. + */ + public void addComponent(Component component, int column1, int row1, + int column2, int row2) throws OverlapsException, + OutOfBoundsException { + + if (component == null) { + throw new NullPointerException("Component must not be null"); + } + + // Checks that the component does not already exist in the container + if (components.contains(component)) { + throw new IllegalArgumentException( + "Component is already in the container"); + } + + // Creates the area + final Area area = new Area(component, column1, row1, column2, row2); + + // Checks the validity of the coordinates + if (column2 < column1 || row2 < row1) { + throw new IllegalArgumentException( + "Illegal coordinates for the component"); + } + if (column1 < 0 || row1 < 0 || column2 >= getColumns() + || row2 >= getRows()) { + throw new OutOfBoundsException(area); + } + + // Checks that newItem does not overlap with existing items + checkExistingOverlaps(area); + + // Inserts the component to right place at the list + // Respect top-down, left-right ordering + // component.setParent(this); + final Iterator<Area> i = areas.iterator(); + int index = 0; + boolean done = false; + while (!done && i.hasNext()) { + final Area existingArea = i.next(); + if ((existingArea.row1 >= row1 && existingArea.column1 > column1) + || existingArea.row1 > row1) { + areas.add(index, area); + components.add(index, component); + done = true; + } + index++; + } + if (!done) { + areas.addLast(area); + components.addLast(component); + } + + // Attempt to add to super + try { + super.addComponent(component); + } catch (IllegalArgumentException e) { + areas.remove(area); + components.remove(component); + throw e; + } + + // update cursor position, if it's within this area; use first position + // outside this area, even if it's occupied + if (cursorX >= column1 && cursorX <= column2 && cursorY >= row1 + && cursorY <= row2) { + // cursor within area + cursorX = column2 + 1; // one right of area + if (cursorX >= getColumns()) { + // overflowed columns + cursorX = 0; // first col + // move one row down, or one row under the area + cursorY = (column1 == 0 ? row2 : row1) + 1; + } else { + cursorY = row1; + } + } + + requestRepaint(); + } + + /** + * Tests if the given area overlaps with any of the items already on the + * grid. + * + * @param area + * the Area to be checked for overlapping. + * @throws OverlapsException + * if <code>area</code> overlaps with any existing area. + */ + private void checkExistingOverlaps(Area area) throws OverlapsException { + for (final Iterator<Area> i = areas.iterator(); i.hasNext();) { + final Area existingArea = i.next(); + if (existingArea.overlaps(area)) { + // Component not added, overlaps with existing component + throw new OverlapsException(existingArea); + } + } + } + + /** + * Adds the component to the grid in cells column1,row1 (NortWest corner of + * the area.) End coordinates (SouthEast corner of the area) are the same as + * column1,row1. The coordinates are zero-based. Component width and height + * is 1. + * + * @param component + * the component to be added. + * @param column + * the column index, starting from 0. + * @param row + * the row index, starting from 0. + * @throws OverlapsException + * if the new component overlaps with any of the components + * already in the grid. + * @throws OutOfBoundsException + * if the cell is outside the grid area. + */ + public void addComponent(Component component, int column, int row) + throws OverlapsException, OutOfBoundsException { + this.addComponent(component, column, row, column, row); + } + + /** + * Forces the next component to be added at the beginning of the next line. + * + * <p> + * Sets the cursor column to 0 and increments the cursor row by one. + * </p> + * + * <p> + * By calling this function you can ensure that no more components are added + * right of the previous component. + * </p> + * + * @see #space() + */ + public void newLine() { + cursorX = 0; + cursorY++; + } + + /** + * Moves the cursor forward by one. If the cursor goes out of the right grid + * border, it is moved to the first column of the next row. + * + * @see #newLine() + */ + public void space() { + cursorX++; + if (cursorX >= getColumns()) { + cursorX = 0; + cursorY++; + } + } + + /** + * Adds the component into this container to the cursor position. If the + * cursor position is already occupied, the cursor is moved forwards to find + * free position. If the cursor goes out from the bottom of the grid, the + * grid is automatically extended. + * + * @param component + * the component to be added. + */ + @Override + public void addComponent(Component component) { + + // Finds first available place from the grid + Area area; + boolean done = false; + while (!done) { + try { + area = new Area(component, cursorX, cursorY, cursorX, cursorY); + checkExistingOverlaps(area); + done = true; + } catch (final OverlapsException e) { + space(); + } + } + + // Extends the grid if needed + if (cursorX >= getColumns()) { + setColumns(cursorX + 1); + } + if (cursorY >= getRows()) { + setRows(cursorY + 1); + } + + addComponent(component, cursorX, cursorY); + } + + /** + * Removes the specified component from the layout. + * + * @param component + * the component to be removed. + */ + @Override + public void removeComponent(Component component) { + + // Check that the component is contained in the container + if (component == null || !components.contains(component)) { + return; + } + + Area area = null; + for (final Iterator<Area> i = areas.iterator(); area == null + && i.hasNext();) { + final Area a = i.next(); + if (a.getComponent() == component) { + area = a; + } + } + + components.remove(component); + if (area != null) { + areas.remove(area); + } + + componentToAlignment.remove(component); + + super.removeComponent(component); + + requestRepaint(); + } + + /** + * Removes the component specified by its cell coordinates. + * + * @param column + * the component's column, starting from 0. + * @param row + * the component's row, starting from 0. + */ + public void removeComponent(int column, int row) { + + // Finds the area + for (final Iterator<Area> i = areas.iterator(); i.hasNext();) { + final Area area = i.next(); + if (area.getColumn1() == column && area.getRow1() == row) { + removeComponent(area.getComponent()); + return; + } + } + } + + /** + * Gets an Iterator for the components contained in the layout. By using the + * Iterator it is possible to step through the contents of the layout. + * + * @return the Iterator of the components inside the layout. + */ + @Override + public Iterator<Component> getComponentIterator() { + return Collections.unmodifiableCollection(components).iterator(); + } + + /** + * Gets the number of components contained in the layout. Consistent with + * the iterator returned by {@link #getComponentIterator()}. + * + * @return the number of contained components + */ + @Override + public int getComponentCount() { + return components.size(); + } + + @Override + public void changeVariables(Object source, Map<String, Object> variables) { + // TODO Remove once Vaadin6Component is no longer implemented + } + + /** + * Paints the contents of this component. + * + * @param target + * the Paint Event. + * @throws PaintException + * if the paint operation failed. + */ + @Override + public void paintContent(PaintTarget target) throws PaintException { + // TODO refactor attribute names in future release. + target.addAttribute("structuralChange", structuralChange); + structuralChange = false; + + // Area iterator + final Iterator<Area> areaiterator = areas.iterator(); + + // Current item to be processed (fetch first item) + Area area = areaiterator.hasNext() ? (Area) areaiterator.next() : null; + + // Collects rowspan related information here + final HashMap<Integer, Integer> cellUsed = new HashMap<Integer, Integer>(); + + // Empty cell collector + int emptyCells = 0; + + final String[] alignmentsArray = new String[components.size()]; + final Integer[] columnExpandRatioArray = new Integer[getColumns()]; + final Integer[] rowExpandRatioArray = new Integer[getRows()]; + + int realColExpandRatioSum = 0; + float colSum = getExpandRatioSum(columnExpandRatio); + if (colSum == 0) { + // no columns has been expanded, all cols have same expand + // rate + float equalSize = 1 / (float) getColumns(); + int myRatio = Math.round(equalSize * 1000); + for (int i = 0; i < getColumns(); i++) { + columnExpandRatioArray[i] = myRatio; + } + realColExpandRatioSum = myRatio * getColumns(); + } else { + for (int i = 0; i < getColumns(); i++) { + int myRatio = Math + .round((getColumnExpandRatio(i) / colSum) * 1000); + columnExpandRatioArray[i] = myRatio; + realColExpandRatioSum += myRatio; + } + } + + boolean equallyDividedRows = false; + int realRowExpandRatioSum = 0; + float rowSum = getExpandRatioSum(rowExpandRatio); + if (rowSum == 0) { + // no rows have been expanded + equallyDividedRows = true; + float equalSize = 1 / (float) getRows(); + int myRatio = Math.round(equalSize * 1000); + for (int i = 0; i < getRows(); i++) { + rowExpandRatioArray[i] = myRatio; + } + realRowExpandRatioSum = myRatio * getRows(); + } + + int index = 0; + + // Iterates every applicable row + for (int cury = 0; cury < getRows(); cury++) { + target.startTag("gr"); + + if (!equallyDividedRows) { + int myRatio = Math + .round((getRowExpandRatio(cury) / rowSum) * 1000); + rowExpandRatioArray[cury] = myRatio; + realRowExpandRatioSum += myRatio; + + } + // Iterates every applicable column + for (int curx = 0; curx < getColumns(); curx++) { + + // Checks if current item is located at curx,cury + if (area != null && (area.row1 == cury) + && (area.column1 == curx)) { + + // First check if empty cell needs to be rendered + if (emptyCells > 0) { + target.startTag("gc"); + target.addAttribute("x", curx - emptyCells); + target.addAttribute("y", cury); + if (emptyCells > 1) { + target.addAttribute("w", emptyCells); + } + target.endTag("gc"); + emptyCells = 0; + } + + // Now proceed rendering current item + final int cols = (area.column2 - area.column1) + 1; + final int rows = (area.row2 - area.row1) + 1; + target.startTag("gc"); + + target.addAttribute("x", curx); + target.addAttribute("y", cury); + + if (cols > 1) { + target.addAttribute("w", cols); + } + if (rows > 1) { + target.addAttribute("h", rows); + } + LegacyPaint.paint(area.getComponent(), target); + + alignmentsArray[index++] = String + .valueOf(getComponentAlignment(area.getComponent()) + .getBitMask()); + + target.endTag("gc"); + + // Fetch next item + if (areaiterator.hasNext()) { + area = areaiterator.next(); + } else { + area = null; + } + + // Updates the cellUsed if rowspan needed + if (rows > 1) { + int spannedx = curx; + for (int j = 1; j <= cols; j++) { + cellUsed.put(new Integer(spannedx), new Integer( + cury + rows - 1)); + spannedx++; + } + } + + // Skips the current item's spanned columns + if (cols > 1) { + curx += cols - 1; + } + + } else { + + // Checks against cellUsed, render space or ignore cell + if (cellUsed.containsKey(new Integer(curx))) { + + // Current column contains already an item, + // check if rowspan affects at current x,y position + final int rowspanDepth = cellUsed + .get(new Integer(curx)).intValue(); + + if (rowspanDepth >= cury) { + + // ignore cell + // Check if empty cell needs to be rendered + if (emptyCells > 0) { + target.startTag("gc"); + target.addAttribute("x", curx - emptyCells); + target.addAttribute("y", cury); + if (emptyCells > 1) { + target.addAttribute("w", emptyCells); + } + target.endTag("gc"); + + emptyCells = 0; + } + } else { + + // empty cell is needed + emptyCells++; + + // Removes the cellUsed key as it has become + // obsolete + cellUsed.remove(Integer.valueOf(curx)); + } + } else { + + // empty cell is needed + emptyCells++; + } + } + + } // iterates every column + + // Last column handled of current row + + // Checks if empty cell needs to be rendered + if (emptyCells > 0) { + target.startTag("gc"); + target.addAttribute("x", getColumns() - emptyCells); + target.addAttribute("y", cury); + if (emptyCells > 1) { + target.addAttribute("w", emptyCells); + } + target.endTag("gc"); + + emptyCells = 0; + } + + target.endTag("gr"); + } // iterates every row + + // Last row handled + + // correct possible rounding error + if (rowExpandRatioArray.length > 0) { + rowExpandRatioArray[0] -= realRowExpandRatioSum - 1000; + } + if (columnExpandRatioArray.length > 0) { + columnExpandRatioArray[0] -= realColExpandRatioSum - 1000; + } + + target.addAttribute("colExpand", columnExpandRatioArray); + target.addAttribute("rowExpand", rowExpandRatioArray); + + // Add child component alignment info to layout tag + target.addAttribute("alignments", alignmentsArray); + + } + + private float getExpandRatioSum(Map<Integer, Float> ratioMap) { + float sum = 0; + for (Iterator<Entry<Integer, Float>> iterator = ratioMap.entrySet() + .iterator(); iterator.hasNext();) { + sum += iterator.next().getValue(); + } + return sum; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.Layout.AlignmentHandler#getComponentAlignment(com + * .vaadin.ui.Component) + */ + @Override + public Alignment getComponentAlignment(Component childComponent) { + Alignment alignment = componentToAlignment.get(childComponent); + if (alignment == null) { + return ALIGNMENT_DEFAULT; + } else { + return alignment; + } + } + + /** + * Defines a rectangular area of cells in a GridLayout. + * + * <p> + * Also maintains a reference to the component contained in the area. + * </p> + * + * <p> + * The area is specified by the cell coordinates of its upper left corner + * (column1,row1) and lower right corner (column2,row2). As otherwise with + * GridLayout, the column and row coordinates start from zero. + * </p> + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ + public class Area implements Serializable { + + /** + * The column of the upper left corner cell of the area. + */ + private final int column1; + + /** + * The row of the upper left corner cell of the area. + */ + private int row1; + + /** + * The column of the lower right corner cell of the area. + */ + private final int column2; + + /** + * The row of the lower right corner cell of the area. + */ + private int row2; + + /** + * Component painted in the area. + */ + private Component component; + + /** + * <p> + * Construct a new area on a grid. + * </p> + * + * @param component + * the component connected to the area. + * @param column1 + * The column of the upper left corner cell of the area. The + * leftmost column has index 0. + * @param row1 + * The row of the upper left corner cell of the area. The + * topmost row has index 0. + * @param column2 + * The column of the lower right corner cell of the area. The + * leftmost column has index 0. + * @param row2 + * The row of the lower right corner cell of the area. The + * topmost row has index 0. + */ + public Area(Component component, int column1, int row1, int column2, + int row2) { + this.column1 = column1; + this.row1 = row1; + this.column2 = column2; + this.row2 = row2; + this.component = component; + } + + /** + * Tests if this Area overlaps with another Area. + * + * @param other + * the other Area that is to be tested for overlap with this + * area + * @return <code>true</code> if <code>other</code> area overlaps with + * this on, <code>false</code> if it does not. + */ + public boolean overlaps(Area other) { + return column1 <= other.getColumn2() && row1 <= other.getRow2() + && column2 >= other.getColumn1() && row2 >= other.getRow1(); + + } + + /** + * Gets the component connected to the area. + * + * @return the Component. + */ + public Component getComponent() { + return component; + } + + /** + * Sets the component connected to the area. + * + * <p> + * This function only sets the value in the data structure and does not + * send any events or set parents. + * </p> + * + * @param newComponent + * the new connected overriding the existing one. + */ + protected void setComponent(Component newComponent) { + component = newComponent; + } + + /** + * @deprecated Use {@link #getColumn1()} instead. + */ + @Deprecated + public int getX1() { + return getColumn1(); + } + + /** + * Gets the column of the top-left corner cell. + * + * @return the column of the top-left corner cell. + */ + public int getColumn1() { + return column1; + } + + /** + * @deprecated Use {@link #getColumn2()} instead. + */ + @Deprecated + public int getX2() { + return getColumn2(); + } + + /** + * Gets the column of the bottom-right corner cell. + * + * @return the column of the bottom-right corner cell. + */ + public int getColumn2() { + return column2; + } + + /** + * @deprecated Use {@link #getRow1()} instead. + */ + @Deprecated + public int getY1() { + return getRow1(); + } + + /** + * Gets the row of the top-left corner cell. + * + * @return the row of the top-left corner cell. + */ + public int getRow1() { + return row1; + } + + /** + * @deprecated Use {@link #getRow2()} instead. + */ + @Deprecated + public int getY2() { + return getRow2(); + } + + /** + * Gets the row of the bottom-right corner cell. + * + * @return the row of the bottom-right corner cell. + */ + public int getRow2() { + return row2; + } + + } + + /** + * Gridlayout does not support laying components on top of each other. An + * <code>OverlapsException</code> is thrown when a component already exists + * (even partly) at the same space on a grid with the new component. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ + public class OverlapsException extends java.lang.RuntimeException { + + private final Area existingArea; + + /** + * Constructs an <code>OverlapsException</code>. + * + * @param existingArea + */ + public OverlapsException(Area existingArea) { + this.existingArea = existingArea; + } + + @Override + public String getMessage() { + StringBuilder sb = new StringBuilder(); + Component component = existingArea.getComponent(); + sb.append(component); + sb.append("( type = "); + sb.append(component.getClass().getName()); + if (component.getCaption() != null) { + sb.append(", caption = \""); + sb.append(component.getCaption()); + sb.append("\""); + } + sb.append(")"); + sb.append(" is already added to "); + sb.append(existingArea.column1); + sb.append(","); + sb.append(existingArea.column1); + sb.append(","); + sb.append(existingArea.row1); + sb.append(","); + sb.append(existingArea.row2); + sb.append("(column1, column2, row1, row2)."); + + return sb.toString(); + } + + /** + * Gets the area . + * + * @return the existing area. + */ + public Area getArea() { + return existingArea; + } + } + + /** + * An <code>Exception</code> object which is thrown when an area exceeds the + * bounds of the grid. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ + public class OutOfBoundsException extends java.lang.RuntimeException { + + private final Area areaOutOfBounds; + + /** + * Constructs an <code>OoutOfBoundsException</code> with the specified + * detail message. + * + * @param areaOutOfBounds + */ + public OutOfBoundsException(Area areaOutOfBounds) { + this.areaOutOfBounds = areaOutOfBounds; + } + + /** + * Gets the area that is out of bounds. + * + * @return the area out of Bound. + */ + public Area getArea() { + return areaOutOfBounds; + } + } + + /** + * Sets the number of columns in the grid. The column count can not be + * reduced if there are any areas that would be outside of the shrunk grid. + * + * @param columns + * the new number of columns in the grid. + */ + public void setColumns(int columns) { + + // The the param + if (columns < 1) { + throw new IllegalArgumentException( + "The number of columns and rows in the grid must be at least 1"); + } + + // In case of no change + if (getColumns() == columns) { + return; + } + + // Checks for overlaps + if (getColumns() > columns) { + for (final Iterator<Area> i = areas.iterator(); i.hasNext();) { + final Area area = i.next(); + if (area.column2 >= columns) { + throw new OutOfBoundsException(area); + } + } + } + + getState().setColumns(columns); + + requestRepaint(); + } + + /** + * Get the number of columns in the grid. + * + * @return the number of columns in the grid. + */ + public int getColumns() { + return getState().getColumns(); + } + + /** + * Sets the number of rows in the grid. The number of rows can not be + * reduced if there are any areas that would be outside of the shrunk grid. + * + * @param rows + * the new number of rows in the grid. + */ + public void setRows(int rows) { + + // The the param + if (rows < 1) { + throw new IllegalArgumentException( + "The number of columns and rows in the grid must be at least 1"); + } + + // In case of no change + if (getRows() == rows) { + return; + } + + // Checks for overlaps + if (getRows() > rows) { + for (final Iterator<Area> i = areas.iterator(); i.hasNext();) { + final Area area = i.next(); + if (area.row2 >= rows) { + throw new OutOfBoundsException(area); + } + } + } + + getState().setRows(rows); + + requestRepaint(); + } + + /** + * Get the number of rows in the grid. + * + * @return the number of rows in the grid. + */ + public int getRows() { + return getState().getRows(); + } + + /** + * Gets the current x-position (column) of the cursor. + * + * <p> + * The cursor position points the position for the next component that is + * added without specifying its coordinates (grid cell). When the cursor + * position is occupied, the next component will be added to first free + * position after the cursor. + * </p> + * + * @return the grid column the cursor is on, starting from 0. + */ + public int getCursorX() { + return cursorX; + } + + /** + * Sets the current cursor x-position. This is usually handled automatically + * by GridLayout. + * + * @param cursorX + */ + public void setCursorX(int cursorX) { + this.cursorX = cursorX; + } + + /** + * Gets the current y-position (row) of the cursor. + * + * <p> + * The cursor position points the position for the next component that is + * added without specifying its coordinates (grid cell). When the cursor + * position is occupied, the next component will be added to the first free + * position after the cursor. + * </p> + * + * @return the grid row the Cursor is on. + */ + public int getCursorY() { + return cursorY; + } + + /** + * Sets the current y-coordinate (row) of the cursor. This is usually + * handled automatically by GridLayout. + * + * @param cursorY + * the row number, starting from 0 for the topmost row. + */ + public void setCursorY(int cursorY) { + this.cursorY = cursorY; + } + + /* Documented in superclass */ + @Override + public void replaceComponent(Component oldComponent, Component newComponent) { + + // Gets the locations + Area oldLocation = null; + Area newLocation = null; + for (final Iterator<Area> i = areas.iterator(); i.hasNext();) { + final Area location = i.next(); + final Component component = location.getComponent(); + if (component == oldComponent) { + oldLocation = location; + } + if (component == newComponent) { + newLocation = location; + } + } + + if (oldLocation == null) { + addComponent(newComponent); + } else if (newLocation == null) { + removeComponent(oldComponent); + addComponent(newComponent, oldLocation.getColumn1(), + oldLocation.getRow1(), oldLocation.getColumn2(), + oldLocation.getRow2()); + } else { + oldLocation.setComponent(newComponent); + newLocation.setComponent(oldComponent); + requestRepaint(); + } + } + + /* + * Removes all components from this container. + * + * @see com.vaadin.ui.ComponentContainer#removeAllComponents() + */ + @Override + public void removeAllComponents() { + super.removeAllComponents(); + componentToAlignment = new HashMap<Component, Alignment>(); + cursorX = 0; + cursorY = 0; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.Layout.AlignmentHandler#setComponentAlignment(com + * .vaadin.ui.Component, int, int) + */ + @Override + public void setComponentAlignment(Component childComponent, + int horizontalAlignment, int verticalAlignment) { + componentToAlignment.put(childComponent, new Alignment( + horizontalAlignment + verticalAlignment)); + requestRepaint(); + } + + @Override + public void setComponentAlignment(Component childComponent, + Alignment alignment) { + componentToAlignment.put(childComponent, alignment); + requestRepaint(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.Layout.SpacingHandler#setSpacing(boolean) + */ + @Override + public void setSpacing(boolean spacing) { + getState().setSpacing(spacing); + requestRepaint(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.Layout.SpacingHandler#isSpacing() + */ + @Override + public boolean isSpacing() { + return getState().isSpacing(); + } + + /** + * Inserts an empty row at the specified position in the grid. + * + * @param row + * Index of the row before which the new row will be inserted. + * The leftmost row has index 0. + */ + public void insertRow(int row) { + if (row > getRows()) { + throw new IllegalArgumentException("Cannot insert row at " + row + + " in a gridlayout with height " + getRows()); + } + + for (Iterator<Area> i = areas.iterator(); i.hasNext();) { + Area existingArea = i.next(); + // Areas ending below the row needs to be moved down or stretched + if (existingArea.row2 >= row) { + existingArea.row2++; + + // Stretch areas that span over the selected row + if (existingArea.row1 >= row) { + existingArea.row1++; + } + + } + } + + if (cursorY >= row) { + cursorY++; + } + + setRows(getRows() + 1); + structuralChange = true; + requestRepaint(); + } + + /** + * Removes a row and all the components in the row. + * + * <p> + * Components which span over several rows are removed if the selected row + * is on the first row of such a component. + * </p> + * + * <p> + * If the last row is removed then all remaining components will be removed + * and the grid will be reduced to one row. The cursor will be moved to the + * upper left cell of the grid. + * </p> + * + * @param row + * Index of the row to remove. The leftmost row has index 0. + */ + public void removeRow(int row) { + if (row >= getRows()) { + throw new IllegalArgumentException("Cannot delete row " + row + + " from a gridlayout with height " + getRows()); + } + + // Remove all components in row + for (int col = 0; col < getColumns(); col++) { + removeComponent(col, row); + } + + // Shrink or remove areas in the selected row + for (Iterator<Area> i = areas.iterator(); i.hasNext();) { + Area existingArea = i.next(); + if (existingArea.row2 >= row) { + existingArea.row2--; + + if (existingArea.row1 > row) { + existingArea.row1--; + } + } + } + + if (getRows() == 1) { + /* + * Removing the last row means that the dimensions of the Grid + * layout will be truncated to 1 empty row and the cursor is moved + * to the first cell + */ + cursorX = 0; + cursorY = 0; + } else { + setRows(getRows() - 1); + if (cursorY > row) { + cursorY--; + } + } + + structuralChange = true; + requestRepaint(); + + } + + /** + * Sets the expand ratio of given column. + * + * <p> + * The expand ratio defines how excess space is distributed among columns. + * Excess space means space that is left over from components that are not + * sized relatively. By default, the excess space is distributed evenly. + * </p> + * + * <p> + * Note that the component width of the GridLayout must be defined (fixed or + * relative, as opposed to undefined) for this method to have any effect. + * </p> + * + * @see #setWidth(float, int) + * + * @param columnIndex + * @param ratio + */ + public void setColumnExpandRatio(int columnIndex, float ratio) { + columnExpandRatio.put(columnIndex, ratio); + requestRepaint(); + } + + /** + * Returns the expand ratio of given column + * + * @see #setColumnExpandRatio(int, float) + * + * @param columnIndex + * @return the expand ratio, 0.0f by default + */ + public float getColumnExpandRatio(int columnIndex) { + Float r = columnExpandRatio.get(columnIndex); + return r == null ? 0 : r.floatValue(); + } + + /** + * Sets the expand ratio of given row. + * + * <p> + * Expand ratio defines how excess space is distributed among rows. Excess + * space means the space left over from components that are not sized + * relatively. By default, the excess space is distributed evenly. + * </p> + * + * <p> + * Note, that height needs to be defined (fixed or relative, as opposed to + * undefined height) for this method to have any effect. + * </p> + * + * @see #setHeight(float, int) + * + * @param rowIndex + * The row index, starting from 0 for the topmost row. + * @param ratio + */ + public void setRowExpandRatio(int rowIndex, float ratio) { + rowExpandRatio.put(rowIndex, ratio); + requestRepaint(); + } + + /** + * Returns the expand ratio of given row. + * + * @see #setRowExpandRatio(int, float) + * + * @param rowIndex + * The row index, starting from 0 for the topmost row. + * @return the expand ratio, 0.0f by default + */ + public float getRowExpandRatio(int rowIndex) { + Float r = rowExpandRatio.get(rowIndex); + return r == null ? 0 : r.floatValue(); + } + + /** + * Gets the Component at given index. + * + * @param x + * The column index, starting from 0 for the leftmost column. + * @param y + * The row index, starting from 0 for the topmost row. + * @return Component in given cell or null if empty + */ + public Component getComponent(int x, int y) { + for (final Iterator<Area> iterator = areas.iterator(); iterator + .hasNext();) { + final Area area = iterator.next(); + if (area.getColumn1() <= x && x <= area.getColumn2() + && area.getRow1() <= y && y <= area.getRow2()) { + return area.getComponent(); + } + } + return null; + } + + /** + * Returns information about the area where given component is laid in the + * GridLayout. + * + * @param component + * the component whose area information is requested. + * @return an Area object that contains information how component is laid in + * the grid + */ + public Area getComponentArea(Component component) { + for (final Iterator<Area> iterator = areas.iterator(); iterator + .hasNext();) { + final Area area = iterator.next(); + if (area.getComponent() == component) { + return area; + } + } + return null; + } + + @Override + public void addListener(LayoutClickListener listener) { + addListener(LayoutClickEventHandler.LAYOUT_CLICK_EVENT_IDENTIFIER, + LayoutClickEvent.class, listener, + LayoutClickListener.clickMethod); + } + + @Override + public void removeListener(LayoutClickListener listener) { + removeListener(LayoutClickEventHandler.LAYOUT_CLICK_EVENT_IDENTIFIER, + LayoutClickEvent.class, listener); + } + +} diff --git a/server/src/com/vaadin/ui/HasComponents.java b/server/src/com/vaadin/ui/HasComponents.java new file mode 100644 index 0000000000..3ebd63bff2 --- /dev/null +++ b/server/src/com/vaadin/ui/HasComponents.java @@ -0,0 +1,49 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.ui; + +import java.util.Iterator; + +/** + * Interface that must be implemented by all {@link Component}s that contain + * other {@link Component}s. + * + * @author Vaadin Ltd + * @version @VERSION@ + * @since 7.0.0 + * + */ +public interface HasComponents extends Component, Iterable<Component> { + /** + * Gets an iterator to the collection of contained components. Using this + * iterator it is possible to step through all components contained in this + * container. + * + * @return the component iterator. + * + * @deprecated Use {@link #iterator()} instead. + */ + @Deprecated + public Iterator<Component> getComponentIterator(); + + /** + * Checks if the child component is visible. This method allows hiding a + * child component from updates and communication to and from the client. + * This is useful for components that show only a limited number of its + * children at any given time and want to allow updates only for the + * children that are visible (e.g. TabSheet has one tab open at a time). + * <p> + * Note that this will prevent updates from reaching the child even though + * the child itself is set to visible. Also if a child is set to invisible + * this will not force it to be visible. + * </p> + * + * @param childComponent + * The child component to check + * @return true if the child component is visible to the user, false + * otherwise + */ + public boolean isComponentVisible(Component childComponent); + +} diff --git a/server/src/com/vaadin/ui/HorizontalLayout.java b/server/src/com/vaadin/ui/HorizontalLayout.java new file mode 100644 index 0000000000..b9dc1c13ca --- /dev/null +++ b/server/src/com/vaadin/ui/HorizontalLayout.java @@ -0,0 +1,24 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.ui; + +/** + * Horizontal layout + * + * <code>HorizontalLayout</code> is a component container, which shows the + * subcomponents in the order of their addition (horizontally). + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 5.3 + */ +@SuppressWarnings("serial") +public class HorizontalLayout extends AbstractOrderedLayout { + + public HorizontalLayout() { + + } + +} diff --git a/server/src/com/vaadin/ui/HorizontalSplitPanel.java b/server/src/com/vaadin/ui/HorizontalSplitPanel.java new file mode 100644 index 0000000000..5bd6c8a075 --- /dev/null +++ b/server/src/com/vaadin/ui/HorizontalSplitPanel.java @@ -0,0 +1,34 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.ui; + +/** + * A horizontal split panel contains two components and lays them horizontally. + * The first component is on the left side. + * + * <pre> + * + * +---------------------++----------------------+ + * | || | + * | The first component || The second component | + * | || | + * +---------------------++----------------------+ + * + * ^ + * | + * the splitter + * + * </pre> + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 6.5 + */ +public class HorizontalSplitPanel extends AbstractSplitPanel { + public HorizontalSplitPanel() { + super(); + setSizeFull(); + } +} diff --git a/server/src/com/vaadin/ui/Html5File.java b/server/src/com/vaadin/ui/Html5File.java new file mode 100644 index 0000000000..aa3fb558fa --- /dev/null +++ b/server/src/com/vaadin/ui/Html5File.java @@ -0,0 +1,65 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.ui; + +import java.io.Serializable; + +import com.vaadin.event.dd.DropHandler; +import com.vaadin.terminal.StreamVariable; + +/** + * {@link DragAndDropWrapper} can receive also files from client computer if + * appropriate HTML 5 features are supported on client side. This class wraps + * information about dragged file on server side. + */ +public class Html5File implements Serializable { + + private String name; + private long size; + private StreamVariable streamVariable; + private String type; + + Html5File(String name, long size, String mimeType) { + this.name = name; + this.size = size; + type = mimeType; + } + + public String getFileName() { + return name; + } + + public long getFileSize() { + return size; + } + + public String getType() { + return type; + } + + /** + * Sets the {@link StreamVariable} that into which the file contents will be + * written. Usage of StreamVariable is similar to {@link Upload} component. + * <p> + * If the {@link StreamVariable} is not set in the {@link DropHandler} the + * file contents will not be sent to server. + * <p> + * <em>Note!</em> receiving file contents is experimental feature depending + * on HTML 5 API's. It is supported only by modern web browsers like Firefox + * 3.6 and above and recent webkit based browsers (Safari 5, Chrome 6) at + * this time. + * + * @param streamVariable + * the callback that returns stream where the implementation + * writes the file contents as it arrives. + */ + public void setStreamVariable(StreamVariable streamVariable) { + this.streamVariable = streamVariable; + } + + public StreamVariable getStreamVariable() { + return streamVariable; + } + +}
\ No newline at end of file diff --git a/server/src/com/vaadin/ui/InlineDateField.java b/server/src/com/vaadin/ui/InlineDateField.java new file mode 100644 index 0000000000..cf61703318 --- /dev/null +++ b/server/src/com/vaadin/ui/InlineDateField.java @@ -0,0 +1,46 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.util.Date; + +import com.vaadin.data.Property; + +/** + * <p> + * A date entry component, which displays the actual date selector inline. + * + * </p> + * + * @see DateField + * @see PopupDateField + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 5.0 + */ +public class InlineDateField extends DateField { + + public InlineDateField() { + super(); + } + + public InlineDateField(Property dataSource) throws IllegalArgumentException { + super(dataSource); + } + + public InlineDateField(String caption, Date value) { + super(caption, value); + } + + public InlineDateField(String caption, Property dataSource) { + super(caption, dataSource); + } + + public InlineDateField(String caption) { + super(caption); + } + +} diff --git a/server/src/com/vaadin/ui/JavaScript.java b/server/src/com/vaadin/ui/JavaScript.java new file mode 100644 index 0000000000..0b4669728a --- /dev/null +++ b/server/src/com/vaadin/ui/JavaScript.java @@ -0,0 +1,157 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.util.HashMap; +import java.util.Map; + +import com.vaadin.external.json.JSONArray; +import com.vaadin.external.json.JSONException; +import com.vaadin.shared.communication.ServerRpc; +import com.vaadin.shared.extension.javascriptmanager.ExecuteJavaScriptRpc; +import com.vaadin.shared.extension.javascriptmanager.JavaScriptManagerState; +import com.vaadin.terminal.AbstractExtension; +import com.vaadin.terminal.Page; + +/** + * Provides access to JavaScript functionality in the web browser. To get an + * instance of JavaScript, either use Page.getJavaScript() or + * JavaScript.getCurrent() as a shorthand for getting the JavaScript object + * corresponding to the current Page. + * + * @author Vaadin Ltd + * @version @VERSION@ + * @since 7.0.0 + */ +public class JavaScript extends AbstractExtension { + private Map<String, JavaScriptFunction> functions = new HashMap<String, JavaScriptFunction>(); + + // Can not be defined in client package as this JSONArray is not available + // in GWT + public interface JavaScriptCallbackRpc extends ServerRpc { + public void call(String name, JSONArray arguments); + } + + /** + * Creates a new JavaScript object. You should typically not this, but + * instead use the JavaScript object already associated with your Page + * object. + */ + public JavaScript() { + registerRpc(new JavaScriptCallbackRpc() { + @Override + public void call(String name, JSONArray arguments) { + JavaScriptFunction function = functions.get(name); + // TODO handle situation if name is not registered + try { + function.call(arguments); + } catch (JSONException e) { + throw new IllegalArgumentException(e); + } + } + }); + } + + @Override + public JavaScriptManagerState getState() { + return (JavaScriptManagerState) super.getState(); + } + + /** + * Add a new function to the global JavaScript namespace (i.e. the window + * object). The <code>call</code> method in the passed + * {@link JavaScriptFunction} object will be invoked with the same + * parameters whenever the JavaScript function is called in the browser. + * + * A function added with the name <code>"myFunction"</code> can thus be + * invoked with the following JavaScript code: + * <code>window.myFunction(argument1, argument2)</code>. + * + * If the name parameter contains dots, simple objects are created on demand + * to allow calling the function using the same name (e.g. + * <code>window.myObject.myFunction</code>). + * + * @param name + * the name that the function should get in the global JavaScript + * namespace. + * @param function + * the JavaScriptFunction that will be invoked if the JavaScript + * function is called. + */ + public void addFunction(String name, JavaScriptFunction function) { + functions.put(name, function); + if (getState().getNames().add(name)) { + requestRepaint(); + } + } + + /** + * Removes a JavaScripFunction from the browser's global JavaScript + * namespace. + * + * If the name contains dots and intermediate objects were created by + * {@link #addFunction(String, JavaScriptFunction)}, these objects will not + * be removed by this method. + * + * @param name + * the name of the callback to remove + */ + public void removeFunction(String name) { + functions.remove(name); + if (getState().getNames().remove(name)) { + requestRepaint(); + } + } + + /** + * Executes the given JavaScript code in the browser. + * + * @param script + * The JavaScript code to run. + */ + public void execute(String script) { + getRpcProxy(ExecuteJavaScriptRpc.class).executeJavaScript(script); + } + + /** + * Executes the given JavaScript code in the browser. + * + * @param script + * The JavaScript code to run. + */ + public static void eval(String script) { + getCurrent().execute(script); + } + + /** + * Get the JavaScript object for the current Page, or null if there is no + * current page. + * + * @see Page#getCurrent() + * + * @return the JavaScript object corresponding to the current Page, or + * <code>null</code> if there is no current page. + */ + public static JavaScript getCurrent() { + Page page = Page.getCurrent(); + if (page == null) { + return null; + } + return page.getJavaScript(); + } + + /** + * JavaScript is not designed to be removed. + * + * @throws UnsupportedOperationException + * when invoked + */ + @Override + public void removeFromTarget() { + throw new UnsupportedOperationException( + "JavaScript is not designed to be removed."); + } + +} diff --git a/server/src/com/vaadin/ui/JavaScriptFunction.java b/server/src/com/vaadin/ui/JavaScriptFunction.java new file mode 100644 index 0000000000..e39ae9b87b --- /dev/null +++ b/server/src/com/vaadin/ui/JavaScriptFunction.java @@ -0,0 +1,41 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.io.Serializable; + +import com.vaadin.external.json.JSONArray; +import com.vaadin.external.json.JSONException; +import com.vaadin.terminal.AbstractJavaScriptExtension; + +/** + * Defines a method that is called by a client-side JavaScript function. When + * the corresponding JavaScript function is called, the {@link #call(JSONArray)} + * method is invoked. + * + * @see JavaScript#addFunction(String, JavaScriptCallback) + * @see AbstractJavaScriptComponent#addFunction(String, JavaScriptCallback) + * @see AbstractJavaScriptExtension#addFunction(String, JavaScriptCallback) + * + * @author Vaadin Ltd + * @version @VERSION@ + * @since 7.0.0 + */ +public interface JavaScriptFunction extends Serializable { + /** + * Invoked whenever the corresponding JavaScript function is called in the + * browser. + * <p> + * Because of the asynchronous nature of the communication between client + * and server, no return value can be sent back to the browser. + * + * @param arguments + * an array with JSON representations of the arguments with which + * the JavaScript function was called. + * @throws JSONException + * if the arguments can not be interpreted + */ + public void call(JSONArray arguments) throws JSONException; +} diff --git a/server/src/com/vaadin/ui/Label.java b/server/src/com/vaadin/ui/Label.java new file mode 100644 index 0000000000..7e50a37805 --- /dev/null +++ b/server/src/com/vaadin/ui/Label.java @@ -0,0 +1,483 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.lang.reflect.Method; +import java.util.logging.Logger; + +import com.vaadin.data.Property; +import com.vaadin.data.util.converter.Converter; +import com.vaadin.data.util.converter.ConverterUtil; +import com.vaadin.shared.ui.label.ContentMode; +import com.vaadin.shared.ui.label.LabelState; + +/** + * Label component for showing non-editable short texts. + * + * The label content can be set to the modes specified by {@link ContentMode} + * + * <p> + * The contents of the label may contain simple formatting: + * <ul> + * <li><b><b></b> Bold + * <li><b><i></b> Italic + * <li><b><u></b> Underlined + * <li><b><br/></b> Linebreak + * <li><b><ul><li>item 1</li><li>item 2</li></ul></b> List of + * items + * </ul> + * The <b>b</b>,<b>i</b>,<b>u</b> and <b>li</b> tags can contain all the tags in + * the list recursively. + * </p> + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +@SuppressWarnings("serial") +public class Label extends AbstractComponent implements Property<String>, + Property.Viewer, Property.ValueChangeListener, + Property.ValueChangeNotifier, Comparable<Label> { + + private static final Logger logger = Logger + .getLogger(Label.class.getName()); + + /** + * @deprecated From 7.0, use {@link ContentMode#TEXT} instead + */ + @Deprecated + public static final ContentMode CONTENT_TEXT = ContentMode.TEXT; + + /** + * @deprecated From 7.0, use {@link ContentMode#PREFORMATTED} instead + */ + @Deprecated + public static final ContentMode CONTENT_PREFORMATTED = ContentMode.PREFORMATTED; + + /** + * @deprecated From 7.0, use {@link ContentMode#XHTML} instead + */ + @Deprecated + public static final ContentMode CONTENT_XHTML = ContentMode.XHTML; + + /** + * @deprecated From 7.0, use {@link ContentMode#XML} instead + */ + @Deprecated + public static final ContentMode CONTENT_XML = ContentMode.XML; + + /** + * @deprecated From 7.0, use {@link ContentMode#RAW} instead + */ + @Deprecated + public static final ContentMode CONTENT_RAW = ContentMode.RAW; + + /** + * @deprecated From 7.0, use {@link ContentMode#TEXT} instead + */ + @Deprecated + public static final ContentMode CONTENT_DEFAULT = ContentMode.TEXT; + + /** + * A converter used to convert from the data model type to the field type + * and vice versa. Label type is always String. + */ + private Converter<String, Object> converter = null; + + private Property<String> dataSource = null; + + /** + * Creates an empty Label. + */ + public Label() { + this(""); + } + + /** + * Creates a new instance of Label with text-contents. + * + * @param content + */ + public Label(String content) { + this(content, ContentMode.TEXT); + } + + /** + * Creates a new instance of Label with text-contents read from given + * datasource. + * + * @param contentSource + */ + public Label(Property contentSource) { + this(contentSource, ContentMode.TEXT); + } + + /** + * Creates a new instance of Label with text-contents. + * + * @param content + * @param contentMode + */ + public Label(String content, ContentMode contentMode) { + setValue(content); + setContentMode(contentMode); + setWidth(100, Unit.PERCENTAGE); + } + + /** + * Creates a new instance of Label with text-contents read from given + * datasource. + * + * @param contentSource + * @param contentMode + */ + public Label(Property contentSource, ContentMode contentMode) { + setPropertyDataSource(contentSource); + setContentMode(contentMode); + setWidth(100, Unit.PERCENTAGE); + } + + @Override + public LabelState getState() { + return (LabelState) super.getState(); + } + + /** + * Gets the value of the label. + * <p> + * The value of the label is the text that is shown to the end user. + * Depending on the {@link ContentMode} it is plain text or markup. + * </p> + * + * @return the value of the label. + */ + @Override + public String getValue() { + if (getPropertyDataSource() == null) { + // Use internal value if we are running without a data source + return getState().getText(); + } + return ConverterUtil.convertFromModel(getPropertyDataSource() + .getValue(), String.class, getConverter(), getLocale()); + } + + /** + * Set the value of the label. Value of the label is the XML contents of the + * label. + * + * @param newStringValue + * the New value of the label. + */ + @Override + public void setValue(Object newStringValue) { + if (newStringValue != null && newStringValue.getClass() != String.class) { + throw new Converter.ConversionException("Value of type " + + newStringValue.getClass() + " cannot be assigned to " + + String.class.getName()); + } + if (getPropertyDataSource() == null) { + getState().setText((String) newStringValue); + requestRepaint(); + } else { + throw new IllegalStateException( + "Label is only a Property.Viewer and cannot update its data source"); + } + } + + /** + * Returns the value displayed by this label. + * + * @see java.lang.Object#toString() + * @deprecated As of 7.0.0, use {@link #getValue()} to get the value of the + * label or {@link #getPropertyDataSource()} .getValue() to get + * the value of the data source. + */ + @Deprecated + @Override + public String toString() { + logger.warning("You are using Label.toString() to get the value for a " + + getClass().getSimpleName() + + ". This is not recommended and will not be supported in future versions."); + return getValue(); + } + + /** + * Gets the type of the Property. + * + * @see com.vaadin.data.Property#getType() + */ + @Override + public Class<String> getType() { + return String.class; + } + + /** + * Gets the viewing data-source property. + * + * @return the data source property. + * @see com.vaadin.data.Property.Viewer#getPropertyDataSource() + */ + @Override + public Property getPropertyDataSource() { + return dataSource; + } + + /** + * Sets the property as data-source for viewing. + * + * @param newDataSource + * the new data source Property + * @see com.vaadin.data.Property.Viewer#setPropertyDataSource(com.vaadin.data.Property) + */ + @Override + public void setPropertyDataSource(Property newDataSource) { + // Stops listening the old data source changes + if (dataSource != null + && Property.ValueChangeNotifier.class + .isAssignableFrom(dataSource.getClass())) { + ((Property.ValueChangeNotifier) dataSource).removeListener(this); + } + + if (!ConverterUtil.canConverterHandle(getConverter(), String.class, + newDataSource.getType())) { + // Try to find a converter + Converter<String, ?> c = ConverterUtil.getConverter(String.class, + newDataSource.getType(), getApplication()); + setConverter(c); + } + dataSource = newDataSource; + + // Listens the new data source if possible + if (dataSource != null + && Property.ValueChangeNotifier.class + .isAssignableFrom(dataSource.getClass())) { + ((Property.ValueChangeNotifier) dataSource).addListener(this); + } + requestRepaint(); + } + + /** + * Gets the content mode of the Label. + * + * @return the Content mode of the label. + * + * @see ContentMode + */ + public ContentMode getContentMode() { + return getState().getContentMode(); + } + + /** + * Sets the content mode of the Label. + * + * @param contentMode + * the New content mode of the label. + * + * @see ContentMode + */ + public void setContentMode(ContentMode contentMode) { + if (contentMode == null) { + throw new IllegalArgumentException("Content mode can not be null"); + } + + getState().setContentMode(contentMode); + requestRepaint(); + } + + /* Value change events */ + + private static final Method VALUE_CHANGE_METHOD; + + static { + try { + VALUE_CHANGE_METHOD = Property.ValueChangeListener.class + .getDeclaredMethod("valueChange", + new Class[] { Property.ValueChangeEvent.class }); + } catch (final java.lang.NoSuchMethodException e) { + // This should never happen + throw new java.lang.RuntimeException( + "Internal error finding methods in Label"); + } + } + + /** + * Value change event + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ + public static class ValueChangeEvent extends Component.Event implements + Property.ValueChangeEvent { + + /** + * New instance of text change event + * + * @param source + * the Source of the event. + */ + public ValueChangeEvent(Label source) { + super(source); + } + + /** + * Gets the Property that has been modified. + * + * @see com.vaadin.data.Property.ValueChangeEvent#getProperty() + */ + @Override + public Property getProperty() { + return (Property) getSource(); + } + } + + /** + * Adds the value change listener. + * + * @param listener + * the Listener to be added. + * @see com.vaadin.data.Property.ValueChangeNotifier#addListener(com.vaadin.data.Property.ValueChangeListener) + */ + @Override + public void addListener(Property.ValueChangeListener listener) { + addListener(Label.ValueChangeEvent.class, listener, VALUE_CHANGE_METHOD); + } + + /** + * Removes the value change listener. + * + * @param listener + * the Listener to be removed. + * @see com.vaadin.data.Property.ValueChangeNotifier#removeListener(com.vaadin.data.Property.ValueChangeListener) + */ + @Override + public void removeListener(Property.ValueChangeListener listener) { + removeListener(Label.ValueChangeEvent.class, listener, + VALUE_CHANGE_METHOD); + } + + /** + * Emits the options change event. + */ + protected void fireValueChange() { + // Set the error message + fireEvent(new Label.ValueChangeEvent(this)); + } + + /** + * Listens the value change events from data source. + * + * @see com.vaadin.data.Property.ValueChangeListener#valueChange(Property.ValueChangeEvent) + */ + @Override + public void valueChange(Property.ValueChangeEvent event) { + // Update the internal value from the data source + getState().setText(getValue()); + requestRepaint(); + + fireValueChange(); + } + + private String getComparableValue() { + String stringValue = getValue(); + if (stringValue == null) { + stringValue = ""; + } + + if (getContentMode() == ContentMode.XHTML + || getContentMode() == ContentMode.XML) { + return stripTags(stringValue); + } else { + return stringValue; + } + + } + + /** + * Compares the Label to other objects. + * + * <p> + * Labels can be compared to other labels for sorting label contents. This + * is especially handy for sorting table columns. + * </p> + * + * <p> + * In RAW, PREFORMATTED and TEXT modes, the label contents are compared as + * is. In XML, UIDL and XHTML modes, only CDATA is compared and tags + * ignored. If the other object is not a Label, its toString() return value + * is used in comparison. + * </p> + * + * @param other + * the Other object to compare to. + * @return a negative integer, zero, or a positive integer as this object is + * less than, equal to, or greater than the specified object. + * @see java.lang.Comparable#compareTo(java.lang.Object) + */ + @Override + public int compareTo(Label other) { + + String thisValue = getComparableValue(); + String otherValue = other.getComparableValue(); + + return thisValue.compareTo(otherValue); + } + + /** + * Strips the tags from the XML. + * + * @param xml + * the String containing a XML snippet. + * @return the original XML without tags. + */ + private String stripTags(String xml) { + + final StringBuffer res = new StringBuffer(); + + int processed = 0; + final int xmlLen = xml.length(); + while (processed < xmlLen) { + int next = xml.indexOf('<', processed); + if (next < 0) { + next = xmlLen; + } + res.append(xml.substring(processed, next)); + if (processed < xmlLen) { + next = xml.indexOf('>', processed); + if (next < 0) { + next = xmlLen; + } + processed = next + 1; + } + } + + return res.toString(); + } + + /** + * Gets the converter used to convert the property data source value to the + * label value. + * + * @return The converter or null if none is set. + */ + public Converter<String, Object> getConverter() { + return converter; + } + + /** + * Sets the converter used to convert the label value to the property data + * source type. The converter must have a presentation type of String. + * + * @param converter + * The new converter to use. + */ + public void setConverter(Converter<String, ?> converter) { + this.converter = (Converter<String, Object>) converter; + requestRepaint(); + } + +} diff --git a/server/src/com/vaadin/ui/Layout.java b/server/src/com/vaadin/ui/Layout.java new file mode 100644 index 0000000000..d083f9afdc --- /dev/null +++ b/server/src/com/vaadin/ui/Layout.java @@ -0,0 +1,229 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.io.Serializable; + +import com.vaadin.shared.ui.VMarginInfo; +import com.vaadin.shared.ui.AlignmentInfo.Bits; + +/** + * Extension to the {@link ComponentContainer} interface which adds the + * layouting control to the elements in the container. This is required by the + * various layout components to enable them to place other components in + * specific locations in the UI. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +public interface Layout extends ComponentContainer, Serializable { + + /** + * Enable layout margins. Affects all four sides of the layout. This will + * tell the client-side implementation to leave extra space around the + * layout. The client-side implementation decides the actual amount, and it + * can vary between themes. + * + * @param enabled + */ + public void setMargin(boolean enabled); + + /** + * Enable specific layout margins. This will tell the client-side + * implementation to leave extra space around the layout in specified edges, + * clockwise from top (top, right, bottom, left). The client-side + * implementation decides the actual amount, and it can vary between themes. + * + * @param top + * @param right + * @param bottom + * @param left + */ + public void setMargin(boolean top, boolean right, boolean bottom, + boolean left); + + /** + * AlignmentHandler is most commonly an advanced {@link Layout} that can + * align its components. + */ + public interface AlignmentHandler extends Serializable { + + /** + * Contained component should be aligned horizontally to the left. + * + * @deprecated Use of {@link Alignment} class and its constants + */ + @Deprecated + public static final int ALIGNMENT_LEFT = Bits.ALIGNMENT_LEFT; + + /** + * Contained component should be aligned horizontally to the right. + * + * @deprecated Use of {@link Alignment} class and its constants + */ + @Deprecated + public static final int ALIGNMENT_RIGHT = Bits.ALIGNMENT_RIGHT; + + /** + * Contained component should be aligned vertically to the top. + * + * @deprecated Use of {@link Alignment} class and its constants + */ + @Deprecated + public static final int ALIGNMENT_TOP = Bits.ALIGNMENT_TOP; + + /** + * Contained component should be aligned vertically to the bottom. + * + * @deprecated Use of {@link Alignment} class and its constants + */ + @Deprecated + public static final int ALIGNMENT_BOTTOM = Bits.ALIGNMENT_BOTTOM; + + /** + * Contained component should be horizontally aligned to center. + * + * @deprecated Use of {@link Alignment} class and its constants + */ + @Deprecated + public static final int ALIGNMENT_HORIZONTAL_CENTER = Bits.ALIGNMENT_HORIZONTAL_CENTER; + + /** + * Contained component should be vertically aligned to center. + * + * @deprecated Use of {@link Alignment} class and its constants + */ + @Deprecated + public static final int ALIGNMENT_VERTICAL_CENTER = Bits.ALIGNMENT_VERTICAL_CENTER; + + /** + * Set alignment for one contained component in this layout. Alignment + * is calculated as a bit mask of the two passed values. + * + * @deprecated Use {@link #setComponentAlignment(Component, Alignment)} + * instead + * + * @param childComponent + * the component to align within it's layout cell. + * @param horizontalAlignment + * the horizontal alignment for the child component (left, + * center, right). Use ALIGNMENT constants. + * @param verticalAlignment + * the vertical alignment for the child component (top, + * center, bottom). Use ALIGNMENT constants. + */ + @Deprecated + public void setComponentAlignment(Component childComponent, + int horizontalAlignment, int verticalAlignment); + + /** + * Set alignment for one contained component in this layout. Use + * predefined alignments from Alignment class. + * + * Example: <code> + * layout.setComponentAlignment(myComponent, Alignment.TOP_RIGHT); + * </code> + * + * @param childComponent + * the component to align within it's layout cell. + * @param alignment + * the Alignment value to be set + */ + public void setComponentAlignment(Component childComponent, + Alignment alignment); + + /** + * Returns the current Alignment of given component. + * + * @param childComponent + * @return the {@link Alignment} + */ + public Alignment getComponentAlignment(Component childComponent); + + } + + /** + * This type of layout supports automatic addition of space between its + * components. + * + */ + public interface SpacingHandler extends Serializable { + /** + * Enable spacing between child components within this layout. + * + * <p> + * <strong>NOTE:</strong> This will only affect the space between + * components, not the space around all the components in the layout + * (i.e. do not confuse this with the cellspacing attribute of a HTML + * Table). Use {@link #setMargin(boolean)} to add space around the + * layout. + * </p> + * + * <p> + * See the reference manual for more information about CSS rules for + * defining the amount of spacing to use. + * </p> + * + * @param enabled + * true if spacing should be turned on, false if it should be + * turned off + */ + public void setSpacing(boolean enabled); + + /** + * + * @return true if spacing between child components within this layout + * is enabled, false otherwise + */ + public boolean isSpacing(); + } + + /** + * This type of layout supports automatic addition of margins (space around + * its components). + */ + public interface MarginHandler extends Serializable { + /** + * Enable margins for this layout. + * + * <p> + * <strong>NOTE:</strong> This will only affect the space around the + * components in the layout, not space between the components in the + * layout. Use {@link #setSpacing(boolean)} to add space between the + * components in the layout. + * </p> + * + * <p> + * See the reference manual for more information about CSS rules for + * defining the size of the margin. + * </p> + * + * @param marginInfo + * MarginInfo object containing the new margins. + */ + public void setMargin(MarginInfo marginInfo); + + /** + * + * @return MarginInfo containing the currently enabled margins. + */ + public MarginInfo getMargin(); + } + + @SuppressWarnings("serial") + public static class MarginInfo extends VMarginInfo implements Serializable { + + public MarginInfo(boolean enabled) { + super(enabled, enabled, enabled, enabled); + } + + public MarginInfo(boolean top, boolean right, boolean bottom, + boolean left) { + super(top, right, bottom, left); + } + } +} diff --git a/server/src/com/vaadin/ui/Link.java b/server/src/com/vaadin/ui/Link.java new file mode 100644 index 0000000000..fd105f3255 --- /dev/null +++ b/server/src/com/vaadin/ui/Link.java @@ -0,0 +1,242 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.util.Map; + +import com.vaadin.terminal.Page; +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.PaintTarget; +import com.vaadin.terminal.Resource; +import com.vaadin.terminal.Vaadin6Component; + +/** + * Link is used to create external or internal URL links. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +@SuppressWarnings("serial") +public class Link extends AbstractComponent implements Vaadin6Component { + + /* Target window border type constant: No window border */ + public static final int TARGET_BORDER_NONE = Page.BORDER_NONE; + + /* Target window border type constant: Minimal window border */ + public static final int TARGET_BORDER_MINIMAL = Page.BORDER_MINIMAL; + + /* Target window border type constant: Default window border */ + public static final int TARGET_BORDER_DEFAULT = Page.BORDER_DEFAULT; + + private Resource resource = null; + + private String targetName; + + private int targetBorder = TARGET_BORDER_DEFAULT; + + private int targetWidth = -1; + + private int targetHeight = -1; + + /** + * Creates a new link. + */ + public Link() { + + } + + /** + * Creates a new instance of Link. + * + * @param caption + * @param resource + */ + public Link(String caption, Resource resource) { + setCaption(caption); + this.resource = resource; + } + + /** + * Creates a new instance of Link that opens a new window. + * + * + * @param caption + * the Link text. + * @param targetName + * the name of the target window where the link opens to. Empty + * name of null implies that the target is opened to the window + * containing the link. + * @param width + * the Width of the target window. + * @param height + * the Height of the target window. + * @param border + * the Border style of the target window. + * + */ + public Link(String caption, Resource resource, String targetName, + int width, int height, int border) { + setCaption(caption); + this.resource = resource; + setTargetName(targetName); + setTargetWidth(width); + setTargetHeight(height); + setTargetBorder(border); + } + + /** + * 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 { + + if (resource != null) { + target.addAttribute("src", resource); + } else { + return; + } + + // Target window name + final String name = getTargetName(); + if (name != null && name.length() > 0) { + target.addAttribute("name", name); + } + + // Target window size + if (getTargetWidth() >= 0) { + target.addAttribute("targetWidth", getTargetWidth()); + } + if (getTargetHeight() >= 0) { + target.addAttribute("targetHeight", getTargetHeight()); + } + + // Target window border + switch (getTargetBorder()) { + case TARGET_BORDER_MINIMAL: + target.addAttribute("border", "minimal"); + break; + case TARGET_BORDER_NONE: + target.addAttribute("border", "none"); + break; + } + } + + /** + * Returns the target window border. + * + * @return the target window border. + */ + public int getTargetBorder() { + return targetBorder; + } + + /** + * Returns the target window height or -1 if not set. + * + * @return the target window height. + */ + public int getTargetHeight() { + return targetHeight < 0 ? -1 : targetHeight; + } + + /** + * Returns the target window name. Empty name of null implies that the + * target is opened to the window containing the link. + * + * @return the target window name. + */ + public String getTargetName() { + return targetName; + } + + /** + * Returns the target window width or -1 if not set. + * + * @return the target window width. + */ + public int getTargetWidth() { + return targetWidth < 0 ? -1 : targetWidth; + } + + /** + * Sets the border of the target window. + * + * @param targetBorder + * the targetBorder to set. + */ + public void setTargetBorder(int targetBorder) { + if (targetBorder == TARGET_BORDER_DEFAULT + || targetBorder == TARGET_BORDER_MINIMAL + || targetBorder == TARGET_BORDER_NONE) { + this.targetBorder = targetBorder; + requestRepaint(); + } + } + + /** + * Sets the target window height. + * + * @param targetHeight + * the targetHeight to set. + */ + public void setTargetHeight(int targetHeight) { + this.targetHeight = targetHeight; + requestRepaint(); + } + + /** + * Sets the target window name. + * + * @param targetName + * the targetName to set. + */ + public void setTargetName(String targetName) { + this.targetName = targetName; + requestRepaint(); + } + + /** + * Sets the target window width. + * + * @param targetWidth + * the targetWidth to set. + */ + public void setTargetWidth(int targetWidth) { + this.targetWidth = targetWidth; + requestRepaint(); + } + + /** + * Returns the resource this link opens. + * + * @return the Resource. + */ + public Resource getResource() { + return resource; + } + + /** + * Sets the resource this link opens. + * + * @param resource + * the resource to set. + */ + public void setResource(Resource resource) { + this.resource = resource; + requestRepaint(); + } + + @Override + public void changeVariables(Object source, Map<String, Object> variables) { + // TODO Remove once Vaadin6Component is no longer implemented + } +} diff --git a/server/src/com/vaadin/ui/ListSelect.java b/server/src/com/vaadin/ui/ListSelect.java new file mode 100644 index 0000000000..35ccb34b3c --- /dev/null +++ b/server/src/com/vaadin/ui/ListSelect.java @@ -0,0 +1,96 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.util.Collection; + +import com.vaadin.data.Container; +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.PaintTarget; + +/** + * 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 columns = 0; + 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); + } + + /** + * Sets the number of columns in the editor. If the number of columns is set + * 0, the actual number of displayed columns is determined implicitly by the + * adapter. + * + * @param columns + * the number of columns to set. + */ + public void setColumns(int columns) { + if (columns < 0) { + columns = 0; + } + if (this.columns != columns) { + this.columns = columns; + requestRepaint(); + } + } + + public int getColumns() { + return columns; + } + + 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; + requestRepaint(); + } + } + + @Override + public void paintContent(PaintTarget target) throws PaintException { + target.addAttribute("type", "list"); + // Adds the number of columns + if (columns != 0) { + target.addAttribute("cols", columns); + } + // Adds the number of rows + if (rows != 0) { + target.addAttribute("rows", rows); + } + super.paintContent(target); + } +} diff --git a/server/src/com/vaadin/ui/LoginForm.java b/server/src/com/vaadin/ui/LoginForm.java new file mode 100644 index 0000000000..db7e5f9dd9 --- /dev/null +++ b/server/src/com/vaadin/ui/LoginForm.java @@ -0,0 +1,353 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.ui; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.Serializable; +import java.io.UnsupportedEncodingException; +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +import com.vaadin.Application; +import com.vaadin.terminal.ApplicationResource; +import com.vaadin.terminal.DownloadStream; +import com.vaadin.terminal.RequestHandler; +import com.vaadin.terminal.WrappedRequest; +import com.vaadin.terminal.WrappedResponse; +import com.vaadin.terminal.gwt.client.ApplicationConnection; + +/** + * LoginForm is a Vaadin component to handle common problem among Ajax + * applications: browsers password managers don't fill dynamically created forms + * like all those UI elements created by Vaadin. + * <p> + * For developer it is easy to use: add component to a desired place in you UI + * and add LoginListener to validate form input. Behind the curtain LoginForm + * creates an iframe with static html that browsers detect. + * <p> + * Login form is by default 100% width and height, so consider using it inside a + * sized {@link Panel} or {@link Window}. + * <p> + * Login page html can be overridden by replacing protected getLoginHTML method. + * As the login page is actually an iframe, styles must be handled manually. By + * default component tries to guess the right place for theme css. + * + * @since 5.3 + */ +public class LoginForm extends CustomComponent { + + private String usernameCaption = "Username"; + private String passwordCaption = "Password"; + private String loginButtonCaption = "Login"; + + private Embedded iframe = new Embedded(); + + private ApplicationResource loginPage = new ApplicationResource() { + + @Override + public Application getApplication() { + return LoginForm.this.getApplication(); + } + + @Override + public int getBufferSize() { + return getLoginHTML().length; + } + + @Override + public long getCacheTime() { + return -1; + } + + @Override + public String getFilename() { + return "login"; + } + + @Override + public DownloadStream getStream() { + return new DownloadStream(new ByteArrayInputStream(getLoginHTML()), + getMIMEType(), getFilename()); + } + + @Override + public String getMIMEType() { + return "text/html; charset=utf-8"; + } + }; + + private final RequestHandler requestHandler = new RequestHandler() { + @Override + public boolean handleRequest(Application application, + WrappedRequest request, WrappedResponse response) + throws IOException { + String requestPathInfo = request.getRequestPathInfo(); + if ("/loginHandler".equals(requestPathInfo)) { + response.setCacheTime(-1); + response.setContentType("text/html; charset=utf-8"); + response.getWriter() + .write("<html><body>Login form handled." + + "<script type='text/javascript'>parent.parent.vaadin.forceSync();" + + "</script></body></html>"); + + Map<String, String[]> parameters = request.getParameterMap(); + + HashMap<String, String> params = new HashMap<String, String>(); + // expecting single params + for (Iterator<String> it = parameters.keySet().iterator(); it + .hasNext();) { + String key = it.next(); + String value = (parameters.get(key))[0]; + params.put(key, value); + } + LoginEvent event = new LoginEvent(params); + fireEvent(event); + return true; + } + return false; + } + }; + + public LoginForm() { + iframe.setType(Embedded.TYPE_BROWSER); + iframe.setSizeFull(); + setSizeFull(); + setCompositionRoot(iframe); + addStyleName("v-loginform"); + } + + /** + * Returns byte array containing login page html. If you need to override + * the login html, use the default html as basis. Login page sets its target + * with javascript. + * + * @return byte array containing login page html + */ + protected byte[] getLoginHTML() { + String appUri = getApplication().getURL().toString(); + + try { + return ("<!DOCTYPE html PUBLIC \"-//W3C//DTD " + + "XHTML 1.0 Transitional//EN\" " + + "\"http://www.w3.org/TR/xhtml1/" + + "DTD/xhtml1-transitional.dtd\">\n" + "<html>" + + "<head><script type='text/javascript'>" + + "var setTarget = function() {" + "var uri = '" + + appUri + + "loginHandler" + + "'; var f = document.getElementById('loginf');" + + "document.forms[0].action = uri;document.forms[0].username.focus();};" + + "" + + "var styles = window.parent.document.styleSheets;" + + "for(var j = 0; j < styles.length; j++) {\n" + + "if(styles[j].href) {" + + "var stylesheet = document.createElement('link');\n" + + "stylesheet.setAttribute('rel', 'stylesheet');\n" + + "stylesheet.setAttribute('type', 'text/css');\n" + + "stylesheet.setAttribute('href', styles[j].href);\n" + + "document.getElementsByTagName('head')[0].appendChild(stylesheet);\n" + + "}" + + "}\n" + + "function submitOnEnter(e) { var keycode = e.keyCode || e.which;" + + " if (keycode == 13) {document.forms[0].submit();} } \n" + + "</script>" + + "</head><body onload='setTarget();' style='margin:0;padding:0; background:transparent;' class=\"" + + ApplicationConnection.GENERATED_BODY_CLASSNAME + + "\">" + + "<div class='v-app v-app-loginpage' style=\"background:transparent;\">" + + "<iframe name='logintarget' style='width:0;height:0;" + + "border:0;margin:0;padding:0;'></iframe>" + + "<form id='loginf' target='logintarget' onkeypress=\"submitOnEnter(event)\" method=\"post\">" + + "<div>" + + usernameCaption + + "</div><div >" + + "<input class='v-textfield v-connector' style='display:block;' type='text' name='username'></div>" + + "<div>" + + passwordCaption + + "</div>" + + "<div><input class='v-textfield v-connector' style='display:block;' type='password' name='password'></div>" + + "<div><div onclick=\"document.forms[0].submit();\" tabindex=\"0\" class=\"v-button\" role=\"button\" ><span class=\"v-button-wrap\"><span class=\"v-button-caption\">" + + loginButtonCaption + + "</span></span></div></div></form></div>" + "</body></html>") + .getBytes("UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException("UTF-8 encoding not avalable", e); + } + } + + @Override + public void attach() { + super.attach(); + getApplication().addResource(loginPage); + getApplication().addRequestHandler(requestHandler); + iframe.setSource(loginPage); + } + + @Override + public void detach() { + getApplication().removeResource(loginPage); + getApplication().removeRequestHandler(requestHandler); + + super.detach(); + } + + /** + * This event is sent when login form is submitted. + */ + public class LoginEvent extends Event { + + private Map<String, String> params; + + private LoginEvent(Map<String, String> params) { + super(LoginForm.this); + this.params = params; + } + + /** + * Access method to form values by field names. + * + * @param name + * @return value in given field + */ + public String getLoginParameter(String name) { + if (params.containsKey(name)) { + return params.get(name); + } else { + return null; + } + } + } + + /** + * Login listener is a class capable to listen LoginEvents sent from + * LoginBox + */ + public interface LoginListener extends Serializable { + /** + * This method is fired on each login form post. + * + * @param event + */ + public void onLogin(LoginForm.LoginEvent event); + } + + private static final Method ON_LOGIN_METHOD; + + private static final String UNDEFINED_HEIGHT = "140px"; + private static final String UNDEFINED_WIDTH = "200px"; + + static { + try { + ON_LOGIN_METHOD = LoginListener.class.getDeclaredMethod("onLogin", + new Class[] { LoginEvent.class }); + } catch (final java.lang.NoSuchMethodException e) { + // This should never happen + throw new java.lang.RuntimeException( + "Internal error finding methods in LoginForm"); + } + } + + /** + * Adds LoginListener to handle login logic + * + * @param listener + */ + public void addListener(LoginListener listener) { + addListener(LoginEvent.class, listener, ON_LOGIN_METHOD); + } + + /** + * Removes LoginListener + * + * @param listener + */ + public void removeListener(LoginListener listener) { + removeListener(LoginEvent.class, listener, ON_LOGIN_METHOD); + } + + @Override + public void setWidth(float width, Unit unit) { + super.setWidth(width, unit); + if (iframe != null) { + if (width < 0) { + iframe.setWidth(UNDEFINED_WIDTH); + } else { + iframe.setWidth("100%"); + } + } + } + + @Override + public void setHeight(float height, Unit unit) { + super.setHeight(height, unit); + if (iframe != null) { + if (height < 0) { + iframe.setHeight(UNDEFINED_HEIGHT); + } else { + iframe.setHeight("100%"); + } + } + } + + /** + * Returns the caption for the user name field. + * + * @return String + */ + public String getUsernameCaption() { + return usernameCaption; + } + + /** + * Sets the caption to show for the user name field. The caption cannot be + * changed after the form has been shown to the user. + * + * @param usernameCaption + */ + public void setUsernameCaption(String usernameCaption) { + this.usernameCaption = usernameCaption; + } + + /** + * Returns the caption for the password field. + * + * @return String + */ + public String getPasswordCaption() { + return passwordCaption; + } + + /** + * Sets the caption to show for the password field. The caption cannot be + * changed after the form has been shown to the user. + * + * @param passwordCaption + */ + public void setPasswordCaption(String passwordCaption) { + this.passwordCaption = passwordCaption; + } + + /** + * Returns the caption for the login button. + * + * @return String + */ + public String getLoginButtonCaption() { + return loginButtonCaption; + } + + /** + * Sets the caption (button text) to show for the login button. The caption + * cannot be changed after the form has been shown to the user. + * + * @param loginButtonCaption + */ + public void setLoginButtonCaption(String loginButtonCaption) { + this.loginButtonCaption = loginButtonCaption; + } + +} diff --git a/server/src/com/vaadin/ui/MenuBar.java b/server/src/com/vaadin/ui/MenuBar.java new file mode 100644 index 0000000000..5b5dc13e20 --- /dev/null +++ b/server/src/com/vaadin/ui/MenuBar.java @@ -0,0 +1,890 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.ui; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Stack; + +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.PaintTarget; +import com.vaadin.terminal.Resource; +import com.vaadin.terminal.Vaadin6Component; +import com.vaadin.terminal.gwt.client.ui.menubar.VMenuBar; + +/** + * <p> + * A class representing a horizontal menu bar. The menu can contain MenuItem + * objects, which in turn can contain more MenuBars. These sub-level MenuBars + * are represented as vertical menu. + * </p> + */ +@SuppressWarnings("serial") +public class MenuBar extends AbstractComponent implements Vaadin6Component { + + // Items of the top-level menu + private final List<MenuItem> menuItems; + + // Number of items in this menu + private int numberOfItems = 0; + + private MenuItem moreItem; + + private boolean openRootOnHover; + + private boolean htmlContentAllowed; + + /** Paint (serialise) the component for the client. */ + @Override + public void paintContent(PaintTarget target) throws PaintException { + target.addAttribute(VMenuBar.OPEN_ROOT_MENU_ON_HOWER, openRootOnHover); + + if (isHtmlContentAllowed()) { + target.addAttribute(VMenuBar.HTML_CONTENT_ALLOWED, true); + } + + target.startTag("options"); + + if (getWidth() > -1) { + target.startTag("moreItem"); + target.addAttribute("text", moreItem.getText()); + if (moreItem.getIcon() != null) { + target.addAttribute("icon", moreItem.getIcon()); + } + target.endTag("moreItem"); + } + + target.endTag("options"); + target.startTag("items"); + + // This generates the tree from the contents of the menu + for (MenuItem item : menuItems) { + paintItem(target, item); + } + + target.endTag("items"); + } + + private void paintItem(PaintTarget target, MenuItem item) + throws PaintException { + if (!item.isVisible()) { + return; + } + + target.startTag("item"); + + target.addAttribute("id", item.getId()); + + if (item.getStyleName() != null) { + target.addAttribute(VMenuBar.ATTRIBUTE_ITEM_STYLE, + item.getStyleName()); + } + + if (item.isSeparator()) { + target.addAttribute("separator", true); + } else { + target.addAttribute("text", item.getText()); + + Command command = item.getCommand(); + if (command != null) { + target.addAttribute("command", true); + } + + Resource icon = item.getIcon(); + if (icon != null) { + target.addAttribute(VMenuBar.ATTRIBUTE_ITEM_ICON, icon); + } + + if (!item.isEnabled()) { + target.addAttribute(VMenuBar.ATTRIBUTE_ITEM_DISABLED, true); + } + + String description = item.getDescription(); + if (description != null && description.length() > 0) { + target.addAttribute(VMenuBar.ATTRIBUTE_ITEM_DESCRIPTION, + description); + } + if (item.isCheckable()) { + // if the "checked" attribute is present (either true or false), + // the item is checkable + target.addAttribute(VMenuBar.ATTRIBUTE_CHECKED, + item.isChecked()); + } + if (item.hasChildren()) { + for (MenuItem child : item.getChildren()) { + paintItem(target, child); + } + } + + } + + target.endTag("item"); + } + + /** Deserialize changes received from client. */ + @Override + public void changeVariables(Object source, Map<String, Object> variables) { + Stack<MenuItem> items = new Stack<MenuItem>(); + boolean found = false; + + if (variables.containsKey("clickedId")) { + + Integer clickedId = (Integer) variables.get("clickedId"); + Iterator<MenuItem> itr = getItems().iterator(); + while (itr.hasNext()) { + items.push(itr.next()); + } + + MenuItem tmpItem = null; + + // Go through all the items in the menu + while (!found && !items.empty()) { + tmpItem = items.pop(); + found = (clickedId.intValue() == tmpItem.getId()); + + if (tmpItem.hasChildren()) { + itr = tmpItem.getChildren().iterator(); + while (itr.hasNext()) { + items.push(itr.next()); + } + } + + }// while + + // If we got the clicked item, launch the command. + if (found && tmpItem.isEnabled()) { + if (tmpItem.isCheckable()) { + tmpItem.setChecked(!tmpItem.isChecked()); + } + if (null != tmpItem.getCommand()) { + tmpItem.getCommand().menuSelected(tmpItem); + } + } + }// if + }// changeVariables + + /** + * Constructs an empty, horizontal menu + */ + public MenuBar() { + menuItems = new ArrayList<MenuItem>(); + setMoreMenuItem(null); + } + + /** + * Add a new item to the menu bar. Command can be null, but a caption must + * be given. + * + * @param caption + * the text for the menu item + * @param command + * the command for the menu item + * @throws IllegalArgumentException + */ + public MenuBar.MenuItem addItem(String caption, MenuBar.Command command) { + return addItem(caption, null, command); + } + + /** + * Add a new item to the menu bar. Icon and command can be null, but a + * caption must be given. + * + * @param caption + * the text for the menu item + * @param icon + * the icon for the menu item + * @param command + * the command for the menu item + * @throws IllegalArgumentException + */ + public MenuBar.MenuItem addItem(String caption, Resource icon, + MenuBar.Command command) { + if (caption == null) { + throw new IllegalArgumentException("caption cannot be null"); + } + MenuItem newItem = new MenuItem(caption, icon, command); + menuItems.add(newItem); + requestRepaint(); + + return newItem; + + } + + /** + * Add an item before some item. If the given item does not exist the item + * is added at the end of the menu. Icon and command can be null, but a + * caption must be given. + * + * @param caption + * the text for the menu item + * @param icon + * the icon for the menu item + * @param command + * the command for the menu item + * @param itemToAddBefore + * the item that will be after the new item + * @throws IllegalArgumentException + */ + public MenuBar.MenuItem addItemBefore(String caption, Resource icon, + MenuBar.Command command, MenuBar.MenuItem itemToAddBefore) { + if (caption == null) { + throw new IllegalArgumentException("caption cannot be null"); + } + + MenuItem newItem = new MenuItem(caption, icon, command); + if (menuItems.contains(itemToAddBefore)) { + int index = menuItems.indexOf(itemToAddBefore); + menuItems.add(index, newItem); + + } else { + menuItems.add(newItem); + } + + requestRepaint(); + + return newItem; + } + + /** + * Returns a list with all the MenuItem objects in the menu bar + * + * @return a list containing the MenuItem objects in the menu bar + */ + public List<MenuItem> getItems() { + return menuItems; + } + + /** + * Remove first occurrence the specified item from the main menu + * + * @param item + * The item to be removed + */ + public void removeItem(MenuBar.MenuItem item) { + if (item != null) { + menuItems.remove(item); + } + requestRepaint(); + } + + /** + * Empty the menu bar + */ + public void removeItems() { + menuItems.clear(); + requestRepaint(); + } + + /** + * Returns the size of the menu. + * + * @return The size of the menu + */ + public int getSize() { + return menuItems.size(); + } + + /** + * Set the item that is used when collapsing the top level menu. All + * "overflowing" items will be added below this. The item command will be + * ignored. If set to null, the default item with a downwards arrow is used. + * + * The item command (if specified) is ignored. + * + * @param item + */ + public void setMoreMenuItem(MenuItem item) { + if (item != null) { + moreItem = item; + } else { + moreItem = new MenuItem("", null, null); + } + requestRepaint(); + } + + /** + * Get the MenuItem used as the collapse menu item. + * + * @return + */ + public MenuItem getMoreMenuItem() { + return moreItem; + } + + /** + * Using this method menubar can be put into a special mode where top level + * menus opens without clicking on the menu, but automatically when mouse + * cursor is moved over the menu. In this mode the menu also closes itself + * if the mouse is moved out of the opened menu. + * <p> + * Note, that on touch devices the menu still opens on a click event. + * + * @param autoOpenTopLevelMenu + * true if menus should be opened without click, the default is + * false + */ + public void setAutoOpen(boolean autoOpenTopLevelMenu) { + if (autoOpenTopLevelMenu != openRootOnHover) { + openRootOnHover = autoOpenTopLevelMenu; + requestRepaint(); + } + } + + /** + * Detects whether the menubar is in a mode where top level menus are + * automatically opened when the mouse cursor is moved over the menu. + * Normally root menu opens only by clicking on the menu. Submenus always + * open automatically. + * + * @return true if the root menus open without click, the default is false + */ + public boolean isAutoOpen() { + return openRootOnHover; + } + + /** + * 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) { + this.htmlContentAllowed = htmlContentAllowed; + requestRepaint(); + } + + /** + * Checks whether item 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 htmlContentAllowed; + } + + /** + * This interface contains the layer for menu commands of the + * {@link com.vaadin.ui.MenuBar} class. It's method will fire when the user + * clicks on the containing {@link com.vaadin.ui.MenuBar.MenuItem}. The + * selected item is given as an argument. + */ + public interface Command extends Serializable { + public void menuSelected(MenuBar.MenuItem selectedItem); + } + + /** + * A composite class for menu items and sub-menus. You can set commands to + * be fired on user click by implementing the + * {@link com.vaadin.ui.MenuBar.Command} interface. You can also add + * multiple MenuItems to a MenuItem and create a sub-menu. + * + */ + public class MenuItem implements Serializable { + + /** Private members * */ + private final int itsId; + private Command itsCommand; + private String itsText; + private List<MenuItem> itsChildren; + private Resource itsIcon; + private MenuItem itsParent; + private boolean enabled = true; + private boolean visible = true; + private boolean isSeparator = false; + private String styleName; + private String description; + private boolean checkable = false; + private boolean checked = false; + + /** + * Constructs a new menu item that can optionally have an icon and a + * command associated with it. Icon and command can be null, but a + * caption must be given. + * + * @param text + * The text associated with the command + * @param command + * The command to be fired + * @throws IllegalArgumentException + */ + public MenuItem(String caption, Resource icon, MenuBar.Command command) { + if (caption == null) { + throw new IllegalArgumentException("caption cannot be null"); + } + itsId = ++numberOfItems; + itsText = caption; + itsIcon = icon; + itsCommand = command; + } + + /** + * Checks if the item has children (if it is a sub-menu). + * + * @return True if this item has children + */ + public boolean hasChildren() { + return !isSeparator() && itsChildren != null; + } + + /** + * Adds a separator to this menu. A separator is a way to visually group + * items in a menu, to make it easier for users to find what they are + * looking for in the menu. + * + * @author Jouni Koivuviita / Vaadin Ltd. + * @since 6.2.0 + */ + public MenuBar.MenuItem addSeparator() { + MenuItem item = addItem("", null, null); + item.setSeparator(true); + return item; + } + + public MenuBar.MenuItem addSeparatorBefore(MenuItem itemToAddBefore) { + MenuItem item = addItemBefore("", null, null, itemToAddBefore); + item.setSeparator(true); + return item; + } + + /** + * Add a new item inside this item, thus creating a sub-menu. Command + * can be null, but a caption must be given. + * + * @param caption + * the text for the menu item + * @param command + * the command for the menu item + */ + public MenuBar.MenuItem addItem(String caption, MenuBar.Command command) { + return addItem(caption, null, command); + } + + /** + * Add a new item inside this item, thus creating a sub-menu. Icon and + * command can be null, but a caption must be given. + * + * @param caption + * the text for the menu item + * @param icon + * the icon for the menu item + * @param command + * the command for the menu item + * @throws IllegalStateException + * If the item is checkable and thus cannot have children. + */ + public MenuBar.MenuItem addItem(String caption, Resource icon, + MenuBar.Command command) throws IllegalStateException { + if (isSeparator()) { + throw new UnsupportedOperationException( + "Cannot add items to a separator"); + } + if (isCheckable()) { + throw new IllegalStateException( + "A checkable item cannot have children"); + } + if (caption == null) { + throw new IllegalArgumentException("Caption cannot be null"); + } + + if (itsChildren == null) { + itsChildren = new ArrayList<MenuItem>(); + } + + MenuItem newItem = new MenuItem(caption, icon, command); + + // The only place where the parent is set + newItem.setParent(this); + itsChildren.add(newItem); + + requestRepaint(); + + return newItem; + } + + /** + * Add an item before some item. If the given item does not exist the + * item is added at the end of the menu. Icon and command can be null, + * but a caption must be given. + * + * @param caption + * the text for the menu item + * @param icon + * the icon for the menu item + * @param command + * the command for the menu item + * @param itemToAddBefore + * the item that will be after the new item + * @throws IllegalStateException + * If the item is checkable and thus cannot have children. + */ + public MenuBar.MenuItem addItemBefore(String caption, Resource icon, + MenuBar.Command command, MenuBar.MenuItem itemToAddBefore) + throws IllegalStateException { + if (isCheckable()) { + throw new IllegalStateException( + "A checkable item cannot have children"); + } + MenuItem newItem = null; + + if (hasChildren() && itsChildren.contains(itemToAddBefore)) { + int index = itsChildren.indexOf(itemToAddBefore); + newItem = new MenuItem(caption, icon, command); + newItem.setParent(this); + itsChildren.add(index, newItem); + } else { + newItem = addItem(caption, icon, command); + } + + requestRepaint(); + + return newItem; + } + + /** + * For the associated command. + * + * @return The associated command, or null if there is none + */ + public Command getCommand() { + return itsCommand; + } + + /** + * Gets the objects icon. + * + * @return The icon of the item, null if the item doesn't have an icon + */ + public Resource getIcon() { + return itsIcon; + } + + /** + * For the containing item. This will return null if the item is in the + * top-level menu bar. + * + * @return The containing {@link com.vaadin.ui.MenuBar.MenuItem} , or + * null if there is none + */ + public MenuBar.MenuItem getParent() { + return itsParent; + } + + /** + * This will return the children of this item or null if there are none. + * + * @return List of children items, or null if there are none + */ + public List<MenuItem> getChildren() { + return itsChildren; + } + + /** + * Gets the objects text + * + * @return The text + */ + public java.lang.String getText() { + return itsText; + } + + /** + * Returns the number of children. + * + * @return The number of child items + */ + public int getSize() { + if (itsChildren != null) { + return itsChildren.size(); + } + return -1; + } + + /** + * Get the unique identifier for this item. + * + * @return The id of this item + */ + public int getId() { + return itsId; + } + + /** + * Set the command for this item. Set null to remove. + * + * @param command + * The MenuCommand of this item + */ + public void setCommand(MenuBar.Command command) { + itsCommand = command; + } + + /** + * Sets the icon. Set null to remove. + * + * @param icon + * The icon for this item + */ + public void setIcon(Resource icon) { + itsIcon = icon; + requestRepaint(); + } + + /** + * Set the text of this object. + * + * @param text + * Text for this object + */ + public void setText(java.lang.String text) { + if (text != null) { + itsText = text; + } + requestRepaint(); + } + + /** + * Remove the first occurrence of the item. + * + * @param item + * The item to be removed + */ + public void removeChild(MenuBar.MenuItem item) { + if (item != null && itsChildren != null) { + itsChildren.remove(item); + if (itsChildren.isEmpty()) { + itsChildren = null; + } + requestRepaint(); + } + } + + /** + * Empty the list of children items. + */ + public void removeChildren() { + if (itsChildren != null) { + itsChildren.clear(); + itsChildren = null; + requestRepaint(); + } + } + + /** + * Set the parent of this item. This is called by the addItem method. + * + * @param parent + * The parent item + */ + protected void setParent(MenuBar.MenuItem parent) { + itsParent = parent; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + requestRepaint(); + } + + public boolean isEnabled() { + return enabled; + } + + public void setVisible(boolean visible) { + this.visible = visible; + requestRepaint(); + } + + public boolean isVisible() { + return visible; + } + + private void setSeparator(boolean isSeparator) { + this.isSeparator = isSeparator; + requestRepaint(); + } + + public boolean isSeparator() { + return isSeparator; + } + + public void setStyleName(String styleName) { + this.styleName = styleName; + requestRepaint(); + } + + public String getStyleName() { + return styleName; + } + + /** + * Sets the items's description. See {@link #getDescription()} for more + * information on what the description is. This method will trigger a + * {@link RepaintRequestEvent}. + * + * @param description + * the new description string for the component. + */ + public void setDescription(String description) { + this.description = description; + requestRepaint(); + } + + /** + * <p> + * Gets the items's description. The description can be used to briefly + * describe the state of the item to the user. The description string + * may contain certain XML tags: + * </p> + * + * <p> + * <table border=1> + * <tr> + * <td width=120><b>Tag</b></td> + * <td width=120><b>Description</b></td> + * <td width=120><b>Example</b></td> + * </tr> + * <tr> + * <td><b></td> + * <td>bold</td> + * <td><b>bold text</b></td> + * </tr> + * <tr> + * <td><i></td> + * <td>italic</td> + * <td><i>italic text</i></td> + * </tr> + * <tr> + * <td><u></td> + * <td>underlined</td> + * <td><u>underlined text</u></td> + * </tr> + * <tr> + * <td><br></td> + * <td>linebreak</td> + * <td>N/A</td> + * </tr> + * <tr> + * <td><ul><br> + * <li>item1<br> + * <li>item1<br> + * </ul></td> + * <td>item list</td> + * <td> + * <ul> + * <li>item1 + * <li>item2 + * </ul> + * </td> + * </tr> + * </table> + * </p> + * + * <p> + * These tags may be nested. + * </p> + * + * @return item's description <code>String</code> + */ + public String getDescription() { + return description; + } + + /** + * Gets the checkable state of the item - whether the item has checked + * and unchecked states. If an item is checkable its checked state (as + * returned by {@link #isChecked()}) is indicated in the UI. + * + * <p> + * An item is not checkable by default. + * </p> + * + * @return true if the item is checkable, false otherwise + * @since 6.6.2 + */ + public boolean isCheckable() { + return checkable; + } + + /** + * Sets the checkable state of the item. If an item is checkable its + * checked state (as returned by {@link #isChecked()}) is indicated in + * the UI. + * + * <p> + * An item is not checkable by default. + * </p> + * + * <p> + * Items with sub items cannot be checkable. + * </p> + * + * @param checkable + * true if the item should be checkable, false otherwise + * @throws IllegalStateException + * If the item has children + * @since 6.6.2 + */ + public void setCheckable(boolean checkable) + throws IllegalStateException { + if (hasChildren()) { + throw new IllegalStateException( + "A menu item with children cannot be checkable"); + } + this.checkable = checkable; + requestRepaint(); + } + + /** + * Gets the checked state of the item (checked or unchecked). Only used + * if the item is checkable (as indicated by {@link #isCheckable()}). + * The checked state is indicated in the UI with the item, if the item + * is checkable. + * + * <p> + * An item is not checked by default. + * </p> + * + * <p> + * The CSS style corresponding to the checked state is "-checked". + * </p> + * + * @return true if the item is checked, false otherwise + * @since 6.6.2 + */ + public boolean isChecked() { + return checked; + } + + /** + * Sets the checked state of the item. Only used if the item is + * checkable (indicated by {@link #isCheckable()}). The checked state is + * indicated in the UI with the item, if the item is checkable. + * + * <p> + * An item is not checked by default. + * </p> + * + * <p> + * The CSS style corresponding to the checked state is "-checked". + * </p> + * + * @return true if the item is checked, false otherwise + * @since 6.6.2 + */ + public void setChecked(boolean checked) { + this.checked = checked; + requestRepaint(); + } + + }// class MenuItem + +}// class MenuBar diff --git a/server/src/com/vaadin/ui/NativeButton.java b/server/src/com/vaadin/ui/NativeButton.java new file mode 100644 index 0000000000..6eb4379261 --- /dev/null +++ b/server/src/com/vaadin/ui/NativeButton.java @@ -0,0 +1,21 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.ui; + +@SuppressWarnings("serial") +public class NativeButton extends Button { + + public NativeButton() { + super(); + } + + public NativeButton(String caption) { + super(caption); + } + + public NativeButton(String caption, ClickListener listener) { + super(caption, listener); + } + +} diff --git a/server/src/com/vaadin/ui/NativeSelect.java b/server/src/com/vaadin/ui/NativeSelect.java new file mode 100644 index 0000000000..1f85f57c97 --- /dev/null +++ b/server/src/com/vaadin/ui/NativeSelect.java @@ -0,0 +1,91 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.util.Collection; + +import com.vaadin.data.Container; +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.PaintTarget; + +/** + * 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 { + + // width in characters, mimics TextField + private int columns = 0; + + public NativeSelect() { + super(); + } + + public NativeSelect(String caption, Collection<?> options) { + super(caption, options); + } + + public NativeSelect(String caption, Container dataSource) { + super(caption, dataSource); + } + + public NativeSelect(String caption) { + super(caption); + } + + /** + * Sets the number of columns in the editor. If the number of columns is set + * 0, the actual number of displayed columns is determined implicitly by the + * adapter. + * + * @param columns + * the number of columns to set. + */ + public void setColumns(int columns) { + if (columns < 0) { + columns = 0; + } + if (this.columns != columns) { + this.columns = columns; + requestRepaint(); + } + } + + public int getColumns() { + return columns; + } + + @Override + public void paintContent(PaintTarget target) throws PaintException { + target.addAttribute("type", "native"); + // Adds the number of columns + if (columns != 0) { + target.addAttribute("cols", columns); + } + + super.paintContent(target); + } + + @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"); + } + } + +} diff --git a/server/src/com/vaadin/ui/Notification.java b/server/src/com/vaadin/ui/Notification.java new file mode 100644 index 0000000000..502e5ff788 --- /dev/null +++ b/server/src/com/vaadin/ui/Notification.java @@ -0,0 +1,367 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.io.Serializable; + +import com.vaadin.terminal.Page; +import com.vaadin.terminal.Resource; + +/** + * A notification message, used to display temporary messages to the user - for + * example "Document saved", or "Save failed". + * <p> + * The notification message can consist of several parts: caption, description + * and icon. It is usually used with only caption - one should be wary of + * filling the notification with too much information. + * </p> + * <p> + * The notification message tries to be as unobtrusive as possible, while still + * drawing needed attention. There are several basic types of messages that can + * be used in different situations: + * <ul> + * <li>TYPE_HUMANIZED_MESSAGE fades away quickly as soon as the user uses the + * mouse or types something. It can be used to show fairly unimportant messages, + * such as feedback that an operation succeeded ("Document Saved") - the kind of + * messages the user ignores once the application is familiar.</li> + * <li>TYPE_WARNING_MESSAGE is shown for a short while after the user uses the + * mouse or types something. It's default style is also more noticeable than the + * humanized message. It can be used for messages that do not contain a lot of + * important information, but should be noticed by the user. Despite the name, + * it does not have to be a warning, but can be used instead of the humanized + * message whenever you want to make the message a little more noticeable.</li> + * <li>TYPE_ERROR_MESSAGE requires to user to click it before disappearing, and + * can be used for critical messages.</li> + * <li>TYPE_TRAY_NOTIFICATION is shown for a while in the lower left corner of + * the window, and can be used for "convenience notifications" that do not have + * to be noticed immediately, and should not interfere with the current task - + * for instance to show "You have a new message in your inbox" while the user is + * working in some other area of the application.</li> + * </ul> + * </p> + * <p> + * In addition to the basic pre-configured types, a Notification can also be + * configured to show up in a custom position, for a specified time (or until + * clicked), and with a custom stylename. An icon can also be added. + * </p> + * + */ +public class Notification implements Serializable { + public static final int TYPE_HUMANIZED_MESSAGE = 1; + public static final int TYPE_WARNING_MESSAGE = 2; + public static final int TYPE_ERROR_MESSAGE = 3; + public static final int TYPE_TRAY_NOTIFICATION = 4; + + public static final int POSITION_CENTERED = 1; + public static final int POSITION_CENTERED_TOP = 2; + public static final int POSITION_CENTERED_BOTTOM = 3; + public static final int POSITION_TOP_LEFT = 4; + public static final int POSITION_TOP_RIGHT = 5; + public static final int POSITION_BOTTOM_LEFT = 6; + public static final int POSITION_BOTTOM_RIGHT = 7; + + public static final int DELAY_FOREVER = -1; + public static final int DELAY_NONE = 0; + + private String caption; + private String description; + private Resource icon; + private int position = POSITION_CENTERED; + private int delayMsec = 0; + private String styleName; + private boolean htmlContentAllowed; + + /** + * Creates a "humanized" notification message. + * + * The caption is rendered as plain text with HTML automatically escaped. + * + * @param caption + * The message to show + */ + public Notification(String caption) { + this(caption, null, TYPE_HUMANIZED_MESSAGE); + } + + /** + * Creates a notification message of the specified type. + * + * The caption is rendered as plain text with HTML automatically escaped. + * + * @param caption + * The message to show + * @param type + * The type of message + */ + public Notification(String caption, int type) { + this(caption, null, type); + } + + /** + * Creates a "humanized" notification message with a bigger caption and + * smaller description. + * + * The caption and description are rendered as plain text with HTML + * automatically escaped. + * + * @param caption + * The message caption + * @param description + * The message description + */ + public Notification(String caption, String description) { + this(caption, description, TYPE_HUMANIZED_MESSAGE); + } + + /** + * Creates a notification message of the specified type, with a bigger + * caption and smaller description. + * + * The caption and description are rendered as plain text with HTML + * automatically escaped. + * + * @param caption + * The message caption + * @param description + * The message description + * @param type + * The type of message + */ + public Notification(String caption, String description, int type) { + this(caption, description, type, false); + } + + /** + * Creates a notification message of the specified type, with a bigger + * caption and smaller description. + * + * Care should be taken to to avoid XSS vulnerabilities if html is allowed. + * + * @param caption + * The message caption + * @param description + * The message description + * @param type + * The type of message + * @param htmlContentAllowed + * Whether html in the caption and description should be + * displayed as html or as plain text + */ + public Notification(String caption, String description, int type, + boolean htmlContentAllowed) { + this.caption = caption; + this.description = description; + this.htmlContentAllowed = htmlContentAllowed; + setType(type); + } + + private void setType(int type) { + switch (type) { + case TYPE_WARNING_MESSAGE: + delayMsec = 1500; + styleName = "warning"; + break; + case TYPE_ERROR_MESSAGE: + delayMsec = -1; + styleName = "error"; + break; + case TYPE_TRAY_NOTIFICATION: + delayMsec = 3000; + position = POSITION_BOTTOM_RIGHT; + styleName = "tray"; + + case TYPE_HUMANIZED_MESSAGE: + default: + break; + } + + } + + /** + * Gets the caption part of the notification message. + * + * @return The message caption + */ + public String getCaption() { + return caption; + } + + /** + * Sets the caption part of the notification message + * + * @param caption + * The message caption + */ + public void setCaption(String caption) { + this.caption = caption; + } + + /** + * Gets the description part of the notification message. + * + * @return The message description. + */ + public String getDescription() { + return description; + } + + /** + * Sets the description part of the notification message. + * + * @param description + */ + public void setDescription(String description) { + this.description = description; + } + + /** + * Gets the position of the notification message. + * + * @return The position + */ + public int getPosition() { + return position; + } + + /** + * Sets the position of the notification message. + * + * @param position + * The desired notification position + */ + public void setPosition(int position) { + this.position = position; + } + + /** + * Gets the icon part of the notification message. + * + * @return The message icon + */ + public Resource getIcon() { + return icon; + } + + /** + * Sets the icon part of the notification message. + * + * @param icon + * The desired message icon + */ + public void setIcon(Resource icon) { + this.icon = icon; + } + + /** + * Gets the delay before the notification disappears. + * + * @return the delay in msec, -1 indicates the message has to be clicked. + */ + public int getDelayMsec() { + return delayMsec; + } + + /** + * Sets the delay before the notification disappears. + * + * @param delayMsec + * the desired delay in msec, -1 to require the user to click the + * message + */ + public void setDelayMsec(int delayMsec) { + this.delayMsec = delayMsec; + } + + /** + * Sets the style name for the notification message. + * + * @param styleName + * The desired style name. + */ + public void setStyleName(String styleName) { + this.styleName = styleName; + } + + /** + * Gets the style name for the notification message. + * + * @return + */ + public String getStyleName() { + return styleName; + } + + /** + * Sets whether html is allowed in the caption and description. If set to + * true, the texts are passed to the browser as html and the developer is + * responsible for ensuring no harmful html is used. If set to false, the + * texts are passed to the browser as plain text. + * + * @param htmlContentAllowed + * true if the texts are used as html, false if used as plain + * text + */ + public void setHtmlContentAllowed(boolean htmlContentAllowed) { + this.htmlContentAllowed = htmlContentAllowed; + } + + /** + * Checks whether caption and description are interpreted as html or plain + * text. + * + * @return true if the texts are used as html, false if used as plain text + * @see #setHtmlContentAllowed(boolean) + */ + public boolean isHtmlContentAllowed() { + return htmlContentAllowed; + } + + /** + * Shows this notification on a Page. + * + * @param page + * The page on which the notification should be shown + */ + public void show(Page page) { + // TODO Can avoid deprecated API when Notification extends Extension + page.showNotification(this); + } + + /** + * Shows a notification message on the middle of the current page. The + * message automatically disappears ("humanized message"). + * + * The caption is rendered as plain text with HTML automatically escaped. + * + * @see #Notification(String) + * @see #show(Page) + * + * @param caption + * The message + */ + public static void show(String caption) { + new Notification(caption).show(Page.getCurrent()); + } + + /** + * Shows a notification message the current page. The position and behavior + * of the message depends on the type, which is one of the basic types + * defined in {@link Notification}, for instance + * Notification.TYPE_WARNING_MESSAGE. + * + * The caption is rendered as plain text with HTML automatically escaped. + * + * @see #Notification(String, int) + * @see #show(Page) + * + * @param caption + * The message + * @param type + * The message type + */ + public static void show(String caption, int type) { + new Notification(caption, type).show(Page.getCurrent()); + } +}
\ No newline at end of file diff --git a/server/src/com/vaadin/ui/OptionGroup.java b/server/src/com/vaadin/ui/OptionGroup.java new file mode 100644 index 0000000000..e3bcdd61b7 --- /dev/null +++ b/server/src/com/vaadin/ui/OptionGroup.java @@ -0,0 +1,203 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import com.vaadin.data.Container; +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.terminal.PaintException; +import com.vaadin.terminal.PaintTarget; +import com.vaadin.terminal.gwt.client.ui.optiongroup.VOptionGroup; + +/** + * 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>(); + private boolean htmlContentAllowed = false; + + 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 + public void paintContent(PaintTarget target) throws PaintException { + target.addAttribute("type", "optiongroup"); + if (isHtmlContentAllowed()) { + target.addAttribute(VOptionGroup.HTML_CONTENT_ALLOWED, true); + } + super.paintContent(target); + } + + @Override + protected void paintItem(PaintTarget target, Object itemId) + throws PaintException { + super.paintItem(target, itemId); + if (!isItemEnabled(itemId)) { + target.addAttribute(VOptionGroup.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 addListener(BlurListener listener) { + addListener(BlurEvent.EVENT_ID, BlurEvent.class, listener, + BlurListener.blurMethod); + } + + @Override + public void removeListener(BlurListener listener) { + removeListener(BlurEvent.EVENT_ID, BlurEvent.class, listener); + } + + @Override + public void addListener(FocusListener listener) { + addListener(FocusEvent.EVENT_ID, FocusEvent.class, listener, + FocusListener.focusMethod); + } + + @Override + public void removeListener(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)) { + requestRepaint(); + return; + } + } + for (Object itemId : newValueSet) { + if (!isItemEnabled(itemId) + && !currentValueSet.contains(itemId)) { + requestRepaint(); + return; + } + } + } else { + if (newValue == null) { + newValue = getNullSelectionItemId(); + } + if (!isItemEnabled(newValue)) { + requestRepaint(); + 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); + } + requestRepaint(); + } + } + + /** + * 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) { + this.htmlContentAllowed = htmlContentAllowed; + requestRepaint(); + } + + /** + * 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 htmlContentAllowed; + } +} diff --git a/server/src/com/vaadin/ui/Panel.java b/server/src/com/vaadin/ui/Panel.java new file mode 100644 index 0000000000..3c26b73f09 --- /dev/null +++ b/server/src/com/vaadin/ui/Panel.java @@ -0,0 +1,486 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.util.Collections; +import java.util.Iterator; +import java.util.Map; + +import com.vaadin.event.Action; +import com.vaadin.event.Action.Handler; +import com.vaadin.event.ActionManager; +import com.vaadin.event.MouseEvents.ClickEvent; +import com.vaadin.event.MouseEvents.ClickListener; +import com.vaadin.shared.MouseEventDetails; +import com.vaadin.shared.ui.panel.PanelServerRpc; +import com.vaadin.shared.ui.panel.PanelState; +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.PaintTarget; +import com.vaadin.terminal.Scrollable; +import com.vaadin.terminal.Vaadin6Component; +import com.vaadin.terminal.gwt.client.ui.ClickEventHandler; +import com.vaadin.ui.Component.Focusable; + +/** + * Panel - a simple single component container. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +@SuppressWarnings("serial") +public class Panel extends AbstractComponentContainer implements Scrollable, + ComponentContainer.ComponentAttachListener, + ComponentContainer.ComponentDetachListener, Action.Notifier, Focusable, + Vaadin6Component { + + /** + * Content of the panel. + */ + private ComponentContainer content; + + /** + * Keeps track of the Actions added to this component, and manages the + * painting and handling as well. + */ + protected ActionManager actionManager; + + private PanelServerRpc rpc = new PanelServerRpc() { + @Override + public void click(MouseEventDetails mouseDetails) { + fireEvent(new ClickEvent(Panel.this, mouseDetails)); + } + }; + + /** + * Creates a new empty panel. A VerticalLayout is used as content. + */ + public Panel() { + this((ComponentContainer) null); + } + + /** + * Creates a new empty panel which contains the given content. The content + * cannot be null. + * + * @param content + * the content for the panel. + */ + public Panel(ComponentContainer content) { + registerRpc(rpc); + setContent(content); + setWidth(100, Unit.PERCENTAGE); + getState().setTabIndex(-1); + } + + /** + * Creates a new empty panel with caption. Default layout is used. + * + * @param caption + * the caption used in the panel (HTML/XHTML). + */ + public Panel(String caption) { + this(caption, null); + } + + /** + * Creates a new empty panel with the given caption and content. + * + * @param caption + * the caption of the panel (HTML/XHTML). + * @param content + * the content used in the panel. + */ + public Panel(String caption, ComponentContainer content) { + this(content); + setCaption(caption); + } + + /** + * Sets the caption of the panel. + * + * Note that the caption is interpreted as HTML/XHTML and therefore care + * should be taken not to enable HTML injection and XSS attacks using panel + * captions. This behavior may change in future versions. + * + * @see AbstractComponent#setCaption(String) + */ + @Override + public void setCaption(String caption) { + super.setCaption(caption); + } + + /** + * Returns the content of the Panel. + * + * @return + */ + public ComponentContainer getContent() { + return content; + } + + /** + * + * Set the content of the Panel. If null is given as the new content then a + * layout is automatically created and set as the content. + * + * @param content + * The new content + */ + public void setContent(ComponentContainer newContent) { + + // If the content is null we create the default content + if (newContent == null) { + newContent = createDefaultContent(); + } + + // if (newContent == null) { + // throw new IllegalArgumentException("Content cannot be null"); + // } + + if (newContent == content) { + // don't set the same content twice + return; + } + + // detach old content if present + if (content != null) { + content.setParent(null); + content.removeListener((ComponentContainer.ComponentAttachListener) this); + content.removeListener((ComponentContainer.ComponentDetachListener) this); + } + + // Sets the panel to be parent for the content + newContent.setParent(this); + + // Sets the new content + content = newContent; + + // Adds the event listeners for new content + newContent + .addListener((ComponentContainer.ComponentAttachListener) this); + newContent + .addListener((ComponentContainer.ComponentDetachListener) this); + + content = newContent; + requestRepaint(); + } + + /** + * Create a ComponentContainer which is added by default to the Panel if + * user does not specify any content. + * + * @return + */ + private ComponentContainer createDefaultContent() { + VerticalLayout layout = new VerticalLayout(); + // Force margins by default + layout.setMargin(true); + return layout; + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.terminal.Vaadin6Component#paintContent(com.vaadin.terminal + * .PaintTarget) + */ + @Override + public void paintContent(PaintTarget target) throws PaintException { + if (actionManager != null) { + actionManager.paintActions(null, target); + } + } + + /** + * Adds the component into this container. + * + * @param c + * the component to be added. + * @see com.vaadin.ui.AbstractComponentContainer#addComponent(com.vaadin.ui.Component) + */ + @Override + public void addComponent(Component c) { + content.addComponent(c); + // No repaint request is made as we except the underlying container to + // request repaints + } + + /** + * Removes the component from this container. + * + * @param c + * The component to be removed. + * @see com.vaadin.ui.AbstractComponentContainer#removeComponent(com.vaadin.ui.Component) + */ + @Override + public void removeComponent(Component c) { + content.removeComponent(c); + // No repaint request is made as we except the underlying container to + // request repaints + } + + /** + * Gets the component container iterator for going through all the + * components in the container. + * + * @return the Iterator of the components inside the container. + * @see com.vaadin.ui.ComponentContainer#getComponentIterator() + */ + @Override + public Iterator<Component> getComponentIterator() { + return Collections.singleton((Component) content).iterator(); + } + + /** + * Called when one or more variables handled by the implementing class are + * changed. + * + * @see com.vaadin.terminal.VariableOwner#changeVariables(Object, Map) + */ + @Override + @SuppressWarnings("unchecked") + public void changeVariables(Object source, Map<String, Object> variables) { + // Get new size + final Integer newWidth = (Integer) variables.get("width"); + final Integer newHeight = (Integer) variables.get("height"); + if (newWidth != null && newWidth.intValue() != getWidth()) { + setWidth(newWidth.intValue(), UNITS_PIXELS); + } + if (newHeight != null && newHeight.intValue() != getHeight()) { + setHeight(newHeight.intValue(), UNITS_PIXELS); + } + + // Scrolling + final Integer newScrollX = (Integer) variables.get("scrollLeft"); + final Integer newScrollY = (Integer) variables.get("scrollTop"); + if (newScrollX != null && newScrollX.intValue() != getScrollLeft()) { + // set internally, not to fire request repaint + getState().setScrollLeft(newScrollX.intValue()); + } + if (newScrollY != null && newScrollY.intValue() != getScrollTop()) { + // set internally, not to fire request repaint + getState().setScrollTop(newScrollY.intValue()); + } + + // Actions + if (actionManager != null) { + actionManager.handleActions(variables, this); + } + + } + + /* Scrolling functionality */ + + /* + * (non-Javadoc) + * + * @see com.vaadin.terminal.Scrollable#setScrollable(boolean) + */ + @Override + public int getScrollLeft() { + return getState().getScrollLeft(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.terminal.Scrollable#setScrollable(boolean) + */ + @Override + public int getScrollTop() { + return getState().getScrollTop(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.terminal.Scrollable#setScrollLeft(int) + */ + @Override + public void setScrollLeft(int scrollLeft) { + if (scrollLeft < 0) { + throw new IllegalArgumentException( + "Scroll offset must be at least 0"); + } + getState().setScrollLeft(scrollLeft); + requestRepaint(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.terminal.Scrollable#setScrollTop(int) + */ + @Override + public void setScrollTop(int scrollTop) { + if (scrollTop < 0) { + throw new IllegalArgumentException( + "Scroll offset must be at least 0"); + } + getState().setScrollTop(scrollTop); + requestRepaint(); + } + + /* Documented in superclass */ + @Override + public void replaceComponent(Component oldComponent, Component newComponent) { + + content.replaceComponent(oldComponent, newComponent); + } + + /** + * A new component is attached to container. + * + * @see com.vaadin.ui.ComponentContainer.ComponentAttachListener#componentAttachedToContainer(com.vaadin.ui.ComponentContainer.ComponentAttachEvent) + */ + @Override + public void componentAttachedToContainer(ComponentAttachEvent event) { + if (event.getContainer() == content) { + fireComponentAttachEvent(event.getAttachedComponent()); + } + } + + /** + * A component has been detached from container. + * + * @see com.vaadin.ui.ComponentContainer.ComponentDetachListener#componentDetachedFromContainer(com.vaadin.ui.ComponentContainer.ComponentDetachEvent) + */ + @Override + public void componentDetachedFromContainer(ComponentDetachEvent event) { + if (event.getContainer() == content) { + fireComponentDetachEvent(event.getDetachedComponent()); + } + } + + /** + * Removes all components from this container. + * + * @see com.vaadin.ui.ComponentContainer#removeAllComponents() + */ + @Override + public void removeAllComponents() { + content.removeAllComponents(); + } + + /* + * ACTIONS + */ + @Override + protected ActionManager getActionManager() { + if (actionManager == null) { + actionManager = new ActionManager(this); + } + return actionManager; + } + + @Override + public <T extends Action & com.vaadin.event.Action.Listener> void addAction( + T action) { + getActionManager().addAction(action); + } + + @Override + public <T extends Action & com.vaadin.event.Action.Listener> void removeAction( + T action) { + if (actionManager != null) { + actionManager.removeAction(action); + } + } + + @Override + public void addActionHandler(Handler actionHandler) { + getActionManager().addActionHandler(actionHandler); + } + + @Override + public void removeActionHandler(Handler actionHandler) { + if (actionManager != null) { + actionManager.removeActionHandler(actionHandler); + } + } + + /** + * Removes all action handlers + */ + public void removeAllActionHandlers() { + if (actionManager != null) { + actionManager.removeAllActionHandlers(); + } + } + + /** + * Add a click listener to the Panel. The listener is called whenever the + * user clicks inside the Panel. Also when the click targets a component + * inside the Panel, provided the targeted component does not prevent the + * click event from propagating. + * + * Use {@link #removeListener(ClickListener)} to remove the listener. + * + * @param listener + * The listener to add + */ + public void addListener(ClickListener listener) { + addListener(ClickEventHandler.CLICK_EVENT_IDENTIFIER, ClickEvent.class, + listener, ClickListener.clickMethod); + } + + /** + * Remove a click listener from the Panel. The listener should earlier have + * been added using {@link #addListener(ClickListener)}. + * + * @param listener + * The listener to remove + */ + public void removeListener(ClickListener listener) { + removeListener(ClickEventHandler.CLICK_EVENT_IDENTIFIER, + ClickEvent.class, listener); + } + + /** + * {@inheritDoc} + */ + @Override + public int getTabIndex() { + return getState().getTabIndex(); + } + + /** + * {@inheritDoc} + */ + @Override + public void setTabIndex(int tabIndex) { + getState().setTabIndex(tabIndex); + requestRepaint(); + } + + /** + * Moves keyboard focus to the component. {@see Focusable#focus()} + * + */ + @Override + public void focus() { + super.focus(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.ComponentContainer#getComponentCount() + */ + @Override + public int getComponentCount() { + // This is so wrong... (#2924) + return content.getComponentCount(); + } + + @Override + public PanelState getState() { + return (PanelState) super.getState(); + } + +} diff --git a/server/src/com/vaadin/ui/PasswordField.java b/server/src/com/vaadin/ui/PasswordField.java new file mode 100644 index 0000000000..c1fccebbfe --- /dev/null +++ b/server/src/com/vaadin/ui/PasswordField.java @@ -0,0 +1,67 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.ui; + +import com.vaadin.data.Property; + +/** + * A field that is used to enter secret text information like passwords. The + * entered text is not displayed on the screen. + */ +public class PasswordField extends AbstractTextField { + + /** + * Constructs an empty PasswordField. + */ + public PasswordField() { + setValue(""); + } + + /** + * Constructs a PasswordField with given property data source. + * + * @param dataSource + * the property data source for the field + */ + public PasswordField(Property dataSource) { + setPropertyDataSource(dataSource); + } + + /** + * Constructs a PasswordField with given caption and property data source. + * + * @param caption + * the caption for the field + * @param dataSource + * the property data source for the field + */ + public PasswordField(String caption, Property dataSource) { + this(dataSource); + setCaption(caption); + } + + /** + * Constructs a PasswordField with given value and caption. + * + * @param caption + * the caption for the field + * @param value + * the value for the field + */ + public PasswordField(String caption, String value) { + setValue(value); + setCaption(caption); + } + + /** + * Constructs a PasswordField with given caption. + * + * @param caption + * the caption for the field + */ + public PasswordField(String caption) { + this(); + setCaption(caption); + } +} diff --git a/server/src/com/vaadin/ui/PopupDateField.java b/server/src/com/vaadin/ui/PopupDateField.java new file mode 100644 index 0000000000..3688d4035f --- /dev/null +++ b/server/src/com/vaadin/ui/PopupDateField.java @@ -0,0 +1,80 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.util.Date; + +import com.vaadin.data.Property; +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.PaintTarget; + +/** + * <p> + * A date entry component, which displays the actual date selector as a popup. + * + * </p> + * + * @see DateField + * @see InlineDateField + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 5.0 + */ +public class PopupDateField extends DateField { + + private String inputPrompt = null; + + public PopupDateField() { + super(); + } + + public PopupDateField(Property dataSource) throws IllegalArgumentException { + super(dataSource); + } + + public PopupDateField(String caption, Date value) { + super(caption, value); + } + + public PopupDateField(String caption, Property dataSource) { + super(caption, dataSource); + } + + public PopupDateField(String caption) { + super(caption); + } + + @Override + public void paintContent(PaintTarget target) throws PaintException { + super.paintContent(target); + + if (inputPrompt != null) { + target.addAttribute("prompt", inputPrompt); + } + } + + /** + * Gets the current input prompt. + * + * @see #setInputPrompt(String) + * @return the current input prompt, or null if not enabled + */ + public String getInputPrompt() { + return inputPrompt; + } + + /** + * Sets the input prompt - a textual prompt that is displayed when the field + * would otherwise be empty, to prompt the user for input. + * + * @param inputPrompt + */ + public void setInputPrompt(String inputPrompt) { + this.inputPrompt = inputPrompt; + requestRepaint(); + } + +} diff --git a/server/src/com/vaadin/ui/PopupView.java b/server/src/com/vaadin/ui/PopupView.java new file mode 100644 index 0000000000..766181b50f --- /dev/null +++ b/server/src/com/vaadin/ui/PopupView.java @@ -0,0 +1,453 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.ui; + +import java.io.Serializable; +import java.lang.reflect.Method; +import java.util.Iterator; +import java.util.Map; + +import com.vaadin.terminal.LegacyPaint; +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.PaintTarget; +import com.vaadin.terminal.Vaadin6Component; + +/** + * + * A component for displaying a two different views to data. The minimized view + * is normally used to render the component, and when it is clicked the full + * view is displayed on a popup. The inner class {@link PopupView.Content} is + * used to deliver contents to this component. + * + * @author Vaadin Ltd. + */ +@SuppressWarnings("serial") +public class PopupView extends AbstractComponentContainer implements + Vaadin6Component { + + private Content content; + private boolean hideOnMouseOut; + private Component visibleComponent; + + private static final Method POPUP_VISIBILITY_METHOD; + static { + try { + POPUP_VISIBILITY_METHOD = PopupVisibilityListener.class + .getDeclaredMethod("popupVisibilityChange", + new Class[] { PopupVisibilityEvent.class }); + } catch (final java.lang.NoSuchMethodException e) { + // This should never happen + throw new java.lang.RuntimeException( + "Internal error finding methods in PopupView"); + } + } + + /** + * Iterator for the visible components (zero or one components), used by + * {@link PopupView#getComponentIterator()}. + */ + private static class SingleComponentIterator implements + Iterator<Component>, Serializable { + + private final Component component; + private boolean first; + + public SingleComponentIterator(Component component) { + this.component = component; + first = (component == null); + } + + @Override + public boolean hasNext() { + return !first; + } + + @Override + public Component next() { + if (!first) { + first = true; + return component; + } else { + return null; + } + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + } + + /* Constructors */ + + /** + * A simple way to create a PopupPanel. Note that the minimal representation + * may not be dynamically updated, in order to achieve this create your own + * Content object and use {@link PopupView#PopupView(Content)}. + * + * @param small + * the minimal textual representation as HTML + * @param large + * the full, Component-type representation + */ + public PopupView(final java.lang.String small, final Component large) { + this(new PopupView.Content() { + @Override + public java.lang.String getMinimizedValueAsHTML() { + return small; + } + + @Override + public Component getPopupComponent() { + return large; + } + }); + + } + + /** + * Creates a PopupView through the PopupView.Content interface. This allows + * the creator to dynamically change the contents of the PopupView. + * + * @param content + * the PopupView.Content that contains the information for this + */ + public PopupView(PopupView.Content content) { + super(); + hideOnMouseOut = true; + setContent(content); + } + + /** + * This method will replace the current content of the panel with a new one. + * + * @param newContent + * PopupView.Content object containing new information for the + * PopupView + * @throws IllegalArgumentException + * if the method is passed a null value, or if one of the + * content methods returns null + */ + public void setContent(PopupView.Content newContent) + throws IllegalArgumentException { + if (newContent == null) { + throw new IllegalArgumentException("Content must not be null"); + } + content = newContent; + requestRepaint(); + } + + /** + * Returns the content-package for this PopupView. + * + * @return the PopupView.Content for this object or null + */ + public PopupView.Content getContent() { + return content; + } + + /** + * @deprecated Use {@link #setPopupVisible()} instead. + */ + @Deprecated + public void setPopupVisibility(boolean visible) { + setPopupVisible(visible); + } + + /** + * @deprecated Use {@link #isPopupVisible()} instead. + */ + @Deprecated + public boolean getPopupVisibility() { + return isPopupVisible(); + } + + /** + * Set the visibility of the popup. Does not hide the minimal + * representation. + * + * @param visible + */ + public void setPopupVisible(boolean visible) { + if (isPopupVisible() != visible) { + if (visible) { + visibleComponent = content.getPopupComponent(); + if (visibleComponent == null) { + throw new java.lang.IllegalStateException( + "PopupView.Content did not return Component to set visible"); + } + super.addComponent(visibleComponent); + } else { + super.removeComponent(visibleComponent); + visibleComponent = null; + } + fireEvent(new PopupVisibilityEvent(this)); + requestRepaint(); + } + } + + /** + * Return whether the popup is visible. + * + * @return true if the popup is showing + */ + public boolean isPopupVisible() { + return visibleComponent != null; + } + + /** + * Check if this popup will be hidden when the user takes the mouse cursor + * out of the popup area. + * + * @return true if the popup is hidden on mouse out, false otherwise + */ + public boolean isHideOnMouseOut() { + return hideOnMouseOut; + } + + /** + * Should the popup automatically hide when the user takes the mouse cursor + * out of the popup area? If this is false, the user must click outside the + * popup to close it. The default is true. + * + * @param hideOnMouseOut + * + */ + public void setHideOnMouseOut(boolean hideOnMouseOut) { + this.hideOnMouseOut = hideOnMouseOut; + } + + /* + * Methods inherited from AbstractComponentContainer. These are unnecessary + * (but mandatory). Most of them are not supported in this implementation. + */ + + /** + * This class only contains other components when the popup is showing. + * + * @see com.vaadin.ui.ComponentContainer#getComponentIterator() + */ + @Override + public Iterator<Component> getComponentIterator() { + return new SingleComponentIterator(visibleComponent); + } + + /** + * Gets the number of contained components. Consistent with the iterator + * returned by {@link #getComponentIterator()}. + * + * @return the number of contained components (zero or one) + */ + @Override + public int getComponentCount() { + return (visibleComponent != null ? 1 : 0); + } + + /** + * Not supported in this implementation. + * + * @see com.vaadin.ui.AbstractComponentContainer#removeAllComponents() + * @throws UnsupportedOperationException + */ + @Override + public void removeAllComponents() { + throw new UnsupportedOperationException(); + } + + /** + * Not supported in this implementation. + * + * @see com.vaadin.ui.AbstractComponentContainer#moveComponentsFrom(com.vaadin.ui.ComponentContainer) + * @throws UnsupportedOperationException + */ + @Override + public void moveComponentsFrom(ComponentContainer source) + throws UnsupportedOperationException { + + throw new UnsupportedOperationException(); + } + + /** + * Not supported in this implementation. + * + * @see com.vaadin.ui.AbstractComponentContainer#addComponent(com.vaadin.ui.Component) + * @throws UnsupportedOperationException + */ + @Override + public void addComponent(Component c) throws UnsupportedOperationException { + throw new UnsupportedOperationException(); + + } + + /** + * Not supported in this implementation. + * + * @see com.vaadin.ui.ComponentContainer#replaceComponent(com.vaadin.ui.Component, + * com.vaadin.ui.Component) + * @throws UnsupportedOperationException + */ + @Override + public void replaceComponent(Component oldComponent, Component newComponent) + throws UnsupportedOperationException { + + throw new UnsupportedOperationException(); + } + + /** + * Not supported in this implementation + * + * @see com.vaadin.ui.AbstractComponentContainer#removeComponent(com.vaadin.ui.Component) + */ + @Override + public void removeComponent(Component c) + throws UnsupportedOperationException { + throw new UnsupportedOperationException(); + + } + + /* + * Methods for server-client communications. + */ + + /** + * Paint (serialize) the component for the client. + * + * @see com.vaadin.ui.AbstractComponent#paintContent(com.vaadin.terminal.PaintTarget) + */ + @Override + public void paintContent(PaintTarget target) throws PaintException { + String html = content.getMinimizedValueAsHTML(); + if (html == null) { + html = ""; + } + target.addAttribute("html", html); + target.addAttribute("hideOnMouseOut", hideOnMouseOut); + + // Only paint component to client if we know that the popup is showing + if (isPopupVisible()) { + target.startTag("popupComponent"); + LegacyPaint.paint(visibleComponent, target); + target.endTag("popupComponent"); + } + + target.addVariable(this, "popupVisibility", isPopupVisible()); + } + + /** + * Deserialize changes received from client. + * + * @see com.vaadin.ui.AbstractComponent#changeVariables(java.lang.Object, + * java.util.Map) + */ + @Override + public void changeVariables(Object source, Map<String, Object> variables) { + if (variables.containsKey("popupVisibility")) { + setPopupVisible(((Boolean) variables.get("popupVisibility")) + .booleanValue()); + } + } + + /** + * Used to deliver customized content-packages to the PopupView. These are + * dynamically loaded when they are redrawn. The user must take care that + * neither of these methods ever return null. + */ + public interface Content extends Serializable { + + /** + * This should return a small view of the full data. + * + * @return value in HTML format + */ + public String getMinimizedValueAsHTML(); + + /** + * This should return the full Component representing the data + * + * @return a Component for the value + */ + public Component getPopupComponent(); + } + + /** + * Add a listener that is called whenever the visibility of the popup is + * changed. + * + * @param listener + * the listener to add + * @see PopupVisibilityListener + * @see PopupVisibilityEvent + * @see #removeListener(PopupVisibilityListener) + * + */ + public void addListener(PopupVisibilityListener listener) { + addListener(PopupVisibilityEvent.class, listener, + POPUP_VISIBILITY_METHOD); + } + + /** + * Removes a previously added listener, so that it no longer receives events + * when the visibility of the popup changes. + * + * @param listener + * the listener to remove + * @see PopupVisibilityListener + * @see #addListener(PopupVisibilityListener) + */ + public void removeListener(PopupVisibilityListener listener) { + removeListener(PopupVisibilityEvent.class, listener, + POPUP_VISIBILITY_METHOD); + } + + /** + * This event is received by the PopupVisibilityListeners when the + * visibility of the popup changes. You can get the new visibility directly + * with {@link #isPopupVisible()}, or get the PopupView that produced the + * event with {@link #getPopupView()}. + * + */ + public class PopupVisibilityEvent extends Event { + + public PopupVisibilityEvent(PopupView source) { + super(source); + } + + /** + * Get the PopupView instance that is the source of this event. + * + * @return the source PopupView + */ + public PopupView getPopupView() { + return (PopupView) getSource(); + } + + /** + * Returns the current visibility of the popup. + * + * @return true if the popup is visible + */ + public boolean isPopupVisible() { + return getPopupView().isPopupVisible(); + } + } + + /** + * Defines a listener that can receive a PopupVisibilityEvent when the + * visibility of the popup changes. + * + */ + public interface PopupVisibilityListener extends Serializable { + /** + * Pass to {@link PopupView#PopupVisibilityEvent} to start listening for + * popup visibility changes. + * + * @param event + * the event + * + * @see {@link PopupVisibilityEvent} + * @see {@link PopupView#addListener(PopupVisibilityListener)} + */ + public void popupVisibilityChange(PopupVisibilityEvent event); + } +} diff --git a/server/src/com/vaadin/ui/ProgressIndicator.java b/server/src/com/vaadin/ui/ProgressIndicator.java new file mode 100644 index 0000000000..fef54a267c --- /dev/null +++ b/server/src/com/vaadin/ui/ProgressIndicator.java @@ -0,0 +1,257 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.util.Map; + +import com.vaadin.data.Property; +import com.vaadin.data.util.ObjectProperty; +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.PaintTarget; +import com.vaadin.terminal.Vaadin6Component; + +/** + * <code>ProgressIndicator</code> is component that shows user state of a + * process (like long computing or file upload) + * + * <code>ProgressIndicator</code> has two mainmodes. One for indeterminate + * processes and other (default) for processes which progress can be measured + * + * May view an other property that indicates progress 0...1 + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 4 + */ +@SuppressWarnings("serial") +public class ProgressIndicator extends AbstractField<Number> implements + Property.Viewer, Property.ValueChangeListener, Vaadin6Component { + + /** + * Content mode, where the label contains only plain text. The getValue() + * result is coded to XML when painting. + */ + public static final int CONTENT_TEXT = 0; + + /** + * Content mode, where the label contains preformatted text. + */ + public static final int CONTENT_PREFORMATTED = 1; + + private boolean indeterminate = false; + + private Property dataSource; + + private int pollingInterval = 1000; + + /** + * Creates an a new ProgressIndicator. + */ + public ProgressIndicator() { + setPropertyDataSource(new ObjectProperty<Float>(new Float(0), + Float.class)); + } + + /** + * Creates a new instance of ProgressIndicator with given state. + * + * @param value + */ + public ProgressIndicator(Float value) { + setPropertyDataSource(new ObjectProperty<Float>(value, Float.class)); + } + + /** + * Creates a new instance of ProgressIndicator with stae read from given + * datasource. + * + * @param contentSource + */ + public ProgressIndicator(Property contentSource) { + setPropertyDataSource(contentSource); + } + + /** + * Sets the component to read-only. Readonly is not used in + * ProgressIndicator. + * + * @param readOnly + * True to enable read-only mode, False to disable it. + */ + @Override + public void setReadOnly(boolean readOnly) { + if (dataSource == null) { + throw new IllegalStateException("Datasource must be se"); + } + dataSource.setReadOnly(readOnly); + } + + /** + * Is the component read-only ? Readonly is not used in ProgressIndicator - + * this returns allways false. + * + * @return True if the component is in read only mode. + */ + @Override + public boolean isReadOnly() { + if (dataSource == null) { + throw new IllegalStateException("Datasource must be se"); + } + return dataSource.isReadOnly(); + } + + /** + * Paints the content of this component. + * + * @param target + * the Paint Event. + * @throws PaintException + * if the Paint Operation fails. + */ + @Override + public void paintContent(PaintTarget target) throws PaintException { + target.addAttribute("indeterminate", indeterminate); + target.addAttribute("pollinginterval", pollingInterval); + target.addAttribute("state", getValue().toString()); + } + + /** + * Gets the value of the ProgressIndicator. Value of the ProgressIndicator + * is Float between 0 and 1. + * + * @return the Value of the ProgressIndicator. + * @see com.vaadin.ui.AbstractField#getValue() + */ + @Override + public Number getValue() { + if (dataSource == null) { + throw new IllegalStateException("Datasource must be set"); + } + // TODO conversions to eliminate cast + return (Number) dataSource.getValue(); + } + + /** + * Sets the value of the ProgressIndicator. Value of the ProgressIndicator + * is the Float between 0 and 1. + * + * @param newValue + * the New value of the ProgressIndicator. + * @see com.vaadin.ui.AbstractField#setValue() + */ + @Override + public void setValue(Object newValue) { + if (dataSource == null) { + throw new IllegalStateException("Datasource must be set"); + } + dataSource.setValue(newValue); + } + + /** + * @see com.vaadin.ui.AbstractField#getType() + */ + @Override + public Class<? extends Number> getType() { + if (dataSource == null) { + throw new IllegalStateException("Datasource must be set"); + } + return dataSource.getType(); + } + + /** + * Gets the viewing data-source property. + * + * @return the datasource. + * @see com.vaadin.ui.AbstractField#getPropertyDataSource() + */ + @Override + public Property getPropertyDataSource() { + return dataSource; + } + + /** + * Sets the property as data-source for viewing. + * + * @param newDataSource + * the new data source. + * @see com.vaadin.ui.AbstractField#setPropertyDataSource(com.vaadin.data.Property) + */ + @Override + public void setPropertyDataSource(Property newDataSource) { + // Stops listening the old data source changes + if (dataSource != null + && Property.ValueChangeNotifier.class + .isAssignableFrom(dataSource.getClass())) { + ((Property.ValueChangeNotifier) dataSource).removeListener(this); + } + + // Sets the new data source + dataSource = newDataSource; + + // Listens the new data source if possible + if (dataSource != null + && Property.ValueChangeNotifier.class + .isAssignableFrom(dataSource.getClass())) { + ((Property.ValueChangeNotifier) dataSource).addListener(this); + } + } + + /** + * Gets the mode of ProgressIndicator. + * + * @return true if in indeterminate mode. + */ + public boolean getContentMode() { + return indeterminate; + } + + /** + * Sets wheter or not the ProgressIndicator is indeterminate. + * + * @param newValue + * true to set to indeterminate mode. + */ + public void setIndeterminate(boolean newValue) { + indeterminate = newValue; + requestRepaint(); + } + + /** + * Gets whether or not the ProgressIndicator is indeterminate. + * + * @return true to set to indeterminate mode. + */ + public boolean isIndeterminate() { + return indeterminate; + } + + /** + * Sets the interval that component checks for progress. + * + * @param newValue + * the interval in milliseconds. + */ + public void setPollingInterval(int newValue) { + pollingInterval = newValue; + requestRepaint(); + } + + /** + * Gets the interval that component checks for progress. + * + * @return the interval in milliseconds. + */ + public int getPollingInterval() { + return pollingInterval; + } + + @Override + public void changeVariables(Object source, Map<String, Object> variables) { + // TODO Remove once Vaadin6Component is no longer implemented + + } + +} diff --git a/server/src/com/vaadin/ui/RichTextArea.java b/server/src/com/vaadin/ui/RichTextArea.java new file mode 100644 index 0000000000..cec952926b --- /dev/null +++ b/server/src/com/vaadin/ui/RichTextArea.java @@ -0,0 +1,344 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.text.Format; +import java.util.Map; + +import com.vaadin.data.Property; +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.PaintTarget; +import com.vaadin.terminal.Vaadin6Component; + +/** + * 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 + Vaadin6Component { + + /** + * Value formatter used to format the string contents. + */ + @Deprecated + private Format format; + + /** + * 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 = getFormattedValue(); + 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(); + requestRepaint(); + } + + /** + * Gets the formatted string value. Sets the field value by using the + * assigned Format. + * + * @return the Formatted value. + * @see #setFormat(Format) + * @see Format + * @deprecated + */ + @Deprecated + protected String getFormattedValue() { + Object v = getValue(); + if (v == null) { + return null; + } + return v.toString(); + } + + @Override + public String getValue() { + String v = super.getValue(); + if (format == null || v == null) { + return v; + } + try { + return format.format(v); + } catch (final IllegalArgumentException e) { + return v; + } + } + + @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 = getFormattedValue(); + 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, or if we have a formatter, + // repaint is needed after all. + if (format != null || wasModified != isModified()) { + requestRepaint(); + } + } + } + + } + + @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; + } + + /** + * Gets the value formatter of TextField. + * + * @return the Format used to format the value. + * @deprecated replaced by {@link com.vaadin.data.util.PropertyFormatter} + */ + @Deprecated + public Format getFormat() { + return format; + } + + /** + * Gets the value formatter of TextField. + * + * @param format + * the Format used to format the value. Null disables the + * formatting. + * @deprecated replaced by {@link com.vaadin.data.util.PropertyFormatter} + */ + @Deprecated + public void setFormat(Format format) { + this.format = format; + requestRepaint(); + } + + @Override + protected boolean isEmpty() { + return super.isEmpty() || getValue().length() == 0; + } + +} diff --git a/server/src/com/vaadin/ui/Root.java b/server/src/com/vaadin/ui/Root.java new file mode 100644 index 0000000000..bd4842632b --- /dev/null +++ b/server/src/com/vaadin/ui/Root.java @@ -0,0 +1,1227 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.Map; + +import com.vaadin.Application; +import com.vaadin.annotations.EagerInit; +import com.vaadin.event.Action; +import com.vaadin.event.Action.Handler; +import com.vaadin.event.ActionManager; +import com.vaadin.event.MouseEvents.ClickEvent; +import com.vaadin.event.MouseEvents.ClickListener; +import com.vaadin.shared.MouseEventDetails; +import com.vaadin.shared.ui.root.RootServerRpc; +import com.vaadin.shared.ui.root.RootState; +import com.vaadin.terminal.Page; +import com.vaadin.terminal.Page.BrowserWindowResizeEvent; +import com.vaadin.terminal.Page.BrowserWindowResizeListener; +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.PaintTarget; +import com.vaadin.terminal.Resource; +import com.vaadin.terminal.Vaadin6Component; +import com.vaadin.terminal.WrappedRequest; +import com.vaadin.terminal.WrappedRequest.BrowserDetails; +import com.vaadin.terminal.gwt.client.ui.root.VRoot; +import com.vaadin.ui.Window.CloseListener; + +/** + * The topmost component in any component hierarchy. There is one root for every + * Vaadin instance in a browser window. A root may either represent an entire + * browser window (or tab) or some part of a html page where a Vaadin + * application is embedded. + * <p> + * The root is the server side entry point for various client side features that + * are not represented as components added to a layout, e.g notifications, sub + * windows, and executing javascript in the browser. + * </p> + * <p> + * When a new application instance is needed, typically because the user opens + * the application in a browser window, + * {@link Application#gerRoot(WrappedRequest)} is invoked to get a root. That + * method does by default create a root according to the + * {@value Application#ROOT_PARAMETER} parameter from web.xml. + * </p> + * <p> + * After a root has been created by the application, it is initialized using + * {@link #init(WrappedRequest)}. This method is intended to be overridden by + * the developer to add components to the user interface and initialize + * non-component functionality. The component hierarchy is initialized by + * passing a {@link ComponentContainer} with the main layout of the view to + * {@link #setContent(ComponentContainer)}. + * </p> + * <p> + * If a {@link EagerInit} annotation is present on a class extending + * <code>Root</code>, the framework will use a faster initialization method + * which will not ensure that {@link BrowserDetails} are present in the + * {@link WrappedRequest} passed to the init method. + * </p> + * + * @see #init(WrappedRequest) + * @see Application#getRoot(WrappedRequest) + * + * @since 7.0 + */ +public abstract class Root extends AbstractComponentContainer implements + Action.Container, Action.Notifier, Vaadin6Component { + + /** + * Helper class to emulate the main window from Vaadin 6 using roots. This + * class should be used in the same way as Window used as a browser level + * window in Vaadin 6 with {@link com.vaadin.Application.LegacyApplication} + */ + @Deprecated + @EagerInit + public static class LegacyWindow extends Root { + private String name; + + /** + * Create a new legacy window + */ + public LegacyWindow() { + super(); + } + + /** + * Creates a new legacy window with the given caption + * + * @param caption + * the caption of the window + */ + public LegacyWindow(String caption) { + super(caption); + } + + /** + * Creates a legacy window with the given caption and content layout + * + * @param caption + * @param content + */ + public LegacyWindow(String caption, ComponentContainer content) { + super(caption, content); + } + + @Override + protected void init(WrappedRequest request) { + // Just empty + } + + /** + * Gets the unique name of the window. The name of the window is used to + * uniquely identify it. + * <p> + * The name also determines the URL that can be used for direct access + * to a window. All windows can be accessed through + * {@code http://host:port/app/win} where {@code http://host:port/app} + * is the application URL (as returned by {@link Application#getURL()} + * and {@code win} is the window name. + * </p> + * <p> + * Note! Portlets do not support direct window access through URLs. + * </p> + * + * @return the Name of the Window. + */ + public String getName() { + return name; + } + + /** + * Sets the unique name of the window. The name of the window is used to + * uniquely identify it inside the application. + * <p> + * The name also determines the URL that can be used for direct access + * to a window. All windows can be accessed through + * {@code http://host:port/app/win} where {@code http://host:port/app} + * is the application URL (as returned by {@link Application#getURL()} + * and {@code win} is the window name. + * </p> + * <p> + * This method can only be called before the window is added to an + * application. + * <p> + * Note! Portlets do not support direct window access through URLs. + * </p> + * + * @param name + * the new name for the window or null if the application + * should automatically assign a name to it + * @throws IllegalStateException + * if the window is attached to an application + */ + public void setName(String name) { + this.name = name; + // The name can not be changed in application + if (getApplication() != null) { + throw new IllegalStateException( + "Window name can not be changed while " + + "the window is in application"); + } + + } + + /** + * Gets the full URL of the window. The returned URL is window specific + * and can be used to directly refer to the window. + * <p> + * Note! This method can not be used for portlets. + * </p> + * + * @return the URL of the window or null if the window is not attached + * to an application + */ + public URL getURL() { + Application application = getApplication(); + if (application == null) { + return null; + } + + try { + return new URL(application.getURL(), getName() + "/"); + } catch (MalformedURLException e) { + throw new RuntimeException( + "Internal problem getting window URL, please report"); + } + } + + /** + * Opens the given resource in this root. The contents of this Root is + * replaced by the {@code Resource}. + * + * @param resource + * the resource to show in this root + * + * @deprecated As of 7.0, use getPage().open instead + */ + @Deprecated + public void open(Resource resource) { + getPage().open(resource); + } + + /* ********************************************************************* */ + + /** + * Opens the given resource in a window with the given name. + * <p> + * The supplied {@code windowName} is used as the target name in a + * window.open call in the client. This means that special values such + * as "_blank", "_self", "_top", "_parent" have special meaning. An + * empty or <code>null</code> window name is also a special case. + * </p> + * <p> + * "", null and "_self" as {@code windowName} all causes the resource to + * be opened in the current window, replacing any old contents. For + * downloadable content you should avoid "_self" as "_self" causes the + * client to skip rendering of any other changes as it considers them + * irrelevant (the page will be replaced by the resource). This can + * speed up the opening of a resource, but it might also put the client + * side into an inconsistent state if the window content is not + * completely replaced e.g., if the resource is downloaded instead of + * displayed in the browser. + * </p> + * <p> + * "_blank" as {@code windowName} causes the resource to always be + * opened in a new window or tab (depends on the browser and browser + * settings). + * </p> + * <p> + * "_top" and "_parent" as {@code windowName} works as specified by the + * HTML standard. + * </p> + * <p> + * Any other {@code windowName} will open the resource in a window with + * that name, either by opening a new window/tab in the browser or by + * replacing the contents of an existing window with that name. + * </p> + * + * @param resource + * the resource. + * @param windowName + * the name of the window. + * @deprecated As of 7.0, use getPage().open instead + */ + @Deprecated + public void open(Resource resource, String windowName) { + getPage().open(resource, windowName); + } + + /** + * Opens the given resource in a window with the given size, border and + * name. For more information on the meaning of {@code windowName}, see + * {@link #open(Resource, String)}. + * + * @param resource + * the resource. + * @param windowName + * the name of the window. + * @param width + * the width of the window in pixels + * @param height + * the height of the window in pixels + * @param border + * the border style of the window. See {@link #BORDER_NONE + * Window.BORDER_* constants} + * @deprecated As of 7.0, use getPage().open instead + */ + @Deprecated + public void open(Resource resource, String windowName, int width, + int height, int border) { + getPage().open(resource, windowName, width, height, border); + } + + /** + * Adds a new {@link BrowserWindowResizeListener} to this root. The + * listener will be notified whenever the browser window within which + * this root resides is resized. + * + * @param resizeListener + * the listener to add + * + * @see BrowserWindowResizeListener#browserWindowResized(BrowserWindowResizeEvent) + * @see #setResizeLazy(boolean) + * + * @deprecated As of 7.0, use the similarly named api in Page instead + */ + @Deprecated + public void addListener(BrowserWindowResizeListener resizeListener) { + getPage().addListener(resizeListener); + } + + /** + * Removes a {@link BrowserWindowResizeListener} from this root. The + * listener will no longer be notified when the browser window is + * resized. + * + * @param resizeListener + * the listener to remove + * @deprecated As of 7.0, use the similarly named api in Page instead + */ + @Deprecated + public void removeListener(BrowserWindowResizeListener resizeListener) { + getPage().removeListener(resizeListener); + } + + /** + * Gets the last known height of the browser window in which this root + * resides. + * + * @return the browser window height in pixels + * @deprecated As of 7.0, use the similarly named api in Page instead + */ + @Deprecated + public int getBrowserWindowHeight() { + return getPage().getBrowserWindowHeight(); + } + + /** + * Gets the last known width of the browser window in which this root + * resides. + * + * @return the browser window width in pixels + * + * @deprecated As of 7.0, use the similarly named api in Page instead + */ + @Deprecated + public int getBrowserWindowWidth() { + return getPage().getBrowserWindowWidth(); + } + + /** + * Executes JavaScript in this window. + * + * <p> + * This method allows one to inject javascript from the server to + * client. A client implementation is not required to implement this + * functionality, but currently all web-based clients do implement this. + * </p> + * + * <p> + * Executing javascript this way often leads to cross-browser + * compatibility issues and regressions that are hard to resolve. Use of + * this method should be avoided and instead it is recommended to create + * new widgets with GWT. For more info on creating own, reusable + * client-side widgets in Java, read the corresponding chapter in Book + * of Vaadin. + * </p> + * + * @param script + * JavaScript snippet that will be executed. + * + * @deprecated as of 7.0, use JavaScript.getCurrent().execute(String) + * instead + */ + @Deprecated + public void executeJavaScript(String script) { + getPage().getJavaScript().execute(script); + } + + @Override + public void setCaption(String caption) { + // Override to provide backwards compatibility + getState().setCaption(caption); + getPage().setTitle(caption); + } + + } + + /** + * The application to which this root belongs + */ + private Application application; + + /** + * List of windows in this root. + */ + private final LinkedHashSet<Window> windows = new LinkedHashSet<Window>(); + + /** + * The component that should be scrolled into view after the next repaint. + * Null if nothing should be scrolled into view. + */ + private Component scrollIntoView; + + /** + * The id of this root, used to find the server side instance of the root + * form which a request originates. A negative value indicates that the root + * id has not yet been assigned by the Application. + * + * @see Application#nextRootId + */ + private int rootId = -1; + + /** + * Keeps track of the Actions added to this component, and manages the + * painting and handling as well. + */ + protected ActionManager actionManager; + + /** + * Thread local for keeping track of the current root. + */ + private static final ThreadLocal<Root> currentRoot = new ThreadLocal<Root>(); + + /** Identifies the click event */ + private static final String CLICK_EVENT_ID = VRoot.CLICK_EVENT_ID; + + private ConnectorTracker connectorTracker = new ConnectorTracker(this); + + private Page page = new Page(this); + + private RootServerRpc rpc = new RootServerRpc() { + @Override + public void click(MouseEventDetails mouseDetails) { + fireEvent(new ClickEvent(Root.this, mouseDetails)); + } + }; + + /** + * Creates a new empty root without a caption. This root will have a + * {@link VerticalLayout} with margins enabled as its content. + */ + public Root() { + this((ComponentContainer) null); + } + + /** + * Creates a new root with the given component container as its content. + * + * @param content + * the content container to use as this roots content. + * + * @see #setContent(ComponentContainer) + */ + public Root(ComponentContainer content) { + registerRpc(rpc); + setSizeFull(); + setContent(content); + } + + /** + * Creates a new empty root with the given caption. This root will have a + * {@link VerticalLayout} with margins enabled as its content. + * + * @param caption + * the caption of the root, used as the page title if there's + * nothing but the application on the web page + * + * @see #setCaption(String) + */ + public Root(String caption) { + this((ComponentContainer) null); + setCaption(caption); + } + + /** + * Creates a new root with the given caption and content. + * + * @param caption + * the caption of the root, used as the page title if there's + * nothing but the application on the web page + * @param content + * the content container to use as this roots content. + * + * @see #setContent(ComponentContainer) + * @see #setCaption(String) + */ + public Root(String caption, ComponentContainer content) { + this(content); + setCaption(caption); + } + + @Override + public RootState getState() { + return (RootState) super.getState(); + } + + @Override + public Class<? extends RootState> getStateType() { + // This is a workaround for a problem with creating the correct state + // object during build + return RootState.class; + } + + /** + * Overridden to return a value instead of referring to the parent. + * + * @return this root + * + * @see com.vaadin.ui.AbstractComponent#getRoot() + */ + @Override + public Root getRoot() { + return this; + } + + @Override + public void replaceComponent(Component oldComponent, Component newComponent) { + throw new UnsupportedOperationException(); + } + + @Override + public Application getApplication() { + return application; + } + + @Override + public void paintContent(PaintTarget target) throws PaintException { + page.paintContent(target); + + if (scrollIntoView != null) { + target.addAttribute("scrollTo", scrollIntoView); + scrollIntoView = null; + } + + if (pendingFocus != null) { + // ensure focused component is still attached to this main window + if (pendingFocus.getRoot() == this + || (pendingFocus.getRoot() != null && pendingFocus + .getRoot().getParent() == this)) { + target.addAttribute("focused", pendingFocus); + } + pendingFocus = null; + } + + if (actionManager != null) { + actionManager.paintActions(null, target); + } + + if (isResizeLazy()) { + target.addAttribute(VRoot.RESIZE_LAZY, true); + } + } + + /** + * Fire a click event to all click listeners. + * + * @param object + * The raw "value" of the variable change from the client side. + */ + private void fireClick(Map<String, Object> parameters) { + MouseEventDetails mouseDetails = MouseEventDetails + .deSerialize((String) parameters.get("mouseDetails")); + fireEvent(new ClickEvent(this, mouseDetails)); + } + + @Override + @SuppressWarnings("unchecked") + public void changeVariables(Object source, Map<String, Object> variables) { + if (variables.containsKey(CLICK_EVENT_ID)) { + fireClick((Map<String, Object>) variables.get(CLICK_EVENT_ID)); + } + + // Actions + if (actionManager != null) { + actionManager.handleActions(variables, this); + } + + if (variables.containsKey(VRoot.FRAGMENT_VARIABLE)) { + String fragment = (String) variables.get(VRoot.FRAGMENT_VARIABLE); + getPage().setFragment(fragment, true); + } + + if (variables.containsKey("height") || variables.containsKey("width")) { + getPage().setBrowserWindowSize((Integer) variables.get("width"), + (Integer) variables.get("height")); + } + + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.ComponentContainer#getComponentIterator() + */ + @Override + public Iterator<Component> getComponentIterator() { + // TODO could directly create some kind of combined iterator instead of + // creating a new ArrayList + ArrayList<Component> components = new ArrayList<Component>(); + + if (getContent() != null) { + components.add(getContent()); + } + + components.addAll(windows); + + return components.iterator(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.ComponentContainer#getComponentCount() + */ + @Override + public int getComponentCount() { + return windows.size() + (getContent() == null ? 0 : 1); + } + + /** + * Sets the application to which this root is assigned. It is not legal to + * change the application once it has been set nor to set a + * <code>null</code> application. + * <p> + * This method is mainly intended for internal use by the framework. + * </p> + * + * @param application + * the application to set + * + * @throws IllegalStateException + * if the application has already been set + * + * @see #getApplication() + */ + public void setApplication(Application application) { + if ((application == null) == (this.application == null)) { + throw new IllegalStateException("Application has already been set"); + } else { + this.application = application; + } + + if (application != null) { + attach(); + } else { + detach(); + } + } + + /** + * Sets the id of this root within its application. The root id is used to + * route requests to the right root. + * <p> + * This method is mainly intended for internal use by the framework. + * </p> + * + * @param rootId + * the id of this root + * + * @throws IllegalStateException + * if the root id has already been set + * + * @see #getRootId() + */ + public void setRootId(int rootId) { + if (this.rootId != -1) { + throw new IllegalStateException("Root id has already been defined"); + } + this.rootId = rootId; + } + + /** + * Gets the id of the root, used to identify this root within its + * application when processing requests. The root id should be present in + * every request to the server that originates from this root. + * {@link Application#getRootForRequest(WrappedRequest)} uses this id to + * find the route to which the request belongs. + * + * @return + */ + public int getRootId() { + return rootId; + } + + /** + * Adds a window as a subwindow inside this root. To open a new browser + * window or tab, you should instead use {@link open(Resource)} with an url + * pointing to this application and ensure + * {@link Application#getRoot(WrappedRequest)} returns an appropriate root + * for the request. + * + * @param window + * @throws IllegalArgumentException + * if the window is already added to an application + * @throws NullPointerException + * if the given <code>Window</code> is <code>null</code>. + */ + public void addWindow(Window window) throws IllegalArgumentException, + NullPointerException { + + if (window == null) { + throw new NullPointerException("Argument must not be null"); + } + + if (window.getApplication() != null) { + throw new IllegalArgumentException( + "Window is already attached to an application."); + } + + attachWindow(window); + } + + /** + * Helper method to attach a window. + * + * @param w + * the window to add + */ + private void attachWindow(Window w) { + windows.add(w); + w.setParent(this); + requestRepaint(); + } + + /** + * Remove the given subwindow from this root. + * + * Since Vaadin 6.5, {@link CloseListener}s are called also when explicitly + * removing a window by calling this method. + * + * Since Vaadin 6.5, returns a boolean indicating if the window was removed + * or not. + * + * @param window + * Window to be removed. + * @return true if the subwindow was removed, false otherwise + */ + public boolean removeWindow(Window window) { + if (!windows.remove(window)) { + // Window window is not a subwindow of this root. + return false; + } + window.setParent(null); + window.fireClose(); + requestRepaint(); + + return true; + } + + /** + * Gets all the windows added to this root. + * + * @return an unmodifiable collection of windows + */ + public Collection<Window> getWindows() { + return Collections.unmodifiableCollection(windows); + } + + @Override + public void focus() { + super.focus(); + } + + /** + * Component that should be focused after the next repaint. Null if no focus + * change should take place. + */ + private Focusable pendingFocus; + + private boolean resizeLazy = false; + + /** + * This method is used by Component.Focusable objects to request focus to + * themselves. Focus renders must be handled at window level (instead of + * Component.Focusable) due we want the last focused component to be focused + * in client too. Not the one that is rendered last (the case we'd get if + * implemented in Focusable only). + * + * To focus component from Vaadin application, use Focusable.focus(). See + * {@link Focusable}. + * + * @param focusable + * to be focused on next paint + */ + public void setFocusedComponent(Focusable focusable) { + pendingFocus = focusable; + requestRepaint(); + } + + /** + * Scrolls any component between the component and root to a suitable + * position so the component is visible to the user. The given component + * must belong to this root. + * + * @param component + * the component to be scrolled into view + * @throws IllegalArgumentException + * if {@code component} does not belong to this root + */ + public void scrollIntoView(Component component) + throws IllegalArgumentException { + if (component.getRoot() != this) { + throw new IllegalArgumentException( + "The component where to scroll must belong to this root."); + } + scrollIntoView = component; + requestRepaint(); + } + + /** + * Gets the content of this root. The content is a component container that + * serves as the outermost item of the visual contents of this root. + * + * @return a component container to use as content + * + * @see #setContent(ComponentContainer) + * @see #createDefaultLayout() + */ + public ComponentContainer getContent() { + return (ComponentContainer) getState().getContent(); + } + + /** + * Helper method to create the default content layout that is used if no + * content has not been explicitly defined. + * + * @return a newly created layout + */ + private static VerticalLayout createDefaultLayout() { + VerticalLayout layout = new VerticalLayout(); + layout.setMargin(true); + return layout; + } + + /** + * Sets the content of this root. The content is a component container that + * serves as the outermost item of the visual contents of this root. If no + * content has been set, a {@link VerticalLayout} with margins enabled will + * be used by default - see {@link #createDefaultLayout()}. The content can + * also be set in a constructor. + * + * @return a component container to use as content + * + * @see #Root(ComponentContainer) + * @see #createDefaultLayout() + */ + public void setContent(ComponentContainer content) { + if (content == null) { + content = createDefaultLayout(); + } + + if (getState().getContent() != null) { + super.removeComponent((Component) getState().getContent()); + } + getState().setContent(content); + if (content != null) { + super.addComponent(content); + } + + requestRepaint(); + } + + /** + * Adds a component to this root. The component is not added directly to the + * root, but instead to the content container ({@link #getContent()}). + * + * @param component + * the component to add to this root + * + * @see #getContent() + */ + @Override + public void addComponent(Component component) { + getContent().addComponent(component); + } + + /** + * This implementation removes the component from the content container ( + * {@link #getContent()}) instead of from the actual root. + */ + @Override + public void removeComponent(Component component) { + getContent().removeComponent(component); + } + + /** + * This implementation removes the components from the content container ( + * {@link #getContent()}) instead of from the actual root. + */ + @Override + public void removeAllComponents() { + getContent().removeAllComponents(); + } + + /** + * Internal initialization method, should not be overridden. This method is + * not declared as final because that would break compatibility with e.g. + * CDI. + * + * @param request + * the initialization request + */ + public void doInit(WrappedRequest request) { + getPage().init(request); + + // Call the init overridden by the application developer + init(request); + } + + /** + * Initializes this root. This method is intended to be overridden by + * subclasses to build the view and configure non-component functionality. + * Performing the initialization in a constructor is not suggested as the + * state of the root is not properly set up when the constructor is invoked. + * <p> + * The {@link WrappedRequest} can be used to get information about the + * request that caused this root to be created. By default, the + * {@link BrowserDetails} will be available in the request. If the browser + * details are not required, loading the application in the browser can take + * some shortcuts giving a faster initial rendering. This can be indicated + * by adding the {@link EagerInit} annotation to the Root class. + * </p> + * + * @param request + * the wrapped request that caused this root to be created + */ + protected abstract void init(WrappedRequest request); + + /** + * Sets the thread local for the current root. This method is used by the + * framework to set the current application whenever a new request is + * processed and it is cleared when the request has been processed. + * <p> + * The application developer can also use this method to define the current + * root outside the normal request handling, e.g. when initiating custom + * background threads. + * </p> + * + * @param root + * the root to register as the current root + * + * @see #getCurrent() + * @see ThreadLocal + */ + public static void setCurrent(Root root) { + currentRoot.set(root); + } + + /** + * Gets the currently used root. The current root is automatically defined + * when processing requests to the server. In other cases, (e.g. from + * background threads), the current root is not automatically defined. + * + * @return the current root instance if available, otherwise + * <code>null</code> + * + * @see #setCurrent(Root) + */ + public static Root getCurrent() { + return currentRoot.get(); + } + + public void setScrollTop(int scrollTop) { + throw new RuntimeException("Not yet implemented"); + } + + @Override + protected ActionManager getActionManager() { + if (actionManager == null) { + actionManager = new ActionManager(this); + } + return actionManager; + } + + @Override + public <T extends Action & com.vaadin.event.Action.Listener> void addAction( + T action) { + getActionManager().addAction(action); + } + + @Override + public <T extends Action & com.vaadin.event.Action.Listener> void removeAction( + T action) { + if (actionManager != null) { + actionManager.removeAction(action); + } + } + + @Override + public void addActionHandler(Handler actionHandler) { + getActionManager().addActionHandler(actionHandler); + } + + @Override + public void removeActionHandler(Handler actionHandler) { + if (actionManager != null) { + actionManager.removeActionHandler(actionHandler); + } + } + + /** + * Should resize operations be lazy, i.e. should there be a delay before + * layout sizes are recalculated. Speeds up resize operations in slow UIs + * with the penalty of slightly decreased usability. + * <p> + * Default value: <code>false</code> + * + * @param resizeLazy + * true to use a delay before recalculating sizes, false to + * calculate immediately. + */ + public void setResizeLazy(boolean resizeLazy) { + this.resizeLazy = resizeLazy; + requestRepaint(); + } + + /** + * Checks whether lazy resize is enabled. + * + * @return <code>true</code> if lazy resize is enabled, <code>false</code> + * if lazy resize is not enabled + */ + public boolean isResizeLazy() { + return resizeLazy; + } + + /** + * Add a click listener to the Root. The listener is called whenever the + * user clicks inside the Root. Also when the click targets a component + * inside the Root, provided the targeted component does not prevent the + * click event from propagating. + * + * Use {@link #removeListener(ClickListener)} to remove the listener. + * + * @param listener + * The listener to add + */ + public void addListener(ClickListener listener) { + addListener(CLICK_EVENT_ID, ClickEvent.class, listener, + ClickListener.clickMethod); + } + + /** + * Remove a click listener from the Root. The listener should earlier have + * been added using {@link #addListener(ClickListener)}. + * + * @param listener + * The listener to remove + */ + public void removeListener(ClickListener listener) { + removeListener(CLICK_EVENT_ID, ClickEvent.class, listener); + } + + @Override + public boolean isConnectorEnabled() { + // TODO How can a Root be invisible? What does it mean? + return isVisible() && isEnabled(); + } + + public ConnectorTracker getConnectorTracker() { + return connectorTracker; + } + + public Page getPage() { + return page; + } + + /** + * Setting the caption of a Root is not supported. To set the title of the + * HTML page, use Page.setTitle + * + * @deprecated as of 7.0.0, use {@link Page#setTitle(String)} + */ + @Override + @Deprecated + public void setCaption(String caption) { + throw new IllegalStateException( + "You can not set the title of a Root. To set the title of the HTML page, use Page.setTitle"); + } + + /** + * Shows a notification message on the middle of the root. The message + * automatically disappears ("humanized message"). + * + * Care should be taken to to avoid XSS vulnerabilities as the caption is + * rendered as html. + * + * @see #showNotification(Notification) + * @see Notification + * + * @param caption + * The message + * + * @deprecated As of 7.0, use Notification.show instead but be aware that + * Notification.show does not allow HTML. + */ + @Deprecated + public void showNotification(String caption) { + Notification notification = new Notification(caption); + notification.setHtmlContentAllowed(true);// Backwards compatibility + getPage().showNotification(notification); + } + + /** + * Shows a notification message the root. The position and behavior of the + * message depends on the type, which is one of the basic types defined in + * {@link Notification}, for instance Notification.TYPE_WARNING_MESSAGE. + * + * Care should be taken to to avoid XSS vulnerabilities as the caption is + * rendered as html. + * + * @see #showNotification(Notification) + * @see Notification + * + * @param caption + * The message + * @param type + * The message type + * + * @deprecated As of 7.0, use Notification.show instead but be aware that + * Notification.show does not allow HTML. + */ + @Deprecated + public void showNotification(String caption, int type) { + Notification notification = new Notification(caption, type); + notification.setHtmlContentAllowed(true);// Backwards compatibility + getPage().showNotification(notification); + } + + /** + * Shows a notification consisting of a bigger caption and a smaller + * description on the middle of the root. The message automatically + * disappears ("humanized message"). + * + * Care should be taken to to avoid XSS vulnerabilities as the caption and + * description are rendered as html. + * + * @see #showNotification(Notification) + * @see Notification + * + * @param caption + * The caption of the message + * @param description + * The message description + * + * @deprecated As of 7.0, use new Notification(...).show(Page) instead but + * be aware that HTML by default not allowed. + */ + @Deprecated + public void showNotification(String caption, String description) { + Notification notification = new Notification(caption, description); + notification.setHtmlContentAllowed(true);// Backwards compatibility + getPage().showNotification(notification); + } + + /** + * Shows a notification consisting of a bigger caption and a smaller + * description. The position and behavior of the message depends on the + * type, which is one of the basic types defined in {@link Notification} , + * for instance Notification.TYPE_WARNING_MESSAGE. + * + * Care should be taken to to avoid XSS vulnerabilities as the caption and + * description are rendered as html. + * + * @see #showNotification(Notification) + * @see Notification + * + * @param caption + * The caption of the message + * @param description + * The message description + * @param type + * The message type + * + * @deprecated As of 7.0, use new Notification(...).show(Page) instead but + * be aware that HTML by default not allowed. + */ + @Deprecated + public void showNotification(String caption, String description, int type) { + Notification notification = new Notification(caption, description, type); + notification.setHtmlContentAllowed(true);// Backwards compatibility + getPage().showNotification(notification); + } + + /** + * Shows a notification consisting of a bigger caption and a smaller + * description. The position and behavior of the message depends on the + * type, which is one of the basic types defined in {@link Notification} , + * for instance Notification.TYPE_WARNING_MESSAGE. + * + * Care should be taken to avoid XSS vulnerabilities if html content is + * allowed. + * + * @see #showNotification(Notification) + * @see Notification + * + * @param caption + * The message caption + * @param description + * The message description + * @param type + * The type of message + * @param htmlContentAllowed + * Whether html in the caption and description should be + * displayed as html or as plain text + * + * @deprecated As of 7.0, use new Notification(...).show(Page). + */ + @Deprecated + public void showNotification(String caption, String description, int type, + boolean htmlContentAllowed) { + getPage() + .showNotification( + new Notification(caption, description, type, + htmlContentAllowed)); + } + + /** + * Shows a notification message. + * + * @see Notification + * @see #showNotification(String) + * @see #showNotification(String, int) + * @see #showNotification(String, String) + * @see #showNotification(String, String, int) + * + * @param notification + * The notification message to show + * + * @deprecated As of 7.0, use Notification.show instead + */ + @Deprecated + public void showNotification(Notification notification) { + getPage().showNotification(notification); + } + +} diff --git a/server/src/com/vaadin/ui/Select.java b/server/src/com/vaadin/ui/Select.java new file mode 100644 index 0000000000..f60935c64b --- /dev/null +++ b/server/src/com/vaadin/ui/Select.java @@ -0,0 +1,803 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import com.vaadin.data.Container; +import com.vaadin.data.util.filter.SimpleStringFilter; +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.terminal.PaintException; +import com.vaadin.terminal.PaintTarget; +import com.vaadin.terminal.Resource; + +/** + * <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.data.Item}s in a + * {@link com.vaadin.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. + * @version + * @VERSION@ + * @since 3.0 + */ +@SuppressWarnings("serial") +public class Select extends AbstractSelect implements AbstractSelect.Filtering, + FieldEvents.BlurNotifier, FieldEvents.FocusNotifier { + + /** + * Holds value of property pageLength. 0 disables paging. + */ + protected int pageLength = 10; + + private int columns = 0; + + // Current page when the user is 'paging' trough options + private int currentPage = -1; + + private int filteringMode = FILTERINGMODE_STARTSWITH; + + 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 if the container is being filtered temporarily and item set change + * notifications should be suppressed. + */ + private boolean filteringContainer; + + /** + * 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; + + /* Constructors */ + + /* 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); + } + + /** + * 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 { + if (isMultiSelect()) { + // background compatibility hack. This object shouldn't be used for + // multiselect lists anymore (ListSelect instead). This fallbacks to + // a simpler paint method in super class. + super.paintContent(target); + // Fix for #4553 + target.addAttribute("type", "legacy-multi"); + return; + } + + // clear caption change listeners + getCaptionChangeListener().clear(); + + // The tab ordering number + if (getTabIndex() != 0) { + target.addAttribute("tabindex", getTabIndex()); + } + + // If the field is modified, but not committed, set modified attribute + if (isModified()) { + target.addAttribute("modified", true); + } + + 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; + if (isMultiSelect()) { + selectedKeys = new String[((Set<?>) getValue()).size()]; + } else { + selectedKeys = new String[(getValue() == null + && getNullSelectionItemId() == null ? 0 : 1)]; + } + + target.addAttribute("pagelength", pageLength); + + target.addAttribute("filteringmode", getFilteringMode()); + + // Paints the options and create array of selected id keys + int keyIndex = 0; + + target.startTag("options"); + + if (currentPage < 0) { + optionRequest = false; + currentPage = 0; + filterstring = ""; + } + + boolean nullFilteredOut = filterstring != null + && !"".equals(filterstring) + && filteringMode != FILTERINGMODE_OFF; + // 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; + + if (paintNullSelection) { + target.startTag("so"); + target.addAttribute("caption", ""); + target.addAttribute("key", ""); + target.endTag("so"); + } + + 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); + + // Paints the option + target.startTag("so"); + if (icon != null) { + target.addAttribute("icon", icon); + } + target.addAttribute("caption", caption); + if (id != null && id.equals(getNullSelectionItemId())) { + target.addAttribute("nullselection", true); + } + target.addAttribute("key", key); + if (isSelected(id) && keyIndex < selectedKeys.length) { + target.addAttribute("selected", true); + selectedKeys[keyIndex++] = key; + } + 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 (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; + } + + /** + * 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 (pageLength == 0) { + // no paging: return all items + filteredSize = container.size(); + 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, filteringMode); + + // 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) { + filteringContainer = true; + filterable.addContainerFilter(filter); + } + + 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 && !isMultiSelect() + && selection != null) { + // ensure proper page + indexToEnsureInView = indexed.indexOfId(selection); + } + + filteredSize = container.size(); + currentPage = adjustCurrentPage(currentPage, needNullSelectOption, + indexToEnsureInView, filteredSize); + int first = getFirstItemIndexOnCurrentPage(needNullSelectOption, + filteredSize); + int last = getLastItemIndexOnCurrentPage(needNullSelectOption, + filteredSize, first); + + List<Object> options = new ArrayList<Object>(); + for (int i = first; i <= last && i < filteredSize; ++i) { + options.add(indexed.getIdByIndex(i)); + } + + // to the outside, filtering should not be visible + if (filter != null) { + filterable.removeContainerFilter(filter); + filteringContainer = false; + } + + return options; + } + + /** + * 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, int filteringMode) { + Filter filter = null; + + if (null != filterString && !"".equals(filterString)) { + switch (filteringMode) { + case FILTERINGMODE_OFF: + break; + case FILTERINGMODE_STARTSWITH: + filter = new SimpleStringFilter(getItemCaptionPropertyId(), + filterString, true, true); + break; + case FILTERINGMODE_CONTAINS: + filter = new SimpleStringFilter(getItemCaptionPropertyId(), + filterString, true, false); + break; + } + } + return filter; + } + + @Override + public void containerItemSetChange(Container.ItemSetChangeEvent event) { + if (!filteringContainer) { + 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 (pageLength != 0 && options.size() > pageLength) { + + 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 && !isMultiSelect() + && 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 * pageLength; + 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 = pageLength + - (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)) + / pageLength; + page = newPage; + } + // adjust the current page if beyond the end of the list + if (page * pageLength > size) { + page = (size + (needNullSelectOption ? 1 : 0)) / pageLength; + } + 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 (null == filterstring || "".equals(filterstring) + || FILTERINGMODE_OFF == filteringMode) { + 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(); + } + switch (filteringMode) { + case FILTERINGMODE_CONTAINS: + if (caption.indexOf(filterstring) > -1) { + filteredOptions.add(itemId); + } + break; + case FILTERINGMODE_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 + + // Selection change + if (variables.containsKey("selected")) { + final String[] ka = (String[]) variables.get("selected"); + + if (isMultiSelect()) { + // Multiselect mode + + // TODO Optimize by adding repaintNotNeeded whan applicaple + + // 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 (id != null && containsId(id)) { + s.add(id); + } + } + + // Limits the deselection to the set of visible items + // (non-visible items can not be deselected) + final Collection<?> visible = getVisibleItemIds(); + if (visible != null) { + @SuppressWarnings("unchecked") + Set<Object> newsel = (Set<Object>) getValue(); + if (newsel == null) { + newsel = new HashSet<Object>(); + } else { + newsel = new HashSet<Object>(newsel); + } + newsel.removeAll(visible); + newsel.addAll(s); + setValue(newsel, true); + } + } else { + // Single select mode + if (ka.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 { + final Object id = itemIdMapper.get(ka[0]); + if (id != null && id.equals(getNullSelectionItemId())) { + setValue(null, true); + } else { + setValue(id, true); + } + } + } + } + + String newFilter; + if ((newFilter = (String) variables.get("filter")) != null) { + // this is a filter request + currentPage = ((Integer) variables.get("page")).intValue(); + filterstring = newFilter; + if (filterstring != null) { + filterstring = filterstring.toLowerCase(); + } + optionRepaint(); + } else if (isNewItemsAllowed()) { + // New option entered (and it is allowed) + final String newitem = (String) variables.get("newitem"); + if (newitem != null && newitem.length() > 0) { + getNewItemHandler().addNewItem(newitem); + // rebuild list + filterstring = null; + prevfilterstring = null; + } + } + + if (variables.containsKey(FocusEvent.EVENT_ID)) { + fireEvent(new FocusEvent(this)); + } + if (variables.containsKey(BlurEvent.EVENT_ID)) { + fireEvent(new BlurEvent(this)); + } + + } + + @Override + public void requestRepaint() { + super.requestRepaint(); + optionRequest = false; + prevfilterstring = filterstring; + filterstring = null; + } + + private void optionRepaint() { + super.requestRepaint(); + } + + @Override + public void setFilteringMode(int filteringMode) { + this.filteringMode = filteringMode; + } + + @Override + public int getFilteringMode() { + return filteringMode; + } + + /** + * Note, one should use more generic setWidth(String) method instead of + * this. This now days actually converts columns to width with em css unit. + * + * Sets the number of columns in the editor. If the number of columns is set + * 0, the actual number of displayed columns is determined implicitly by the + * adapter. + * + * @deprecated + * + * @param columns + * the number of columns to set. + */ + @Deprecated + public void setColumns(int columns) { + if (columns < 0) { + columns = 0; + } + if (this.columns != columns) { + this.columns = columns; + setWidth(columns, Select.UNITS_EM); + requestRepaint(); + } + } + + /** + * @deprecated see setter function + * @return + */ + @Deprecated + public int getColumns() { + return columns; + } + + @Override + public void addListener(BlurListener listener) { + addListener(BlurEvent.EVENT_ID, BlurEvent.class, listener, + BlurListener.blurMethod); + } + + @Override + public void removeListener(BlurListener listener) { + removeListener(BlurEvent.EVENT_ID, BlurEvent.class, listener); + } + + @Override + public void addListener(FocusListener listener) { + addListener(FocusEvent.EVENT_ID, FocusEvent.class, listener, + FocusListener.focusMethod); + } + + @Override + public void removeListener(FocusListener listener) { + removeListener(FocusEvent.EVENT_ID, FocusEvent.class, listener); + + } + + /** + * @deprecated use {@link ListSelect}, {@link OptionGroup} or + * {@link TwinColSelect} instead + * @see com.vaadin.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"); + } + } + + /** + * @deprecated use {@link ListSelect}, {@link OptionGroup} or + * {@link TwinColSelect} instead + * + * @see com.vaadin.ui.AbstractSelect#isMultiSelect() + */ + @Deprecated + @Override + public boolean isMultiSelect() { + return super.isMultiSelect(); + } + + /** + * 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; + } + +} diff --git a/server/src/com/vaadin/ui/Slider.java b/server/src/com/vaadin/ui/Slider.java new file mode 100644 index 0000000000..94afe4e2bd --- /dev/null +++ b/server/src/com/vaadin/ui/Slider.java @@ -0,0 +1,372 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.util.Map; + +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.PaintTarget; +import com.vaadin.terminal.Vaadin6Component; + +/** + * A component for selecting a numerical value within a range. + * + * Example code: <code> + * class MyPlayer extends CustomComponent implements ValueChangeListener { + * + * Label volumeIndicator = new Label(); + * Slider slider; + * + * public MyPlayer() { + * VerticalLayout vl = new VerticalLayout(); + * setCompositionRoot(vl); + * slider = new Slider("Volume", 0, 100); + * slider.setImmediate(true); + * slider.setValue(new Double(50)); + * vl.addComponent(slider); + * vl.addComponent(volumeIndicator); + * volumeIndicator.setValue("Current volume:" + 50.0); + * slider.addListener(this); + * + * } + * + * public void setVolume(double d) { + * volumeIndicator.setValue("Current volume: " + d); + * } + * + * public void valueChange(ValueChangeEvent event) { + * Double d = (Double) event.getProperty().getValue(); + * setVolume(d.doubleValue()); + * } + * } + * + * </code> + * + * @author Vaadin Ltd. + */ +public class Slider extends AbstractField<Double> implements Vaadin6Component { + + public static final int ORIENTATION_HORIZONTAL = 0; + + public static final int ORIENTATION_VERTICAL = 1; + + /** Minimum value of slider */ + private double min = 0; + + /** Maximum value of slider */ + private double max = 100; + + /** + * Resolution, how many digits are considered relevant after the decimal + * point. Must be a non-negative value + */ + private int resolution = 0; + + /** + * Slider orientation (horizontal/vertical), defaults . + */ + private int orientation = ORIENTATION_HORIZONTAL; + + /** + * Default slider constructor. Sets all values to defaults and the slide + * handle at minimum value. + * + */ + public Slider() { + super(); + super.setValue(new Double(min)); + } + + /** + * Create a new slider with the caption given as parameter. + * + * The range of the slider is set to 0-100 and only integer values are + * allowed. + * + * @param caption + * The caption for this slider (e.g. "Volume"). + */ + public Slider(String caption) { + this(); + setCaption(caption); + } + + /** + * Create a new slider with the given range and resolution. + * + * @param min + * The minimum value of the slider + * @param max + * The maximum value of the slider + * @param resolution + * The number of digits after the decimal point. + */ + public Slider(double min, double max, int resolution) { + this(); + setMin(min); + setMax(max); + setResolution(resolution); + } + + /** + * Create a new slider with the given range that only allows integer values. + * + * @param min + * The minimum value of the slider + * @param max + * The maximum value of the slider + */ + public Slider(int min, int max) { + this(); + setMin(min); + setMax(max); + setResolution(0); + } + + /** + * Create a new slider with the given caption and range that only allows + * integer values. + * + * @param caption + * The caption for the slider + * @param min + * The minimum value of the slider + * @param max + * The maximum value of the slider + */ + public Slider(String caption, int min, int max) { + this(min, max); + setCaption(caption); + } + + /** + * Gets the maximum slider value + * + * @return the largest value the slider can have + */ + public double getMax() { + return max; + } + + /** + * Set the maximum slider value. If the current value of the slider is + * larger than this, the value is set to the new maximum. + * + * @param max + * The new maximum slider value + */ + public void setMax(double max) { + this.max = max; + if (getValue() > max) { + setValue(max); + } + requestRepaint(); + } + + /** + * Gets the minimum slider value + * + * @return the smallest value the slider can have + */ + public double getMin() { + return min; + } + + /** + * Set the minimum slider value. If the current value of the slider is + * smaller than this, the value is set to the new minimum. + * + * @param max + * The new minimum slider value + */ + public void setMin(double min) { + this.min = min; + if (getValue() < min) { + setValue(min); + } + requestRepaint(); + } + + /** + * Get the current orientation of the slider (horizontal or vertical). + * + * @return {@link #ORIENTATION_HORIZONTAL} or + * {@link #ORIENTATION_HORIZONTAL} + */ + public int getOrientation() { + return orientation; + } + + /** + * Set the orientation of the slider. + * + * @param The + * new orientation, either {@link #ORIENTATION_HORIZONTAL} or + * {@link #ORIENTATION_VERTICAL} + */ + public void setOrientation(int orientation) { + this.orientation = orientation; + requestRepaint(); + } + + /** + * Get the current resolution of the slider. The resolution is the number of + * digits after the decimal point. + * + * @return resolution + */ + public int getResolution() { + return resolution; + } + + /** + * Set a new resolution for the slider. The resolution is the number of + * digits after the decimal point. + * + * @param resolution + */ + public void setResolution(int resolution) { + if (resolution < 0) { + return; + } + this.resolution = resolution; + requestRepaint(); + } + + /** + * Sets the value of the slider. + * + * @param value + * The new value of the slider. + * @param repaintIsNotNeeded + * If true, client-side is not requested to repaint itself. + * @throws ValueOutOfBoundsException + * If the given value is not inside the range of the slider. + * @see #setMin(double) {@link #setMax(double)} + */ + @Override + protected void setValue(Double value, boolean repaintIsNotNeeded) { + final double v = value.doubleValue(); + double newValue; + if (resolution > 0) { + // Round up to resolution + newValue = (int) (v * Math.pow(10, resolution)); + newValue = newValue / Math.pow(10, resolution); + if (min > newValue || max < newValue) { + throw new ValueOutOfBoundsException(value); + } + } else { + newValue = (int) v; + if (min > newValue || max < newValue) { + throw new ValueOutOfBoundsException(value); + } + } + super.setValue(newValue, repaintIsNotNeeded); + } + + @Override + public void setValue(Object newFieldValue) + throws com.vaadin.data.Property.ReadOnlyException { + if (newFieldValue != null && newFieldValue instanceof Number + && !(newFieldValue instanceof Double)) { + // Support setting all types of Numbers + newFieldValue = ((Number) newFieldValue).doubleValue(); + } + + super.setValue(newFieldValue); + } + + @Override + public void paintContent(PaintTarget target) throws PaintException { + + target.addAttribute("min", min); + if (max > min) { + target.addAttribute("max", max); + } else { + target.addAttribute("max", min); + } + target.addAttribute("resolution", resolution); + + if (resolution > 0) { + target.addVariable(this, "value", getValue().doubleValue()); + } else { + target.addVariable(this, "value", getValue().intValue()); + } + + if (orientation == ORIENTATION_VERTICAL) { + target.addAttribute("vertical", true); + } + + } + + /** + * Invoked when the value of a variable has changed. Slider listeners are + * notified if the slider value has changed. + * + * @param source + * @param variables + */ + @Override + public void changeVariables(Object source, Map<String, Object> variables) { + if (variables.containsKey("value")) { + final Object value = variables.get("value"); + final Double newValue = new Double(value.toString()); + if (newValue != null && newValue != getValue() + && !newValue.equals(getValue())) { + try { + setValue(newValue, true); + } catch (final ValueOutOfBoundsException e) { + // Convert to nearest bound + double out = e.getValue().doubleValue(); + if (out < min) { + out = min; + } + if (out > max) { + out = max; + } + super.setValue(new Double(out), false); + } + } + } + } + + /** + * Thrown when the value of the slider is about to be set to a value that is + * outside the valid range of the slider. + * + * @author Vaadin Ltd. + * + */ + public class ValueOutOfBoundsException extends RuntimeException { + + private final Double value; + + /** + * Constructs an <code>ValueOutOfBoundsException</code> with the + * specified detail message. + * + * @param valueOutOfBounds + */ + public ValueOutOfBoundsException(Double valueOutOfBounds) { + value = valueOutOfBounds; + } + + /** + * Gets the value that is outside the valid range of the slider. + * + * @return the value that is out of bounds + */ + public Double getValue() { + return value; + } + + } + + @Override + public Class<Double> getType() { + return Double.class; + } + +} diff --git a/server/src/com/vaadin/ui/TabSheet.java b/server/src/com/vaadin/ui/TabSheet.java new file mode 100644 index 0000000000..c52e9394c0 --- /dev/null +++ b/server/src/com/vaadin/ui/TabSheet.java @@ -0,0 +1,1328 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.io.Serializable; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +import com.vaadin.event.FieldEvents.BlurEvent; +import com.vaadin.event.FieldEvents.BlurListener; +import com.vaadin.event.FieldEvents.BlurNotifier; +import com.vaadin.event.FieldEvents.FocusEvent; +import com.vaadin.event.FieldEvents.FocusListener; +import com.vaadin.event.FieldEvents.FocusNotifier; +import com.vaadin.terminal.ErrorMessage; +import com.vaadin.terminal.KeyMapper; +import com.vaadin.terminal.LegacyPaint; +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.PaintTarget; +import com.vaadin.terminal.Resource; +import com.vaadin.terminal.Vaadin6Component; +import com.vaadin.terminal.gwt.client.ui.tabsheet.TabsheetBaseConnector; +import com.vaadin.terminal.gwt.client.ui.tabsheet.VTabsheet; +import com.vaadin.ui.Component.Focusable; +import com.vaadin.ui.themes.Reindeer; +import com.vaadin.ui.themes.Runo; + +/** + * TabSheet component. + * + * Tabs are typically identified by the component contained on the tab (see + * {@link ComponentContainer}), and tab metadata (including caption, icon, + * visibility, enabledness, closability etc.) is kept in separate {@link Tab} + * instances. + * + * Tabs added with {@link #addComponent(Component)} get the caption and the icon + * of the component at the time when the component is created, and these are not + * automatically updated after tab creation. + * + * A tab sheet can have multiple tab selection listeners and one tab close + * handler ({@link CloseHandler}), which by default removes the tab from the + * TabSheet. + * + * The {@link TabSheet} can be styled with the .v-tabsheet, .v-tabsheet-tabs and + * .v-tabsheet-content styles. Themes may also have pre-defined variations of + * the tab sheet presentation, such as {@link Reindeer#TABSHEET_BORDERLESS}, + * {@link Runo#TABSHEET_SMALL} and several other styles in {@link Reindeer}. + * + * The current implementation does not load the tabs to the UI before the first + * time they are shown, but this may change in future releases. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +public class TabSheet extends AbstractComponentContainer implements Focusable, + FocusNotifier, BlurNotifier, Vaadin6Component { + + /** + * List of component tabs (tab contents). In addition to being on this list, + * there is a {@link Tab} object in tabs for each tab with meta-data about + * the tab. + */ + private final ArrayList<Component> components = new ArrayList<Component>(); + + /** + * Map containing information related to the tabs (caption, icon etc). + */ + private final HashMap<Component, Tab> tabs = new HashMap<Component, Tab>(); + + /** + * Selected tab content component. + */ + private Component selected = null; + + /** + * Mapper between server-side component instances (tab contents) and keys + * given to the client that identify tabs. + */ + private final KeyMapper<Component> keyMapper = new KeyMapper<Component>(); + + /** + * When true, the tab selection area is not displayed to the user. + */ + private boolean tabsHidden; + + /** + * Handler to be called when a tab is closed. + */ + private CloseHandler closeHandler; + + private int tabIndex; + + /** + * Constructs a new Tabsheet. Tabsheet is immediate by default, and the + * default close handler removes the tab being closed. + */ + public TabSheet() { + super(); + // expand horizontally by default + setWidth(100, UNITS_PERCENTAGE); + setImmediate(true); + setCloseHandler(new CloseHandler() { + + @Override + public void onTabClose(TabSheet tabsheet, Component c) { + tabsheet.removeComponent(c); + } + }); + } + + /** + * Gets the component container iterator for going through all the + * components (tab contents). + * + * @return the unmodifiable Iterator of the tab content components + */ + + @Override + public Iterator<Component> getComponentIterator() { + return Collections.unmodifiableList(components).iterator(); + } + + /** + * Gets the number of contained components (tabs). Consistent with the + * iterator returned by {@link #getComponentIterator()}. + * + * @return the number of contained components + */ + + @Override + public int getComponentCount() { + return components.size(); + } + + /** + * Removes a component and its corresponding tab. + * + * If the tab was selected, the first eligible (visible and enabled) + * remaining tab is selected. + * + * @param c + * the component to be removed. + */ + + @Override + public void removeComponent(Component c) { + if (c != null && components.contains(c)) { + super.removeComponent(c); + keyMapper.remove(c); + components.remove(c); + tabs.remove(c); + if (c.equals(selected)) { + if (components.isEmpty()) { + setSelected(null); + } else { + // select the first enabled and visible tab, if any + updateSelection(); + fireSelectedTabChange(); + } + } + requestRepaint(); + } + } + + /** + * Removes a {@link Tab} and the component associated with it, as previously + * added with {@link #addTab(Component)}, + * {@link #addTab(Component, String, Resource)} or + * {@link #addComponent(Component)}. + * <p> + * If the tab was selected, the first eligible (visible and enabled) + * remaining tab is selected. + * </p> + * + * @see #addTab(Component) + * @see #addTab(Component, String, Resource) + * @see #addComponent(Component) + * @see #removeComponent(Component) + * @param tab + * the Tab to remove + */ + public void removeTab(Tab tab) { + removeComponent(tab.getComponent()); + } + + /** + * Adds a new tab into TabSheet. Component caption and icon are copied to + * the tab metadata at creation time. + * + * @see #addTab(Component) + * + * @param c + * the component to be added. + */ + + @Override + public void addComponent(Component c) { + addTab(c); + } + + /** + * Adds a new tab into TabSheet. + * + * The first tab added to a tab sheet is automatically selected and a tab + * selection event is fired. + * + * If the component is already present in the tab sheet, changes its caption + * and returns the corresponding (old) tab, preserving other tab metadata. + * + * @param c + * the component to be added onto tab - should not be null. + * @param caption + * the caption to be set for the component and used rendered in + * tab bar + * @return the created {@link Tab} + */ + public Tab addTab(Component c, String caption) { + return addTab(c, caption, null); + } + + /** + * Adds a new tab into TabSheet. + * + * The first tab added to a tab sheet is automatically selected and a tab + * selection event is fired. + * + * If the component is already present in the tab sheet, changes its caption + * and icon and returns the corresponding (old) tab, preserving other tab + * metadata. + * + * @param c + * the component to be added onto tab - should not be null. + * @param caption + * the caption to be set for the component and used rendered in + * tab bar + * @param icon + * the icon to be set for the component and used rendered in tab + * bar + * @return the created {@link Tab} + */ + public Tab addTab(Component c, String caption, Resource icon) { + return addTab(c, caption, icon, components.size()); + } + + /** + * Adds a new tab into TabSheet. + * + * The first tab added to a tab sheet is automatically selected and a tab + * selection event is fired. + * + * If the component is already present in the tab sheet, changes its caption + * and icon and returns the corresponding (old) tab, preserving other tab + * metadata like the position. + * + * @param c + * the component to be added onto tab - should not be null. + * @param caption + * the caption to be set for the component and used rendered in + * tab bar + * @param icon + * the icon to be set for the component and used rendered in tab + * bar + * @param position + * the position at where the the tab should be added. + * @return the created {@link Tab} + */ + public Tab addTab(Component c, String caption, Resource icon, int position) { + if (c == null) { + return null; + } else if (tabs.containsKey(c)) { + Tab tab = tabs.get(c); + tab.setCaption(caption); + tab.setIcon(icon); + return tab; + } else { + components.add(position, c); + + Tab tab = new TabSheetTabImpl(caption, icon); + + tabs.put(c, tab); + if (selected == null) { + setSelected(c); + fireSelectedTabChange(); + } + super.addComponent(c); + requestRepaint(); + return tab; + } + } + + /** + * Adds a new tab into TabSheet. Component caption and icon are copied to + * the tab metadata at creation time. + * + * If the tab sheet already contains the component, its tab is returned. + * + * @param c + * the component to be added onto tab - should not be null. + * @return the created {@link Tab} + */ + public Tab addTab(Component c) { + return addTab(c, components.size()); + } + + /** + * Adds a new tab into TabSheet. Component caption and icon are copied to + * the tab metadata at creation time. + * + * If the tab sheet already contains the component, its tab is returned. + * + * @param c + * the component to be added onto tab - should not be null. + * @param position + * The position where the tab should be added + * @return the created {@link Tab} + */ + public Tab addTab(Component c, int position) { + if (c == null) { + return null; + } else if (tabs.containsKey(c)) { + return tabs.get(c); + } else { + return addTab(c, c.getCaption(), c.getIcon(), position); + } + } + + /** + * Moves all components from another container to this container. The + * components are removed from the other container. + * + * If the source container is a {@link TabSheet}, component captions and + * icons are copied from it. + * + * @param source + * the container components are removed from. + */ + + @Override + public void moveComponentsFrom(ComponentContainer source) { + for (final Iterator<Component> i = source.getComponentIterator(); i + .hasNext();) { + final Component c = i.next(); + String caption = null; + Resource icon = null; + if (TabSheet.class.isAssignableFrom(source.getClass())) { + caption = ((TabSheet) source).getTabCaption(c); + icon = ((TabSheet) source).getTabIcon(c); + } + source.removeComponent(c); + addTab(c, caption, icon); + + } + } + + /** + * Paints the content of this component. + * + * @param target + * the paint target + * @throws PaintException + * if the paint operation failed. + */ + + @Override + public void paintContent(PaintTarget target) throws PaintException { + + if (areTabsHidden()) { + target.addAttribute("hidetabs", true); + } + + if (tabIndex != 0) { + target.addAttribute("tabindex", tabIndex); + } + + target.startTag("tabs"); + + for (final Iterator<Component> i = getComponentIterator(); i.hasNext();) { + final Component component = i.next(); + + Tab tab = tabs.get(component); + + target.startTag("tab"); + if (!tab.isEnabled() && tab.isVisible()) { + target.addAttribute( + TabsheetBaseConnector.ATTRIBUTE_TAB_DISABLED, true); + } + + if (!tab.isVisible()) { + target.addAttribute("hidden", true); + } + + if (tab.isClosable()) { + target.addAttribute("closable", true); + } + + // tab icon, caption and description, but used via + // VCaption.updateCaption(uidl) + final Resource icon = tab.getIcon(); + if (icon != null) { + target.addAttribute(TabsheetBaseConnector.ATTRIBUTE_TAB_ICON, + icon); + } + final String caption = tab.getCaption(); + if (caption != null && caption.length() > 0) { + target.addAttribute( + TabsheetBaseConnector.ATTRIBUTE_TAB_CAPTION, caption); + } + ErrorMessage tabError = tab.getComponentError(); + if (tabError != null) { + target.addAttribute( + TabsheetBaseConnector.ATTRIBUTE_TAB_ERROR_MESSAGE, + tabError.getFormattedHtmlMessage()); + } + final String description = tab.getDescription(); + if (description != null) { + target.addAttribute( + TabsheetBaseConnector.ATTRIBUTE_TAB_DESCRIPTION, + description); + } + + final String styleName = tab.getStyleName(); + if (styleName != null && styleName.length() != 0) { + target.addAttribute(VTabsheet.TAB_STYLE_NAME, styleName); + } + + target.addAttribute("key", keyMapper.key(component)); + if (component.equals(selected)) { + target.addAttribute("selected", true); + LegacyPaint.paint(component, target); + } + target.endTag("tab"); + } + + target.endTag("tabs"); + + if (selected != null) { + target.addVariable(this, "selected", keyMapper.key(selected)); + } + + } + + /** + * Are the tab selection parts ("tabs") hidden. + * + * @return true if the tabs are hidden in the UI + */ + public boolean areTabsHidden() { + return tabsHidden; + } + + /** + * Hides or shows the tab selection parts ("tabs"). + * + * @param tabsHidden + * true if the tabs should be hidden + */ + public void hideTabs(boolean tabsHidden) { + this.tabsHidden = tabsHidden; + requestRepaint(); + } + + /** + * Gets tab caption. The tab is identified by the tab content component. + * + * @param c + * the component in the tab + * @deprecated Use {@link #getTab(Component)} and {@link Tab#getCaption()} + * instead. + */ + @Deprecated + public String getTabCaption(Component c) { + Tab info = tabs.get(c); + if (info == null) { + return ""; + } else { + return info.getCaption(); + } + } + + /** + * Sets tab caption. The tab is identified by the tab content component. + * + * @param c + * the component in the tab + * @param caption + * the caption to set. + * @deprecated Use {@link #getTab(Component)} and + * {@link Tab#setCaption(String)} instead. + */ + @Deprecated + public void setTabCaption(Component c, String caption) { + Tab info = tabs.get(c); + if (info != null) { + info.setCaption(caption); + requestRepaint(); + } + } + + /** + * Gets the icon for a tab. The tab is identified by the tab content + * component. + * + * @param c + * the component in the tab + * @deprecated Use {@link #getTab(Component)} and {@link Tab#getIcon()} + * instead. + */ + @Deprecated + public Resource getTabIcon(Component c) { + Tab info = tabs.get(c); + if (info == null) { + return null; + } else { + return info.getIcon(); + } + } + + /** + * Sets icon for the given component. The tab is identified by the tab + * content component. + * + * @param c + * the component in the tab + * @param icon + * the icon to set + * @deprecated Use {@link #getTab(Component)} and + * {@link Tab#setIcon(Resource)} instead. + */ + @Deprecated + public void setTabIcon(Component c, Resource icon) { + Tab info = tabs.get(c); + if (info != null) { + info.setIcon(icon); + requestRepaint(); + } + } + + /** + * Returns the {@link Tab} (metadata) for a component. The {@link Tab} + * object can be used for setting caption,icon, etc for the tab. + * + * @param c + * the component + * @return The tab instance associated with the given component, or null if + * the tabsheet does not contain the component. + */ + public Tab getTab(Component c) { + return tabs.get(c); + } + + /** + * Returns the {@link Tab} (metadata) for a component. The {@link Tab} + * object can be used for setting caption,icon, etc for the tab. + * + * @param position + * the position of the tab + * @return The tab in the given position, or null if the position is out of + * bounds. + */ + public Tab getTab(int position) { + if (position >= 0 && position < getComponentCount()) { + return getTab(components.get(position)); + } else { + return null; + } + } + + /** + * Sets the selected tab. The tab is identified by the tab content + * component. Does nothing if the tabsheet doesn't contain the component. + * + * @param c + */ + public void setSelectedTab(Component c) { + if (c != null && components.contains(c) && !c.equals(selected)) { + setSelected(c); + updateSelection(); + fireSelectedTabChange(); + requestRepaint(); + } + } + + /** + * Sets the selected tab in the TabSheet. Ensures that the selected tab is + * repainted if needed. + * + * @param c + * The new selection or null for no selection + */ + private void setSelected(Component c) { + selected = c; + // Repaint of the selected component is needed as only the selected + // component is communicated to the client. Otherwise this will be a + // "cached" update even though the client knows nothing about the + // connector + if (selected instanceof ComponentContainer) { + ((ComponentContainer) selected).requestRepaintAll(); + } else if (selected instanceof Table) { + // Workaround until there's a generic way of telling a component + // that there is no client side state to rely on. See #8642 + ((Table) selected).refreshRowCache(); + } else if (selected != null) { + selected.requestRepaint(); + } + + } + + /** + * Sets the selected tab. The tab is identified by the corresponding + * {@link Tab Tab} instance. Does nothing if the tabsheet doesn't contain + * the given tab. + * + * @param tab + */ + public void setSelectedTab(Tab tab) { + if (tab != null) { + setSelectedTab(tab.getComponent()); + } + } + + /** + * Sets the selected tab, identified by its position. Does nothing if the + * position is out of bounds. + * + * @param position + */ + public void setSelectedTab(int position) { + setSelectedTab(getTab(position)); + } + + /** + * Checks if the current selection is valid, and updates the selection if + * the previously selected component is not visible and enabled. The first + * visible and enabled tab is selected if the current selection is empty or + * invalid. + * + * This method does not fire tab change events, but the caller should do so + * if appropriate. + * + * @return true if selection was changed, false otherwise + */ + private boolean updateSelection() { + Component originalSelection = selected; + for (final Iterator<Component> i = getComponentIterator(); i.hasNext();) { + final Component component = i.next(); + + Tab tab = tabs.get(component); + + /* + * If we have no selection, if the current selection is invisible or + * if the current selection is disabled (but the whole component is + * not) we select this tab instead + */ + Tab selectedTabInfo = null; + if (selected != null) { + selectedTabInfo = tabs.get(selected); + } + if (selected == null || selectedTabInfo == null + || !selectedTabInfo.isVisible() + || !selectedTabInfo.isEnabled()) { + + // The current selection is not valid so we need to change + // it + if (tab.isEnabled() && tab.isVisible()) { + setSelected(component); + break; + } else { + /* + * The current selection is not valid but this tab cannot be + * selected either. + */ + setSelected(null); + } + } + } + return originalSelection != selected; + } + + /** + * Gets the selected tab content component. + * + * @return the selected tab contents + */ + public Component getSelectedTab() { + return selected; + } + + // inherits javadoc + + @Override + public void changeVariables(Object source, Map<String, Object> variables) { + if (variables.containsKey("selected")) { + setSelectedTab(keyMapper.get((String) variables.get("selected"))); + } + if (variables.containsKey("close")) { + final Component tab = keyMapper + .get((String) variables.get("close")); + if (tab != null) { + closeHandler.onTabClose(this, tab); + } + } + if (variables.containsKey(FocusEvent.EVENT_ID)) { + fireEvent(new FocusEvent(this)); + } + if (variables.containsKey(BlurEvent.EVENT_ID)) { + fireEvent(new BlurEvent(this)); + } + } + + /** + * Replaces a component (tab content) with another. This can be used to + * change tab contents or to rearrange tabs. The tab position and some + * metadata are preserved when moving components within the same + * {@link TabSheet}. + * + * If the oldComponent is not present in the tab sheet, the new one is added + * at the end. + * + * If the oldComponent is already in the tab sheet but the newComponent + * isn't, the old tab is replaced with a new one, and the caption and icon + * of the old one are copied to the new tab. + * + * If both old and new components are present, their positions are swapped. + * + * {@inheritDoc} + */ + + @Override + public void replaceComponent(Component oldComponent, Component newComponent) { + + if (selected == oldComponent) { + // keep selection w/o selectedTabChange event + setSelected(newComponent); + } + + Tab newTab = tabs.get(newComponent); + Tab oldTab = tabs.get(oldComponent); + + // Gets the locations + int oldLocation = -1; + int newLocation = -1; + int location = 0; + for (final Iterator<Component> i = components.iterator(); i.hasNext();) { + final Component component = i.next(); + + if (component == oldComponent) { + oldLocation = location; + } + if (component == newComponent) { + newLocation = location; + } + + location++; + } + + if (oldLocation == -1) { + addComponent(newComponent); + } else if (newLocation == -1) { + removeComponent(oldComponent); + newTab = addTab(newComponent, oldLocation); + // Copy all relevant metadata to the new tab (#8793) + // TODO Should reuse the old tab instance instead? + copyTabMetadata(oldTab, newTab); + } else { + components.set(oldLocation, newComponent); + components.set(newLocation, oldComponent); + + // Tab associations are not changed, but metadata is swapped between + // the instances + // TODO Should reassociate the instances instead? + Tab tmp = new TabSheetTabImpl(null, null); + copyTabMetadata(newTab, tmp); + copyTabMetadata(oldTab, newTab); + copyTabMetadata(tmp, oldTab); + + requestRepaint(); + } + + } + + /* Click event */ + + private static final Method SELECTED_TAB_CHANGE_METHOD; + static { + try { + SELECTED_TAB_CHANGE_METHOD = SelectedTabChangeListener.class + .getDeclaredMethod("selectedTabChange", + new Class[] { SelectedTabChangeEvent.class }); + } catch (final java.lang.NoSuchMethodException e) { + // This should never happen + throw new java.lang.RuntimeException( + "Internal error finding methods in TabSheet"); + } + } + + /** + * Selected tab change event. This event is sent when the selected (shown) + * tab in the tab sheet is changed. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ + public class SelectedTabChangeEvent extends Component.Event { + + /** + * New instance of selected tab change event + * + * @param source + * the Source of the event. + */ + public SelectedTabChangeEvent(Component source) { + super(source); + } + + /** + * TabSheet where the event occurred. + * + * @return the Source of the event. + */ + public TabSheet getTabSheet() { + return (TabSheet) getSource(); + } + } + + /** + * Selected tab change event listener. The listener is called whenever + * another tab is selected, including when adding the first tab to a + * tabsheet. + * + * @author Vaadin Ltd. + * + * @version + * @VERSION@ + * @since 3.0 + */ + public interface SelectedTabChangeListener extends Serializable { + + /** + * Selected (shown) tab in tab sheet has has been changed. + * + * @param event + * the selected tab change event. + */ + public void selectedTabChange(SelectedTabChangeEvent event); + } + + /** + * Adds a tab selection listener + * + * @param listener + * the Listener to be added. + */ + public void addListener(SelectedTabChangeListener listener) { + addListener(SelectedTabChangeEvent.class, listener, + SELECTED_TAB_CHANGE_METHOD); + } + + /** + * Removes a tab selection listener + * + * @param listener + * the Listener to be removed. + */ + public void removeListener(SelectedTabChangeListener listener) { + removeListener(SelectedTabChangeEvent.class, listener, + SELECTED_TAB_CHANGE_METHOD); + } + + /** + * Sends an event that the currently selected tab has changed. + */ + protected void fireSelectedTabChange() { + fireEvent(new SelectedTabChangeEvent(this)); + } + + /** + * Tab meta-data for a component in a {@link TabSheet}. + * + * The meta-data includes the tab caption, icon, visibility and enabledness, + * closability, description (tooltip) and an optional component error shown + * in the tab. + * + * Tabs are identified by the component contained on them in most cases, and + * the meta-data can be obtained with {@link TabSheet#getTab(Component)}. + */ + public interface Tab extends Serializable { + /** + * Returns the visible status for the tab. An invisible tab is not shown + * in the tab bar and cannot be selected. + * + * @return true for visible, false for hidden + */ + public boolean isVisible(); + + /** + * Sets the visible status for the tab. An invisible tab is not shown in + * the tab bar and cannot be selected, selection is changed + * automatically when there is an attempt to select an invisible tab. + * + * @param visible + * true for visible, false for hidden + */ + public void setVisible(boolean visible); + + /** + * Returns the closability status for the tab. + * + * @return true if the tab is allowed to be closed by the end user, + * false for not allowing closing + */ + public boolean isClosable(); + + /** + * Sets the closability status for the tab. A closable tab can be closed + * by the user through the user interface. This also controls if a close + * button is shown to the user or not. + * <p> + * Note! Currently only supported by TabSheet, not Accordion. + * </p> + * + * @param visible + * true if the end user is allowed to close the tab, false + * for not allowing to close. Should default to false. + */ + public void setClosable(boolean closable); + + /** + * Returns the enabled status for the tab. A disabled tab is shown as + * such in the tab bar and cannot be selected. + * + * @return true for enabled, false for disabled + */ + public boolean isEnabled(); + + /** + * Sets the enabled status for the tab. A disabled tab is shown as such + * in the tab bar and cannot be selected. + * + * @param enabled + * true for enabled, false for disabled + */ + public void setEnabled(boolean enabled); + + /** + * Sets the caption for the tab. + * + * @param caption + * the caption to set + */ + public void setCaption(String caption); + + /** + * Gets the caption for the tab. + */ + public String getCaption(); + + /** + * Gets the icon for the tab. + */ + public Resource getIcon(); + + /** + * Sets the icon for the tab. + * + * @param icon + * the icon to set + */ + public void setIcon(Resource icon); + + /** + * Gets the description for the tab. The description can be used to + * briefly describe the state of the tab to the user, and is typically + * shown as a tooltip when hovering over the tab. + * + * @return the description for the tab + */ + public String getDescription(); + + /** + * Sets the description for the tab. The description can be used to + * briefly describe the state of the tab to the user, and is typically + * shown as a tooltip when hovering over the tab. + * + * @param description + * the new description string for the tab. + */ + public void setDescription(String description); + + /** + * Sets an error indicator to be shown in the tab. This can be used e.g. + * to communicate to the user that there is a problem in the contents of + * the tab. + * + * @see AbstractComponent#setComponentError(ErrorMessage) + * + * @param componentError + * error message or null for none + */ + public void setComponentError(ErrorMessage componentError); + + /** + * Gets the current error message shown for the tab. + * + * TODO currently not sent to the client + * + * @see AbstractComponent#setComponentError(ErrorMessage) + */ + public ErrorMessage getComponentError(); + + /** + * Get the component related to the Tab + */ + public Component getComponent(); + + /** + * Sets a style name for the tab. The style name will be rendered as a + * HTML class name, which can be used in a CSS definition. + * + * <pre> + * Tab tab = tabsheet.addTab(tabContent, "Tab text"); + * tab.setStyleName("mystyle"); + * </pre> + * <p> + * The used style name will be prefixed with " + * {@code v-tabsheet-tabitemcell-}". For example, if you give a tab the + * style "{@code mystyle}", the tab will get a " + * {@code v-tabsheet-tabitemcell-mystyle}" style. You could then style + * the component with: + * </p> + * + * <pre> + * .v-tabsheet-tabitemcell-mystyle {font-style: italic;} + * </pre> + * + * <p> + * This method will trigger a {@link RepaintRequestEvent} on the + * TabSheet to which the Tab belongs. + * </p> + * + * @param styleName + * the new style to be set for tab + * @see #getStyleName() + */ + public void setStyleName(String styleName); + + /** + * Gets the user-defined CSS style name of the tab. Built-in style names + * defined in Vaadin or GWT are not returned. + * + * @return the style name or of the tab + * @see #setStyleName(String) + */ + public String getStyleName(); + } + + /** + * TabSheet's implementation of {@link Tab} - tab metadata. + */ + public class TabSheetTabImpl implements Tab { + + private String caption = ""; + private Resource icon = null; + private boolean enabled = true; + private boolean visible = true; + private boolean closable = false; + private String description = null; + private ErrorMessage componentError = null; + private String styleName; + + public TabSheetTabImpl(String caption, Resource icon) { + if (caption == null) { + caption = ""; + } + this.caption = caption; + this.icon = icon; + } + + /** + * Returns the tab caption. Can never be null. + */ + + @Override + public String getCaption() { + return caption; + } + + @Override + public void setCaption(String caption) { + this.caption = caption; + requestRepaint(); + } + + @Override + public Resource getIcon() { + return icon; + } + + @Override + public void setIcon(Resource icon) { + this.icon = icon; + requestRepaint(); + } + + @Override + public boolean isEnabled() { + return enabled; + } + + @Override + public void setEnabled(boolean enabled) { + this.enabled = enabled; + if (updateSelection()) { + fireSelectedTabChange(); + } + requestRepaint(); + } + + @Override + public boolean isVisible() { + return visible; + } + + @Override + public void setVisible(boolean visible) { + this.visible = visible; + if (updateSelection()) { + fireSelectedTabChange(); + } + requestRepaint(); + } + + @Override + public boolean isClosable() { + return closable; + } + + @Override + public void setClosable(boolean closable) { + this.closable = closable; + requestRepaint(); + } + + public void close() { + + } + + @Override + public String getDescription() { + return description; + } + + @Override + public void setDescription(String description) { + this.description = description; + requestRepaint(); + } + + @Override + public ErrorMessage getComponentError() { + return componentError; + } + + @Override + public void setComponentError(ErrorMessage componentError) { + this.componentError = componentError; + requestRepaint(); + } + + @Override + public Component getComponent() { + for (Map.Entry<Component, Tab> entry : tabs.entrySet()) { + if (entry.getValue() == this) { + return entry.getKey(); + } + } + return null; + } + + @Override + public void setStyleName(String styleName) { + this.styleName = styleName; + requestRepaint(); + } + + @Override + public String getStyleName() { + return styleName; + } + } + + /** + * CloseHandler is used to process tab closing events. Default behavior is + * to remove the tab from the TabSheet. + * + * @author Jouni Koivuviita / Vaadin Ltd. + * @since 6.2.0 + * + */ + public interface CloseHandler extends Serializable { + + /** + * Called when a user has pressed the close icon of a tab in the client + * side widget. + * + * @param tabsheet + * the TabSheet to which the tab belongs to + * @param tabContent + * the component that corresponds to the tab whose close + * button was clicked + */ + void onTabClose(final TabSheet tabsheet, final Component tabContent); + } + + /** + * Provide a custom {@link CloseHandler} for this TabSheet if you wish to + * perform some additional tasks when a user clicks on a tabs close button, + * e.g. show a confirmation dialogue before removing the tab. + * + * To remove the tab, if you provide your own close handler, you must call + * {@link #removeComponent(Component)} yourself. + * + * The default CloseHandler for TabSheet will only remove the tab. + * + * @param handler + */ + public void setCloseHandler(CloseHandler handler) { + closeHandler = handler; + } + + /** + * Sets the position of the tab. + * + * @param tab + * The tab + * @param position + * The new position of the tab + */ + public void setTabPosition(Tab tab, int position) { + int oldPosition = getTabPosition(tab); + components.remove(oldPosition); + components.add(position, tab.getComponent()); + requestRepaint(); + } + + /** + * Gets the position of the tab + * + * @param tab + * The tab + * @return + */ + public int getTabPosition(Tab tab) { + return components.indexOf(tab.getComponent()); + } + + @Override + public void focus() { + super.focus(); + } + + @Override + public int getTabIndex() { + return tabIndex; + } + + @Override + public void setTabIndex(int tabIndex) { + this.tabIndex = tabIndex; + requestRepaint(); + } + + @Override + public void addListener(BlurListener listener) { + addListener(BlurEvent.EVENT_ID, BlurEvent.class, listener, + BlurListener.blurMethod); + } + + @Override + public void removeListener(BlurListener listener) { + removeListener(BlurEvent.EVENT_ID, BlurEvent.class, listener); + } + + @Override + public void addListener(FocusListener listener) { + addListener(FocusEvent.EVENT_ID, FocusEvent.class, listener, + FocusListener.focusMethod); + } + + @Override + public void removeListener(FocusListener listener) { + removeListener(FocusEvent.EVENT_ID, FocusEvent.class, listener); + + } + + @Override + public boolean isComponentVisible(Component childComponent) { + return childComponent == getSelectedTab(); + } + + /** + * Copies properties from one Tab to another. + * + * @param from + * The tab whose data to copy. + * @param to + * The tab to which copy the data. + */ + private static void copyTabMetadata(Tab from, Tab to) { + to.setCaption(from.getCaption()); + to.setIcon(from.getIcon()); + to.setDescription(from.getDescription()); + to.setVisible(from.isVisible()); + to.setEnabled(from.isEnabled()); + to.setClosable(from.isClosable()); + to.setStyleName(from.getStyleName()); + to.setComponentError(from.getComponentError()); + } +} diff --git a/server/src/com/vaadin/ui/Table.java b/server/src/com/vaadin/ui/Table.java new file mode 100644 index 0000000000..39b7fb7473 --- /dev/null +++ b/server/src/com/vaadin/ui/Table.java @@ -0,0 +1,5449 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.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.Map; +import java.util.Set; +import java.util.StringTokenizer; +import java.util.logging.Level; +import java.util.logging.Logger; + +import com.vaadin.data.Container; +import com.vaadin.data.Item; +import com.vaadin.data.Property; +import com.vaadin.data.util.ContainerOrderedWrapper; +import com.vaadin.data.util.IndexedContainer; +import com.vaadin.data.util.converter.Converter; +import com.vaadin.data.util.converter.ConverterUtil; +import com.vaadin.event.Action; +import com.vaadin.event.Action.Handler; +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.shared.MouseEventDetails; +import com.vaadin.terminal.KeyMapper; +import com.vaadin.terminal.LegacyPaint; +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.PaintTarget; +import com.vaadin.terminal.Resource; +import com.vaadin.terminal.gwt.client.ui.table.VScrollTable; + +/** + * <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. + * @version + * @VERSION@ + * @since 3.0 + */ +@SuppressWarnings({ "deprecation" }) +public class Table extends AbstractSelect implements Action.Container, + Container.Ordered, Container.Sortable, ItemClickNotifier, DragSource, + DropTarget, HasComponents { + + 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 from 7.0, use {@link Align#LEFT} instead + */ + @Deprecated + public static final Align ALIGN_LEFT = Align.LEFT; + + /** + * @deprecated from 7.0, use {@link Align#CENTER} instead + */ + @Deprecated + public static final Align ALIGN_CENTER = Align.CENTER; + + /** + * @deprecated from 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 from 7.0, use {@link ColumnHeaderMode#HIDDEN} instead + */ + @Deprecated + public static final ColumnHeaderMode COLUMN_HEADER_MODE_HIDDEN = ColumnHeaderMode.HIDDEN; + + /** + * @deprecated from 7.0, use {@link ColumnHeaderMode#ID} instead + */ + @Deprecated + public static final ColumnHeaderMode COLUMN_HEADER_MODE_ID = ColumnHeaderMode.ID; + + /** + * @deprecated from 7.0, use {@link ColumnHeaderMode#EXPLICIT} instead + */ + @Deprecated + public static final ColumnHeaderMode COLUMN_HEADER_MODE_EXPLICIT = ColumnHeaderMode.EXPLICIT; + + /** + * @deprecated from 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 from 7.0, use {@link RowHeaderMode#HIDDEN} instead + */ + @Deprecated + public static final RowHeaderMode ROW_HEADER_MODE_HIDDEN = RowHeaderMode.HIDDEN; + + /** + * @deprecated from 7.0, use {@link RowHeaderMode#ID} instead + */ + @Deprecated + public static final RowHeaderMode ROW_HEADER_MODE_ID = RowHeaderMode.ID; + + /** + * @deprecated from 7.0, use {@link RowHeaderMode#ITEM} instead + */ + @Deprecated + public static final RowHeaderMode ROW_HEADER_MODE_ITEM = RowHeaderMode.ITEM; + + /** + * @deprecated from 7.0, use {@link RowHeaderMode#INDEX} instead + */ + @Deprecated + public static final RowHeaderMode ROW_HEADER_MODE_INDEX = RowHeaderMode.INDEX; + + /** + * @deprecated from 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 from 7.0, use {@link RowHeaderMode#EXPLICIT} instead + */ + @Deprecated + public static final RowHeaderMode ROW_HEADER_MODE_EXPLICIT = RowHeaderMode.EXPLICIT; + + /** + * @deprecated from 7.0, use {@link RowHeaderMode#ICON_ONLY} instead + */ + @Deprecated + public static final RowHeaderMode ROW_HEADER_MODE_ICON_ONLY = RowHeaderMode.ICON_ONLY; + + /** + * @deprecated from 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() { + }; + + /* 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 (Integer) or expand ratios (Float) for + * visible columns (by propertyId). + */ + private final HashMap<Object, Object> columnWidths = new HashMap<Object, Object>(); + + /** + * 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; + + /** + * Index of the first item on the current page. + */ + private int currentPageFirstItemIndex = 0; + + /** + * Holds value of property selectable. + */ + private boolean selectable = false; + + /** + * 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; + + /* Table constructors */ + + /** + * Creates a new empty table. + */ + public Table() { + setRowHeaderMode(ROW_HEADER_MODE_HIDDEN); + } + + /** + * 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"); + } + + // TODO add error check that no duplicate identifiers exist + + // Checks that the new visible columns contains no nulls and properties + // exist + 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]); + } + } + + // If this is called before the constructor is finished, it might be + // uninitialized + final LinkedList<Object> newVC = new LinkedList<Object>(); + for (int i = 0; i < visibleColumns.length; i++) { + newVC.add(visibleColumns[i]); + } + + // Removes alignments, icons and headers from hidden columns + if (this.visibleColumns != null) { + boolean disabledHere = disableContentRefreshing(); + try { + for (final Iterator<Object> i = this.visibleColumns.iterator(); i + .hasNext();) { + final Object col = i.next(); + if (!newVC.contains(col)) { + setColumnHeader(col, null); + setColumnAlignment(col, (Align) null); + setColumnIcon(col, null); + } + } + } finally { + if (disabledHere) { + enableContentRefreshing(false); + } + } + } + + this.visibleColumns = newVC; + + // Assures visual refresh + refreshRowCache(); + } + + /** + * Gets the headers of the columns. + * + * <p> + * The headers match the property id:s given my 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 my 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]); + } + + requestRepaint(); + } + + /** + * Gets the icons of the columns. + * + * <p> + * The icons in headers match the property id:s given my 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 my 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]); + } + + requestRepaint(); + } + + /** + * 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 necessary 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 + * colunmns property id + * @param width + * width to be reserved for colunmns 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; + } + if (width < 0) { + columnWidths.remove(propertyId); + } else { + columnWidths.put(propertyId, Integer.valueOf(width)); + } + requestRepaint(); + } + + /** + * 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 (expandRatio < 0) { + columnWidths.remove(propertyId); + } else { + columnWidths.put(propertyId, new Float(expandRatio)); + } + } + + public float getColumnExpandRatio(Object propertyId) { + final Object width = columnWidths.get(propertyId); + if (width == null || !(width instanceof Float)) { + return -1; + } + final Float value = (Float) width; + return value.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 Object width = columnWidths.get(propertyId); + if (width == null || !(width instanceof Integer)) { + return -1; + } + final Integer value = (Integer) width; + return value.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 width set ({@link #setWidth(float, int)} ) 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; + requestRepaint(); + } + } + + /** + * @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() { + + // Priorise 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; + } + + 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 indentifying 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); + } + + requestRepaint(); + } + + /** + * 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); + } + + requestRepaint(); + } + + /** + * Gets the specified column's alignment. + * + * @param propertyId + * the propertyID identifying the column. + * @return the specified column's alignment if it as one; null 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 + */ + 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 (collapsed) { + collapsedColumns.add(propertyId); + } else { + collapsedColumns.remove(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; + requestRepaint(); + } + } + + /* + * 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; + } + + /* + * FIXME #7607 Take somehow into account the case where we want to + * scroll to the bottom so that the last row is completely visible even + * if (table height) / (row height) is not an integer. Reverted the + * original fix because of #8662 regression. + */ + if (newIndex > maxIndex) { + newIndex = maxIndex; + } + + // Refresh first item id + if (items instanceof Container.Indexed) { + try { + currentPageFirstItemId = getIdByIndex(newIndex); + } catch (final IndexOutOfBoundsException e) { + currentPageFirstItemId = null; + } + currentPageFirstItemIndex = newIndex; + } 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); + } + + /** + * Getter for property pageBuffering. + * + * @deprecated functionality is not needed in ajax rendering model + * + * @return the Value of property pageBuffering. + */ + @Deprecated + public boolean isPageBufferingEnabled() { + return true; + } + + /** + * Setter for property pageBuffering. + * + * @deprecated functionality is not needed in ajax rendering model + * + * @param pageBuffering + * the New value of property pageBuffering. + */ + @Deprecated + public void setPageBufferingEnabled(boolean pageBuffering) { + + } + + /** + * Getter for property selectable. + * + * <p> + * The table is not selectable by default. + * </p> + * + * @return the Value of property selectable. + */ + public boolean isSelectable() { + return selectable; + } + + /** + * Setter for property selectable. + * + * <p> + * The table is not selectable by default. + * </p> + * + * @param selectable + * the New value of property selectable. + */ + public void setSelectable(boolean selectable) { + if (this.selectable != selectable) { + this.selectable = selectable; + requestRepaint(); + } + } + + /** + * 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; + requestRepaint(); + } + + } + + /** + * 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 (getParent() == null) { + 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; + } + + setRowCacheInvalidated(true); + requestRepaint(); + } + + /** + * 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 requestRepaint() { + // Overridden only for javadoc + super.requestRepaint(); + } + + @Override + public void requestRepaintAll() { + super.requestRepaintAll(); + + // 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().finest( + "Insert " + rows + " rows at index " + firstIndex + + " to existing page buffer requested"); + + // Page buffer must not become larger than pageLength*cacheRate before + // or after the current page + int minPageBufferIndex = getCurrentPageFirstItemIndex() + - (int) (getPageLength() * getCacheRate()); + if (minPageBufferIndex < 0) { + minPageBufferIndex = 0; + } + + int maxPageBufferIndex = getCurrentPageFirstItemIndex() + + (int) (getPageLength() * (1 + getCacheRate())); + int maxBufferSize = maxPageBufferIndex - minPageBufferIndex; + + if (getPageLength() == 0) { + // If pageLength == 0 then all rows should be rendered + maxBufferSize = pageBuffer[0].length + rows; + } + /* + * 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; + + /* + * firstIndexInPageBuffer is the offset in pageBuffer where the new rows + * will be inserted (firstIndex is the index in the whole table). + * + * E.g. scrolled down to row 1000: firstIndex==1010, + * pageBufferFirstIndex==1000 -> cacheIx==10 + */ + int firstIndexInPageBuffer = firstIndex - pageBufferFirstIndex; + + /* If rows > size available in page buffer */ + if (firstIndexInPageBuffer + rows > maxBufferSize) { + rows = maxBufferSize - firstIndexInPageBuffer; + } + + /* + * "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. + */ + + /* All rows until the insertion point remain, always. */ + int firstCacheRowToRemoveInPageBuffer = firstIndexInPageBuffer; + + /* + * IF there is space remaining in the buffer after the rows have been + * inserted, we can keep more rows. + */ + int numberOfOldRowsAfterInsertedRows = maxBufferSize + - firstIndexInPageBuffer - rows; + if (numberOfOldRowsAfterInsertedRows > 0) { + firstCacheRowToRemoveInPageBuffer += numberOfOldRowsAfterInsertedRows; + } + + if (firstCacheRowToRemoveInPageBuffer <= currentlyCachedRowCount) { + /* + * Unregister all components that fall beyond the cache limits after + * inserting the new rows. + */ + unregisterComponentsAndPropertiesInRows( + firstCacheRowToRemoveInPageBuffer + pageBufferFirstIndex, + currentlyCachedRowCount - firstCacheRowToRemoveInPageBuffer + + pageBufferFirstIndex); + } + + // Calculate the new cache size + int newCachedRowCount = currentlyCachedRowCount; + if (maxBufferSize == 0 || currentlyCachedRowCount < maxBufferSize) { + newCachedRowCount = currentlyCachedRowCount + rows; + if (maxBufferSize > 0 && newCachedRowCount > maxBufferSize) { + newCachedRowCount = maxBufferSize; + } + } + + /* 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 < firstIndexInPageBuffer; row++) { + // Copy the first rows + newPageBuffer[i][row] = pageBuffer[i][row]; + } + for (int row = firstIndexInPageBuffer; row < firstIndexInPageBuffer + + rows; row++) { + // Copy the newly created rows + newPageBuffer[i][row] = cells[i][row - firstIndexInPageBuffer]; + } + for (int row = firstIndexInPageBuffer + rows; row < newCachedRowCount; row++) { + // Move the old rows down below the newly inserted rows + newPageBuffer[i][row] = pageBuffer[i][row - rows]; + } + } + pageBuffer = newPageBuffer; + getLogger().finest( + "Page Buffer now contains " + + pageBuffer[CELL_ITEMID].length + + " rows (" + + pageBufferFirstIndex + + "-" + + (pageBufferFirstIndex + + pageBuffer[CELL_ITEMID].length - 1) + ")"); + return cells; + } + + /** + * 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) { + getLogger().finest( + "Render visible cells for rows " + 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; + } + + // Gets the first item id + Object id; + if (items instanceof Container.Indexed) { + id = getIdByIndex(firstIndex); + } else { + id = firstItemId(); + for (int i = 0; i < firstIndex; i++) { + id = nextItemId(id); + } + } + + 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; + for (int i = 0; i < rows && id != null; i++) { + 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: + cells[CELL_HEADER][i] = getItemCaption(id); + } + cells[CELL_ICON][i] = getItemIcon(id); + } + + 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) { + p = getContainerProperty(id, colids[j]); + } + + 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 in 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]); + value = cg.generateCell(this, id, colids[j]); + 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]) { + value = p.getValue(); + listenProperty(p, oldListenedProperties); + } else if (p != null) { + value = getPropertyValue(id, colids[j], p); + /* + * If returned value is Component (via + * fieldfactory or overridden getPropertyValue) + * we excpect 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 { + value = getPropertyValue(id, colids[j], null); + } + } + } + } + + if (value instanceof Component) { + registerComponent((Component) value); + } + cells[CELL_FIRSTCOL + j][i] = value; + } + + // Gets the next item id + if (items instanceof Container.Indexed) { + int index = firstIndex + i + 1; + if (index < size()) { + id = getIdByIndex(index); + } else { + id = null; + } + } else { + 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 void registerComponent(Component component) { + getLogger().finest( + "Registered " + component.getClass().getSimpleName() + ": " + + component.getCaption()); + if (component.getParent() != this) { + 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) { + getLogger().finest( + "Unregistering components in rows " + 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 oldVisibleComponents + * a set of components that should be unregistered. + */ + protected void unregisterComponent(Component component) { + getLogger().finest( + "Unregistered " + 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); + } + } + } + + /** + * Refreshes the current page contents. + * + * @deprecated should not need to be used + */ + @Deprecated + public void refreshCurrentPage() { + + } + + /** + * 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(int) + */ + 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 cant 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(); + } + + @Override + public void setContainerDataSource(Container newDataSource) { + + disableContentRefreshing(); + + if (newDataSource == null) { + newDataSource = new IndexedContainer(); + } + + // 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(); + } + + // columnGenerators 'override' properties, don't add the same id twice + Collection<Object> col = new LinkedList<Object>(); + for (Iterator<?> it = getContainerPropertyIds().iterator(); it + .hasNext();) { + Object id = it.next(); + if (columnGenerators == null || !columnGenerators.containsKey(id)) { + col.add(id); + } + } + // generators added last + if (columnGenerators != null && columnGenerators.size() > 0) { + col.addAll(columnGenerators.keySet()); + } + + setVisibleColumns(col.toArray()); + + // Assure visual refresh + resetPageBuffer(); + + enableContentRefreshing(true); + } + + /** + * Gets items ids from a range of key values + * + * @param startRowKey + * The start key + * @param endRowKey + * The end key + * @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 + requestRepaint(); + } 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 + requestRepaint(); + 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.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(); + // sanity check + if (reqFirstRowToPaint + reqRowsToPaint > size()) { + reqRowsToPaint = size() - reqFirstRowToPaint; + } + } + } + getLogger().finest( + "Client wants rows " + 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"); + for (final Iterator<Object> it = visibleColumns.iterator(); it + .hasNext();) { + setColumnCollapsed(it.next(), false); + } + for (int i = 0; i < ids.length; i++) { + setColumnCollapsed(columnIdMap.get(ids[i].toString()), + 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 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 + requestRepaint(); + } + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.AbstractSelect#paintContent(com.vaadin. + * terminal.PaintTarget) + */ + + @Override + public void paintContent(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); + /* + * 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. + * + * This could probably be done for full repaints also to simplify + * the client side. + */ + int pageBufferLastIndex = pageBufferFirstIndex + + pageBuffer[CELL_ITEMID].length - 1; + target.addAttribute(VScrollTable.ATTRIBUTE_PAGEBUFFER_FIRST, + pageBufferFirstIndex); + target.addAttribute(VScrollTable.ATTRIBUTE_PAGEBUFFER_LAST, + pageBufferLastIndex); + } else if (target.isFullRepaint() || isRowCacheInvalidated()) { + paintRows(target, cells, actionSet); + setRowCacheInvalidated(false); + } + + paintSorting(target); + + resetVariablesAndPageBuffer(target); + + // Actions + paintActions(target, actionSet); + + paintColumnOrder(target); + + // Available columns + paintAvailableColumns(target); + + paintVisibleColumns(target); + + if (keyMapperReset) { + keyMapperReset = false; + target.addAttribute(VScrollTable.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"); + } + + 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().finest( + "Paint rows for add. Index: " + firstIx + ", count: " + + 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().finest( + "Paint rows for remove. Index: " + firstIx + ", count: " + + count + "."); + removeRowsFromCacheAndFillBottom(firstIx, count); + target.addAttribute("hide", true); + } + + target.addAttribute("firstprowix", firstIx); + target.addAttribute("numprows", count); + target.endTag("prows"); + } + + /** + * 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); + + 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()); + } + } + + /** + * 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); + 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); + 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)) { + if (getColumnWidth(columnId) > -1) { + target.addAttribute("width", + String.valueOf(getColumnWidth(columnId))); + } else { + target.addAttribute("er", getColumnExpandRatio(columnId)); + } + } + } + + private boolean rowHeadersAreEnabled() { + return getRowHeaderMode() != ROW_HEADER_MODE_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(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) { + target.addText(""); + paintCellTooltips(target, itemId, columnId); + } 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(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 Field. 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 { + ConverterUtil.getConverter(String.class, property.getType(), + getApplication()); + } + Object value = property.getValue(); + if (converter != null) { + return converter.convertToPresentation(value, 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.data.Property.ValueChangeListener#valueChange(Property.ValueChangeEvent) + */ + + @Override + public void valueChange(Property.ValueChangeEvent event) { + if (event.getProperty() == this + || event.getProperty() == getPropertyDataSource()) { + super.valueChange(event); + } else { + refreshRowCache(); + containerChangeToBeRendered = true; + } + requestRepaint(); + } + + /** + * 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.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.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.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); + + return super.removeContainerProperty(propertyId); + } + + /** + * Adds a new property to the table and show it as a visible column. + * + * @param propertyId + * the Id of the proprty. + * @param type + * the class of the property. + * @param defaultValue + * the default value given for all existing items. + * @see com.vaadin.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 proprty + * @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.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.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.data.Container.ItemSetChangeListener#containerItemSetChange(com.vaadin.data.Container.ItemSetChangeEvent) + */ + + @Override + public void containerItemSetChange(Container.ItemSetChangeEvent event) { + super.containerItemSetChange(event); + + // super method clears the key map, must inform client about this to + // avoid getting invalid keys back (#8584) + keyMapperReset = true; + + // ensure that page still has first item in page, ignore buffer refresh + // (forced in this method) + setCurrentPageFirstItemIndex(getCurrentPageFirstItemIndex(), false); + refreshRowCache(); + } + + /** + * Container datasource property set change. Table must flush its buffers on + * change. + * + * @see com.vaadin.data.Container.PropertySetChangeListener#containerPropertySetChange(com.vaadin.data.Container.PropertySetChangeEvent) + */ + + @Override + public void containerPropertySetChange( + Container.PropertySetChangeEvent event) { + disableContentRefreshing(); + super.containerPropertySetChange(event); + + // sanitetize 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.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.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.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.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.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.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.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.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.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 Field instances. + * @see #isEditable + */ + public TableFieldFactory getTableFieldFactory() { + return fieldFactory; + } + + /** + * Is table editable. + * + * If table is editable a editor of type Field 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 implementins the + * FieldFactory interface, and assign it to table, and set the editable + * property to true. + * + * @return true if table is editable, false oterwise. + * @see Field + * @see FieldFactory + * + */ + public boolean isEditable() { + return editable; + } + + /** + * Sets the editable property. + * + * If table is editable a editor of type Field 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 implementins 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.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(); + ((Container.Sortable) c).sort(propertyId, ascending); + setCurrentPageFirstItemIndex(pageIndex); + refreshRowCache(); + + } 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.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 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 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; + requestRepaint(); + } + } + + /** + * 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 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(Object itemId, Object propertyId); + } + + @Override + public void addListener(ItemClickListener listener) { + addListener(VScrollTable.ITEM_CLICK_EVENT_ID, ItemClickEvent.class, + listener, ItemClickEvent.ITEM_CLICK_METHOD); + } + + @Override + public void removeListener(ItemClickListener listener) { + removeListener(VScrollTable.ITEM_CLICK_EVENT_ID, ItemClickEvent.class, + 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 { + requestRepaintAll(); + } + } + + /** + * 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; + requestRepaint(); + } + + /** + * @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; + requestRepaint(); + } + + /** + * 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.terminal.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 propety 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 propety 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 addListener(HeaderClickListener listener) { + addListener(VScrollTable.HEADER_CLICK_EVENT_ID, HeaderClickEvent.class, + listener, HeaderClickEvent.HEADER_CLICK_METHOD); + } + + /** + * Removes a header click listener + * + * @param listener + * The listener to remove. + */ + public void removeListener(HeaderClickListener listener) { + removeListener(VScrollTable.HEADER_CLICK_EVENT_ID, + HeaderClickEvent.class, 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 addListener(FooterClickListener listener) { + addListener(VScrollTable.FOOTER_CLICK_EVENT_ID, FooterClickEvent.class, + listener, FooterClickEvent.FOOTER_CLICK_METHOD); + } + + /** + * Removes a footer click listener + * + * @param listener + * The listener to remove. + */ + public void removeListener(FooterClickListener listener) { + removeListener(VScrollTable.FOOTER_CLICK_EVENT_ID, + FooterClickEvent.class, 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); + } + + requestRepaint(); + } + + /** + * 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; + requestRepaint(); + } + } + + /** + * 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 addListener(ColumnResizeListener listener) { + addListener(VScrollTable.COLUMN_RESIZE_EVENT_ID, + ColumnResizeEvent.class, listener, + ColumnResizeEvent.COLUMN_RESIZE_METHOD); + } + + /** + * Removes a column resize listener from the Table. + * + * @param listener + * The listener to remove + */ + public void removeListener(ColumnResizeListener listener) { + removeListener(VScrollTable.COLUMN_RESIZE_EVENT_ID, + ColumnResizeEvent.class, 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); + } + + /** + * 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 addListener(ColumnReorderListener listener) { + addListener(VScrollTable.COLUMN_REORDER_EVENT_ID, + ColumnReorderEvent.class, listener, ColumnReorderEvent.METHOD); + } + + /** + * Removes a column reorder listener from the Table. + * + * @param listener + * The listener to remove + */ + public void removeListener(ColumnReorderListener listener) { + removeListener(VScrollTable.COLUMN_REORDER_EVENT_ID, + ColumnReorderEvent.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"); + } + // FIXME: This check should be here but primitive types like Boolean + // formatter for boolean property must be handled + + // if (!converter.getSourceType().isAssignableFrom(getType(propertyId))) + // { + // throw new IllegalArgumentException("Property type (" + // + getType(propertyId) + // + ") must match converter source type (" + // + converter.getSourceType() + ")"); + // } + 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() { + return getComponentIterator(); + } + + @Override + public Iterator<Component> getComponentIterator() { + if (visibleComponents == null) { + Collection<Component> empty = Collections.emptyList(); + return empty.iterator(); + } + + return visibleComponents.iterator(); + } + + @Override + public boolean isComponentVisible(Component childComponent) { + return true; + } + + private final Logger getLogger() { + if (logger == null) { + logger = Logger.getLogger(Table.class.getName()); + } + return logger; + } +} diff --git a/server/src/com/vaadin/ui/TableFieldFactory.java b/server/src/com/vaadin/ui/TableFieldFactory.java new file mode 100644 index 0000000000..6c9a641aa8 --- /dev/null +++ b/server/src/com/vaadin/ui/TableFieldFactory.java @@ -0,0 +1,45 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.ui; + +import java.io.Serializable; + +import com.vaadin.data.Container; + +/** + * Factory interface for creating new Field-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. + * @version + * @VERSION@ + * @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/server/src/com/vaadin/ui/TextArea.java b/server/src/com/vaadin/ui/TextArea.java new file mode 100644 index 0000000000..d7837dd33f --- /dev/null +++ b/server/src/com/vaadin/ui/TextArea.java @@ -0,0 +1,121 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import com.vaadin.data.Property; +import com.vaadin.shared.ui.textarea.TextAreaState; + +/** + * 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 + public TextAreaState getState() { + return (TextAreaState) super.getState(); + } + + /** + * 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().setRows(rows); + requestRepaint(); + } + + /** + * Gets the number of rows in the text area. + * + * @return number of explicitly set rows. + */ + public int getRows() { + return getState().getRows(); + } + + /** + * 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().setWordwrap(wordwrap); + requestRepaint(); + } + + /** + * 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().isWordwrap(); + } + +} diff --git a/server/src/com/vaadin/ui/TextField.java b/server/src/com/vaadin/ui/TextField.java new file mode 100644 index 0000000000..567e9c1c10 --- /dev/null +++ b/server/src/com/vaadin/ui/TextField.java @@ -0,0 +1,92 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import com.vaadin.data.Property; + +/** + * <p> + * A text editor component that can be bound to any bindable Property. The text + * editor supports both multiline and single line modes, default is one-line + * mode. + * </p> + * + * <p> + * Since <code>TextField</code> extends <code>AbstractField</code> it implements + * the {@link com.vaadin.data.Buffered} interface. A <code>TextField</code> is + * in write-through mode by default, so + * {@link com.vaadin.ui.AbstractField#setWriteThrough(boolean)} must be called + * to enable buffering. + * </p> + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +@SuppressWarnings("serial") +public class TextField extends AbstractTextField { + + /** + * Constructs an empty <code>TextField</code> with no caption. + */ + public TextField() { + setValue(""); + } + + /** + * Constructs an empty <code>TextField</code> with given caption. + * + * @param caption + * the caption <code>String</code> for the editor. + */ + public TextField(String caption) { + this(); + setCaption(caption); + } + + /** + * Constructs a new <code>TextField</code> that's bound to the specified + * <code>Property</code> and has no caption. + * + * @param dataSource + * the Property to be edited with this editor. + */ + public TextField(Property dataSource) { + setPropertyDataSource(dataSource); + } + + /** + * Constructs a new <code>TextField</code> that's bound to the specified + * <code>Property</code> and has the given caption <code>String</code>. + * + * @param caption + * the caption <code>String</code> for the editor. + * @param dataSource + * the Property to be edited with this editor. + */ + public TextField(String caption, Property dataSource) { + this(dataSource); + setCaption(caption); + } + + /** + * Constructs a new <code>TextField</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)} + * is called to bind it. + * + * @param caption + * the caption <code>String</code> for the editor. + * @param value + * the initial text content of the editor. + */ + public TextField(String caption, String value) { + setValue(value); + setCaption(caption); + } + +} diff --git a/server/src/com/vaadin/ui/Tree.java b/server/src/com/vaadin/ui/Tree.java new file mode 100644 index 0000000000..c15975d879 --- /dev/null +++ b/server/src/com/vaadin/ui/Tree.java @@ -0,0 +1,1615 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.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 com.vaadin.data.Container; +import com.vaadin.data.Item; +import com.vaadin.data.util.ContainerHierarchicalWrapper; +import com.vaadin.data.util.IndexedContainer; +import com.vaadin.event.Action; +import com.vaadin.event.Action.Handler; +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.shared.MouseEventDetails; +import com.vaadin.shared.ui.dd.VerticalDropLocation; +import com.vaadin.terminal.KeyMapper; +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.PaintTarget; +import com.vaadin.terminal.Resource; +import com.vaadin.terminal.gwt.client.ui.tree.TreeConnector; +import com.vaadin.terminal.gwt.client.ui.tree.VTree; +import com.vaadin.tools.ReflectTools; + +/** + * Tree component. A Tree can be used to select an item (or multiple items) from + * a hierarchical set of items. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +@SuppressWarnings({ "serial", "deprecation" }) +public class Tree extends AbstractSelect implements Container.Hierarchical, + Action.Container, ItemClickNotifier, DragSource, DropTarget { + + /* Private members */ + + /** + * Set of expanded nodes. + */ + private final 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() { + } + + /** + * Creates a new empty tree with caption. + * + * @param caption + */ + public Tree(String caption) { + setCaption(caption); + } + + /** + * Creates a new tree with caption and connect it to a Container. + * + * @param caption + * @param dataSource + */ + public Tree(String caption, Container dataSource) { + setCaption(caption); + setContainerDataSource(dataSource); + } + + /* 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); + requestRepaint(); + 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 iff 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) { + requestRepaint(); + } else if (sendChildTree) { + requestPartialRepaint(); + } + fireExpandEvent(itemId); + + return true; + } + + @Override + public void requestRepaint() { + super.requestRepaint(); + partialUpdate = false; + } + + private void requestPartialRepaint() { + super.requestRepaint(); + 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)); + } + } + requestRepaint(); + 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); + requestRepaint(); + 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; + requestRepaint(); + } + } + + /** + * 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; + requestRepaint(); + } + } + + /** + * 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); + 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 + requestRepaint(); + } else if (id != null && containsId(id)) { + s.add(id); + } + } + + if (!isNullSelectionAllowed() && s.size() < 1) { + // empty selection not allowed, keep old value + requestRepaint(); + 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.ordinal()); + } + } 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()); + } + + } + + // 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(itemId); + if (stylename != null) { + target.addAttribute(TreeConnector.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(TreeConnector.ATTRIBUTE_NODE_CAPTION, + getItemCaption(itemId)); + final Resource icon = getItemIcon(itemId); + if (icon != null) { + target.addAttribute(TreeConnector.ATTRIBUTE_NODE_ICON, + getItemIcon(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(TreeConnector.ATTRIBUTE_ACTION_CAPTION, + a.getCaption()); + } + if (a.getIcon() != null) { + target.addAttribute(TreeConnector.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.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.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.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.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.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.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.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) { + requestRepaint(); + } + 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) { + requestRepaint(); + } + return success; + } + + /* Overriding select behavior */ + + /** + * Sets the Container that serves as the data source of the viewer. + * + * @see com.vaadin.data.Container.Viewer#setContainerDataSource(Container) + */ + @Override + public void setContainerDataSource(Container newDataSource) { + if (newDataSource == null) { + // Note: using wrapped IndexedContainer to match constructor (super + // creates an IndexedContainer, which is then wrapped). + newDataSource = new ContainerHierarchicalWrapper( + new IndexedContainer()); + } + + // 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)); + } + } + + /* 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. + * @version + * @VERSION@ + * @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. + * @version + * @VERSION@ + * @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 addListener(ExpandListener listener) { + addListener(ExpandEvent.class, listener, ExpandListener.EXPAND_METHOD); + } + + /** + * Removes the expand listener. + * + * @param listener + * the Listener to be removed. + */ + public void removeListener(ExpandListener listener) { + removeListener(ExpandEvent.class, listener, + ExpandListener.EXPAND_METHOD); + } + + /** + * 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. + * @version + * @VERSION@ + * @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. + * @version + * @VERSION@ + * @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 addListener(CollapseListener listener) { + addListener(CollapseEvent.class, listener, + CollapseListener.COLLAPSE_METHOD); + } + + /** + * Removes the collapse listener. + * + * @param listener + * the Listener to be removed. + */ + public void removeListener(CollapseListener listener) { + removeListener(CollapseEvent.class, listener, + CollapseListener.COLLAPSE_METHOD); + } + + /** + * 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); + requestRepaint(); + } + } + } + + /** + * 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; + } + + requestRepaint(); + } + } + + /** + * Removes all action handlers + */ + public void removeAllActionHandlers() { + actionHandlers = null; + actionMapper = null; + requestRepaint(); + } + + /** + * Gets the visible item ids. + * + * @see com.vaadin.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.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.ui.Select#setNewItemsAllowed(boolean) + */ + @Override + public void setNewItemsAllowed(boolean allowNewOptions) + throws UnsupportedOperationException { + if (allowNewOptions) { + throw new UnsupportedOperationException(); + } + } + + /** + * Tree does not support lazy options loading mode. Setting this true will + * throw UnsupportedOperationException. + * + * @see com.vaadin.ui.Select#setLazyLoading(boolean) + */ + public void setLazyLoading(boolean useLazyLoading) { + if (useLazyLoading) { + throw new UnsupportedOperationException( + "Lazy options loading is not supported by Tree."); + } + } + + private ItemStyleGenerator itemStyleGenerator; + + private DropHandler dropHandler; + + @Override + public void addListener(ItemClickListener listener) { + addListener(VTree.ITEM_CLICK_EVENT_ID, ItemClickEvent.class, listener, + ItemClickEvent.ITEM_CLICK_METHOD); + } + + @Override + public void removeListener(ItemClickListener listener) { + removeListener(VTree.ITEM_CLICK_EVENT_ID, ItemClickEvent.class, + 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; + requestRepaint(); + } + } + + /** + * @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 cell content is + * <tt>v-tree-node-[style name]</tt>. + */ + public interface ItemStyleGenerator extends Serializable { + + /** + * Called by Tree when an item is painted. + * + * @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(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; + requestRepaint(); + } + + /** + * @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.terminal.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; + requestRepaint(); + } + } + + /** + * Get the item description generator which generates tooltips for tree + * items + */ + public ItemDescriptionGenerator getItemDescriptionGenerator() { + return itemDescriptionGenerator; + } + +} diff --git a/server/src/com/vaadin/ui/TreeTable.java b/server/src/com/vaadin/ui/TreeTable.java new file mode 100644 index 0000000000..6132b652f7 --- /dev/null +++ b/server/src/com/vaadin/ui/TreeTable.java @@ -0,0 +1,824 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.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.logging.Logger; + +import com.vaadin.data.Collapsible; +import com.vaadin.data.Container; +import com.vaadin.data.Container.Hierarchical; +import com.vaadin.data.Container.ItemSetChangeEvent; +import com.vaadin.data.util.ContainerHierarchicalWrapper; +import com.vaadin.data.util.HierarchicalContainer; +import com.vaadin.data.util.HierarchicalContainerOrderedWrapper; +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.PaintTarget; +import com.vaadin.terminal.Resource; +import com.vaadin.terminal.gwt.client.ui.treetable.TreeTableConnector; +import com.vaadin.ui.Tree.CollapseEvent; +import com.vaadin.ui.Tree.CollapseListener; +import com.vaadin.ui.Tree.ExpandEvent; +import com.vaadin.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().finest("Item " + itemId + " is now expanded"); + } else { + getLogger().finest("Item " + itemId + " is now collapsed"); + } + 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 + 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; + } + requestRepaint(); + } + + @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( + TreeTableConnector.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) { + requestRepaint(); + } 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 instanceof Hierarchical)) { + newDataSource = new ContainerHierarchicalWrapper(newDataSource); + } + + if (!(newDataSource instanceof Ordered)) { + newDataSource = new HierarchicalContainerOrderedWrapper( + (Hierarchical) newDataSource); + } + + super.setContainerDataSource(newDataSource); + } + + @Override + public void containerItemSetChange( + com.vaadin.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 addListener(ExpandListener listener) { + addListener(ExpandEvent.class, listener, ExpandListener.EXPAND_METHOD); + } + + /** + * Removes an expand listener. + * + * @param listener + * the Listener to be removed. + */ + public void removeListener(ExpandListener listener) { + removeListener(ExpandEvent.class, listener, + ExpandListener.EXPAND_METHOD); + } + + /** + * 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 addListener(CollapseListener listener) { + addListener(CollapseEvent.class, listener, + CollapseListener.COLLAPSE_METHOD); + } + + /** + * Removes a collapse listener. + * + * @param listener + * the Listener to be removed. + */ + public void removeListener(CollapseListener listener) { + removeListener(CollapseEvent.class, listener, + CollapseListener.COLLAPSE_METHOD); + } + + /** + * 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; + requestRepaint(); + } + + private static final Logger getLogger() { + return Logger.getLogger(TreeTable.class.getName()); + } + +} diff --git a/server/src/com/vaadin/ui/TwinColSelect.java b/server/src/com/vaadin/ui/TwinColSelect.java new file mode 100644 index 0000000000..5539236f77 --- /dev/null +++ b/server/src/com/vaadin/ui/TwinColSelect.java @@ -0,0 +1,180 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.util.Collection; + +import com.vaadin.data.Container; +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.PaintTarget; +import com.vaadin.terminal.gwt.client.ui.twincolselect.VTwinColSelect; + +/** + * 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 columns = 0; + 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); + } + + /** + * Sets the number of columns in the editor. If the number of columns is set + * 0, the actual number of displayed columns is determined implicitly by the + * adapter. + * <p> + * The number of columns overrides the value set by setWidth. Only if + * columns are set to 0 (default) the width set using + * {@link #setWidth(float, int)} or {@link #setWidth(String)} is used. + * + * @param columns + * the number of columns to set. + */ + public void setColumns(int columns) { + if (columns < 0) { + columns = 0; + } + if (this.columns != columns) { + this.columns = columns; + requestRepaint(); + } + } + + public int getColumns() { + return columns; + } + + 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; + requestRepaint(); + } + } + + /** + * @param caption + * @param options + */ + public TwinColSelect(String caption, Collection<?> options) { + super(caption, options); + setMultiSelect(true); + } + + @Override + public void paintContent(PaintTarget target) throws PaintException { + target.addAttribute("type", "twincol"); + // Adds the number of columns + if (columns != 0) { + target.addAttribute("cols", 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(VTwinColSelect.ATTRIBUTE_LEFT_CAPTION, lc); + } + if (rc != null) { + target.addAttribute(VTwinColSelect.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; + requestRepaint(); + } + + /** + * 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; + requestRepaint(); + } + + /** + * Returns the text shown above the left column. + * + * @return The text shown or null if not set. + */ + public String getLeftColumnCaption() { + return leftColumnCaption; + } + +} diff --git a/server/src/com/vaadin/ui/UniqueSerializable.java b/server/src/com/vaadin/ui/UniqueSerializable.java new file mode 100644 index 0000000000..828b285538 --- /dev/null +++ b/server/src/com/vaadin/ui/UniqueSerializable.java @@ -0,0 +1,30 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.ui; + +import java.io.Serializable; + +/** + * A base class for generating an unique object that is serializable. + * <p> + * This class is abstract but has no abstract methods to force users to create + * an anonymous inner class. Otherwise each instance will not be unique. + * + * @author Vaadin Ltd + * @version @VERSION@ + * @since 7.0 + * + */ +public abstract class UniqueSerializable implements Serializable { + + @Override + public int hashCode() { + return getClass().hashCode(); + } + + @Override + public boolean equals(Object obj) { + return getClass() == obj.getClass(); + } +} diff --git a/server/src/com/vaadin/ui/Upload.java b/server/src/com/vaadin/ui/Upload.java new file mode 100644 index 0000000000..9d533b67f6 --- /dev/null +++ b/server/src/com/vaadin/ui/Upload.java @@ -0,0 +1,1055 @@ +/* + * @VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.io.OutputStream; +import java.io.Serializable; +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.Map; + +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.PaintTarget; +import com.vaadin.terminal.StreamVariable.StreamingProgressEvent; +import com.vaadin.terminal.Vaadin6Component; +import com.vaadin.terminal.gwt.server.NoInputStreamException; +import com.vaadin.terminal.gwt.server.NoOutputStreamException; + +/** + * Component for uploading files from client to server. + * + * <p> + * The visible component consists of a file name input box and a browse button + * and an upload submit button to start uploading. + * + * <p> + * The Upload component needs a java.io.OutputStream to write the uploaded data. + * You need to implement the Upload.Receiver interface and return the output + * stream in the receiveUpload() method. + * + * <p> + * You can get an event regarding starting (StartedEvent), progress + * (ProgressEvent), and finishing (FinishedEvent) of upload by implementing + * StartedListener, ProgressListener, and FinishedListener, respectively. The + * FinishedListener is called for both failed and succeeded uploads. If you wish + * to separate between these two cases, you can use SucceededListener + * (SucceededEvenet) and FailedListener (FailedEvent). + * + * <p> + * The upload component does not itself show upload progress, but you can use + * the ProgressIndicator for providing progress feedback by implementing + * ProgressListener and updating the indicator in updateProgress(). + * + * <p> + * Setting upload component immediate initiates the upload as soon as a file is + * selected, instead of the common pattern of file selection field and upload + * button. + * + * <p> + * Note! Because of browser dependent implementations of <input type="file"> + * element, setting size for Upload component is not supported. For some + * browsers setting size may work to some extend. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +@SuppressWarnings("serial") +public class Upload extends AbstractComponent implements Component.Focusable, + Vaadin6Component { + + /** + * Should the field be focused on next repaint? + */ + private final boolean focus = false; + + /** + * The tab order number of this field. + */ + private int tabIndex = 0; + + /** + * The output of the upload is redirected to this receiver. + */ + private Receiver receiver; + + private boolean isUploading; + + private long contentLength = -1; + + private int totalBytes; + + private String buttonCaption = "Upload"; + + /** + * ProgressListeners to which information about progress is sent during + * upload + */ + private LinkedHashSet<ProgressListener> progressListeners; + + private boolean interrupted = false; + + private boolean notStarted; + + private int nextid; + + /** + * Flag to indicate that submitting file has been requested. + */ + private boolean forceSubmit; + + /** + * Creates a new instance of Upload. + * + * The receiver must be set before performing an upload. + */ + public Upload() { + } + + public Upload(String caption, Receiver uploadReceiver) { + setCaption(caption); + receiver = uploadReceiver; + } + + /** + * 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) { + if (variables.containsKey("pollForStart")) { + int id = (Integer) variables.get("pollForStart"); + if (!isUploading && id == nextid) { + notStarted = true; + requestRepaint(); + } else { + } + } + } + + /** + * Paints the content of this component. + * + * @param target + * Target to paint the content on. + * @throws PaintException + * if the paint operation failed. + */ + @Override + public void paintContent(PaintTarget target) throws PaintException { + if (notStarted) { + target.addAttribute("notStarted", true); + notStarted = false; + return; + } + if (forceSubmit) { + target.addAttribute("forceSubmit", true); + forceSubmit = true; + return; + } + // The field should be focused + if (focus) { + target.addAttribute("focus", true); + } + + // The tab ordering number + if (tabIndex >= 0) { + target.addAttribute("tabindex", tabIndex); + } + + target.addAttribute("state", isUploading); + + if (buttonCaption != null) { + target.addAttribute("buttoncaption", buttonCaption); + } + + target.addAttribute("nextid", nextid); + + // Post file to this strean variable + target.addVariable(this, "action", getStreamVariable()); + + } + + /** + * Interface that must be implemented by the upload receivers to provide the + * Upload component an output stream to write the uploaded data. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ + public interface Receiver extends Serializable { + + /** + * Invoked when a new upload arrives. + * + * @param filename + * the desired filename of the upload, usually as specified + * by the client. + * @param mimeType + * the MIME type of the uploaded file. + * @return Stream to which the uploaded file should be written. + */ + public OutputStream receiveUpload(String filename, String mimeType); + + } + + /* Upload events */ + + private static final Method UPLOAD_FINISHED_METHOD; + + private static final Method UPLOAD_FAILED_METHOD; + + private static final Method UPLOAD_SUCCEEDED_METHOD; + + private static final Method UPLOAD_STARTED_METHOD; + + static { + try { + UPLOAD_FINISHED_METHOD = FinishedListener.class.getDeclaredMethod( + "uploadFinished", new Class[] { FinishedEvent.class }); + UPLOAD_FAILED_METHOD = FailedListener.class.getDeclaredMethod( + "uploadFailed", new Class[] { FailedEvent.class }); + UPLOAD_STARTED_METHOD = StartedListener.class.getDeclaredMethod( + "uploadStarted", new Class[] { StartedEvent.class }); + UPLOAD_SUCCEEDED_METHOD = SucceededListener.class + .getDeclaredMethod("uploadSucceeded", + new Class[] { SucceededEvent.class }); + } catch (final java.lang.NoSuchMethodException e) { + // This should never happen + throw new java.lang.RuntimeException( + "Internal error finding methods in Upload"); + } + } + + /** + * Upload.FinishedEvent is sent when the upload receives a file, regardless + * of whether the reception was successful or failed. If you wish to + * distinguish between the two cases, use either SucceededEvent or + * FailedEvent, which are both subclasses of the FinishedEvent. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ + public static class FinishedEvent extends Component.Event { + + /** + * Length of the received file. + */ + private final long length; + + /** + * MIME type of the received file. + */ + private final String type; + + /** + * Received file name. + */ + private final String filename; + + /** + * + * @param source + * the source of the file. + * @param filename + * the received file name. + * @param MIMEType + * the MIME type of the received file. + * @param length + * the length of the received file. + */ + public FinishedEvent(Upload source, String filename, String MIMEType, + long length) { + super(source); + type = MIMEType; + this.filename = filename; + this.length = length; + } + + /** + * Uploads where the event occurred. + * + * @return the Source of the event. + */ + public Upload getUpload() { + return (Upload) getSource(); + } + + /** + * Gets the file name. + * + * @return the filename. + */ + public String getFilename() { + return filename; + } + + /** + * Gets the MIME Type of the file. + * + * @return the MIME type. + */ + public String getMIMEType() { + return type; + } + + /** + * Gets the length of the file. + * + * @return the length. + */ + public long getLength() { + return length; + } + + } + + /** + * Upload.FailedEvent event is sent when the upload is received, but the + * reception is interrupted for some reason. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ + public static class FailedEvent extends FinishedEvent { + + private Exception reason = null; + + /** + * + * @param source + * @param filename + * @param MIMEType + * @param length + * @param exception + */ + public FailedEvent(Upload source, String filename, String MIMEType, + long length, Exception reason) { + this(source, filename, MIMEType, length); + this.reason = reason; + } + + /** + * + * @param source + * @param filename + * @param MIMEType + * @param length + * @param exception + */ + public FailedEvent(Upload source, String filename, String MIMEType, + long length) { + super(source, filename, MIMEType, length); + } + + /** + * Gets the exception that caused the failure. + * + * @return the exception that caused the failure, null if n/a + */ + public Exception getReason() { + return reason; + } + + } + + /** + * FailedEvent that indicates that an output stream could not be obtained. + */ + public static class NoOutputStreamEvent extends FailedEvent { + + /** + * + * @param source + * @param filename + * @param MIMEType + * @param length + */ + public NoOutputStreamEvent(Upload source, String filename, + String MIMEType, long length) { + super(source, filename, MIMEType, length); + } + } + + /** + * FailedEvent that indicates that an input stream could not be obtained. + */ + public static class NoInputStreamEvent extends FailedEvent { + + /** + * + * @param source + * @param filename + * @param MIMEType + * @param length + */ + public NoInputStreamEvent(Upload source, String filename, + String MIMEType, long length) { + super(source, filename, MIMEType, length); + } + + } + + /** + * Upload.SucceededEvent event is sent when the upload is received + * successfully. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ + public static class SucceededEvent extends FinishedEvent { + + /** + * + * @param source + * @param filename + * @param MIMEType + * @param length + */ + public SucceededEvent(Upload source, String filename, String MIMEType, + long length) { + super(source, filename, MIMEType, length); + } + + } + + /** + * Upload.StartedEvent event is sent when the upload is started to received. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 5.0 + */ + public static class StartedEvent extends Component.Event { + + private final String filename; + private final String type; + /** + * Length of the received file. + */ + private final long length; + + /** + * + * @param source + * @param filename + * @param MIMEType + * @param length + */ + public StartedEvent(Upload source, String filename, String MIMEType, + long contentLength) { + super(source); + this.filename = filename; + type = MIMEType; + length = contentLength; + } + + /** + * Uploads where the event occurred. + * + * @return the Source of the event. + */ + public Upload getUpload() { + return (Upload) getSource(); + } + + /** + * Gets the file name. + * + * @return the filename. + */ + public String getFilename() { + return filename; + } + + /** + * Gets the MIME Type of the file. + * + * @return the MIME type. + */ + public String getMIMEType() { + return type; + } + + /** + * @return the length of the file that is being uploaded + */ + public long getContentLength() { + return length; + } + + } + + /** + * Receives the events when the upload starts. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 5.0 + */ + public interface StartedListener extends Serializable { + + /** + * Upload has started. + * + * @param event + * the Upload started event. + */ + public void uploadStarted(StartedEvent event); + } + + /** + * Receives the events when the uploads are ready. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ + public interface FinishedListener extends Serializable { + + /** + * Upload has finished. + * + * @param event + * the Upload finished event. + */ + public void uploadFinished(FinishedEvent event); + } + + /** + * Receives events when the uploads are finished, but unsuccessful. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ + public interface FailedListener extends Serializable { + + /** + * Upload has finished unsuccessfully. + * + * @param event + * the Upload failed event. + */ + public void uploadFailed(FailedEvent event); + } + + /** + * Receives events when the uploads are successfully finished. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ + public interface SucceededListener extends Serializable { + + /** + * Upload successfull.. + * + * @param event + * the Upload successfull event. + */ + public void uploadSucceeded(SucceededEvent event); + } + + /** + * Adds the upload started event listener. + * + * @param listener + * the Listener to be added. + */ + public void addListener(StartedListener listener) { + addListener(StartedEvent.class, listener, UPLOAD_STARTED_METHOD); + } + + /** + * Removes the upload started event listener. + * + * @param listener + * the Listener to be removed. + */ + public void removeListener(StartedListener listener) { + removeListener(StartedEvent.class, listener, UPLOAD_STARTED_METHOD); + } + + /** + * Adds the upload received event listener. + * + * @param listener + * the Listener to be added. + */ + public void addListener(FinishedListener listener) { + addListener(FinishedEvent.class, listener, UPLOAD_FINISHED_METHOD); + } + + /** + * Removes the upload received event listener. + * + * @param listener + * the Listener to be removed. + */ + public void removeListener(FinishedListener listener) { + removeListener(FinishedEvent.class, listener, UPLOAD_FINISHED_METHOD); + } + + /** + * Adds the upload interrupted event listener. + * + * @param listener + * the Listener to be added. + */ + public void addListener(FailedListener listener) { + addListener(FailedEvent.class, listener, UPLOAD_FAILED_METHOD); + } + + /** + * Removes the upload interrupted event listener. + * + * @param listener + * the Listener to be removed. + */ + public void removeListener(FailedListener listener) { + removeListener(FailedEvent.class, listener, UPLOAD_FAILED_METHOD); + } + + /** + * Adds the upload success event listener. + * + * @param listener + * the Listener to be added. + */ + public void addListener(SucceededListener listener) { + addListener(SucceededEvent.class, listener, UPLOAD_SUCCEEDED_METHOD); + } + + /** + * Removes the upload success event listener. + * + * @param listener + * the Listener to be removed. + */ + public void removeListener(SucceededListener listener) { + removeListener(SucceededEvent.class, listener, UPLOAD_SUCCEEDED_METHOD); + } + + /** + * Adds the upload success event listener. + * + * @param listener + * the Listener to be added. + */ + public void addListener(ProgressListener listener) { + if (progressListeners == null) { + progressListeners = new LinkedHashSet<ProgressListener>(); + } + progressListeners.add(listener); + } + + /** + * Removes the upload success event listener. + * + * @param listener + * the Listener to be removed. + */ + public void removeListener(ProgressListener listener) { + if (progressListeners != null) { + progressListeners.remove(listener); + } + } + + /** + * Emit upload received event. + * + * @param filename + * @param MIMEType + * @param length + */ + protected void fireStarted(String filename, String MIMEType) { + fireEvent(new Upload.StartedEvent(this, filename, MIMEType, + contentLength)); + } + + /** + * Emits the upload failed event. + * + * @param filename + * @param MIMEType + * @param length + */ + protected void fireUploadInterrupted(String filename, String MIMEType, + long length) { + fireEvent(new Upload.FailedEvent(this, filename, MIMEType, length)); + } + + protected void fireNoInputStream(String filename, String MIMEType, + long length) { + fireEvent(new Upload.NoInputStreamEvent(this, filename, MIMEType, + length)); + } + + protected void fireNoOutputStream(String filename, String MIMEType, + long length) { + fireEvent(new Upload.NoOutputStreamEvent(this, filename, MIMEType, + length)); + } + + protected void fireUploadInterrupted(String filename, String MIMEType, + long length, Exception e) { + fireEvent(new Upload.FailedEvent(this, filename, MIMEType, length, e)); + } + + /** + * Emits the upload success event. + * + * @param filename + * @param MIMEType + * @param length + * + */ + protected void fireUploadSuccess(String filename, String MIMEType, + long length) { + fireEvent(new Upload.SucceededEvent(this, filename, MIMEType, length)); + } + + /** + * Emits the progress event. + * + * @param totalBytes + * bytes received so far + * @param contentLength + * actual size of the file being uploaded, if known + * + */ + protected void fireUpdateProgress(long totalBytes, long contentLength) { + // this is implemented differently than other listeners to maintain + // backwards compatibility + if (progressListeners != null) { + for (Iterator<ProgressListener> it = progressListeners.iterator(); it + .hasNext();) { + ProgressListener l = it.next(); + l.updateProgress(totalBytes, contentLength); + } + } + } + + /** + * Returns the current receiver. + * + * @return the StreamVariable. + */ + public Receiver getReceiver() { + return receiver; + } + + /** + * Sets the receiver. + * + * @param receiver + * the receiver to set. + */ + public void setReceiver(Receiver receiver) { + this.receiver = receiver; + } + + /** + * {@inheritDoc} + */ + @Override + public void focus() { + super.focus(); + } + + /** + * Gets the Tabulator index of this Focusable component. + * + * @see com.vaadin.ui.Component.Focusable#getTabIndex() + */ + @Override + public int getTabIndex() { + return tabIndex; + } + + /** + * Sets the Tabulator index of this Focusable component. + * + * @see com.vaadin.ui.Component.Focusable#setTabIndex(int) + */ + @Override + public void setTabIndex(int tabIndex) { + this.tabIndex = tabIndex; + } + + /** + * Go into upload state. This is to prevent double uploading on same + * component. + * + * Warning: this is an internal method used by the framework and should not + * be used by user of the Upload component. Using it results in the Upload + * component going in wrong state and not working. It is currently public + * because it is used by another class. + */ + public void startUpload() { + if (isUploading) { + throw new IllegalStateException("uploading already started"); + } + isUploading = true; + nextid++; + } + + /** + * Interrupts the upload currently being received. The interruption will be + * done by the receiving tread so this method will return immediately and + * the actual interrupt will happen a bit later. + */ + public void interruptUpload() { + if (isUploading) { + interrupted = true; + } + } + + /** + * Go into state where new uploading can begin. + * + * Warning: this is an internal method used by the framework and should not + * be used by user of the Upload component. + */ + private void endUpload() { + isUploading = false; + contentLength = -1; + interrupted = false; + requestRepaint(); + } + + public boolean isUploading() { + return isUploading; + } + + /** + * Gets read bytes of the file currently being uploaded. + * + * @return bytes + */ + public long getBytesRead() { + return totalBytes; + } + + /** + * Returns size of file currently being uploaded. Value sane only during + * upload. + * + * @return size in bytes + */ + public long getUploadSize() { + return contentLength; + } + + /** + * This method is deprecated, use addListener(ProgressListener) instead. + * + * @deprecated Use addListener(ProgressListener) instead. + * @param progressListener + */ + @Deprecated + public void setProgressListener(ProgressListener progressListener) { + addListener(progressListener); + } + + /** + * This method is deprecated. + * + * @deprecated Replaced with addListener/removeListener + * @return listener + * + */ + @Deprecated + public ProgressListener getProgressListener() { + if (progressListeners == null || progressListeners.isEmpty()) { + return null; + } else { + return progressListeners.iterator().next(); + } + } + + /** + * ProgressListener receives events to track progress of upload. + */ + public interface ProgressListener extends Serializable { + /** + * Updates progress to listener + * + * @param readBytes + * bytes transferred + * @param contentLength + * total size of file currently being uploaded, -1 if unknown + */ + public void updateProgress(long readBytes, long contentLength); + } + + /** + * @return String to be rendered into button that fires uploading + */ + public String getButtonCaption() { + return buttonCaption; + } + + /** + * In addition to the actual file chooser, upload components have button + * that starts actual upload progress. This method is used to set text in + * that button. + * <p> + * In case the button text is set to null, the button is hidden. In this + * case developer must explicitly initiate the upload process with + * {@link #submitUpload()}. + * <p> + * In case the Upload is used in immediate mode using + * {@link #setImmediate(boolean)}, the file choose (html input with type + * "file") is hidden and only the button with this text is shown. + * <p> + * + * <p> + * <strong>Note</strong> the string given is set as is to the button. HTML + * formatting is not stripped. Be sure to properly validate your value + * according to your needs. + * + * @param buttonCaption + * text for upload components button. + */ + public void setButtonCaption(String buttonCaption) { + this.buttonCaption = buttonCaption; + requestRepaint(); + } + + /** + * Forces the upload the send selected file to the server. + * <p> + * In case developer wants to use this feature, he/she will most probably + * want to hide the uploads internal submit button by setting its caption to + * null with {@link #setButtonCaption(String)} method. + * <p> + * Note, that the upload runs asynchronous. Developer should use normal + * upload listeners to trac the process of upload. If the field is empty + * uploaded the file name will be empty string and file length 0 in the + * upload finished event. + * <p> + * Also note, that the developer should not remove or modify the upload in + * the same user transaction where the upload submit is requested. The + * upload may safely be hidden or removed once the upload started event is + * fired. + */ + public void submitUpload() { + requestRepaint(); + forceSubmit = true; + } + + @Override + public void requestRepaint() { + forceSubmit = false; + super.requestRepaint(); + } + + /* + * Handle to terminal via Upload monitors and controls the upload during it + * is being streamed. + */ + private com.vaadin.terminal.StreamVariable streamVariable; + + protected com.vaadin.terminal.StreamVariable getStreamVariable() { + if (streamVariable == null) { + streamVariable = new com.vaadin.terminal.StreamVariable() { + private StreamingStartEvent lastStartedEvent; + + @Override + public boolean listenProgress() { + return (progressListeners != null && !progressListeners + .isEmpty()); + } + + @Override + public void onProgress(StreamingProgressEvent event) { + fireUpdateProgress(event.getBytesReceived(), + event.getContentLength()); + } + + @Override + public boolean isInterrupted() { + return interrupted; + } + + @Override + public OutputStream getOutputStream() { + OutputStream receiveUpload = receiver.receiveUpload( + lastStartedEvent.getFileName(), + lastStartedEvent.getMimeType()); + lastStartedEvent = null; + return receiveUpload; + } + + @Override + public void streamingStarted(StreamingStartEvent event) { + startUpload(); + contentLength = event.getContentLength(); + fireStarted(event.getFileName(), event.getMimeType()); + lastStartedEvent = event; + } + + @Override + public void streamingFinished(StreamingEndEvent event) { + fireUploadSuccess(event.getFileName(), event.getMimeType(), + event.getContentLength()); + endUpload(); + requestRepaint(); + } + + @Override + public void streamingFailed(StreamingErrorEvent event) { + Exception exception = event.getException(); + if (exception instanceof NoInputStreamException) { + fireNoInputStream(event.getFileName(), + event.getMimeType(), 0); + } else if (exception instanceof NoOutputStreamException) { + fireNoOutputStream(event.getFileName(), + event.getMimeType(), 0); + } else { + fireUploadInterrupted(event.getFileName(), + event.getMimeType(), 0, exception); + } + endUpload(); + } + }; + } + return streamVariable; + } + + @Override + public java.util.Collection<?> getListeners(java.lang.Class<?> eventType) { + if (StreamingProgressEvent.class.isAssignableFrom(eventType)) { + if (progressListeners == null) { + return Collections.EMPTY_LIST; + } else { + return Collections.unmodifiableCollection(progressListeners); + } + + } + return super.getListeners(eventType); + }; +} diff --git a/server/src/com/vaadin/ui/VerticalLayout.java b/server/src/com/vaadin/ui/VerticalLayout.java new file mode 100644 index 0000000000..a04d052d98 --- /dev/null +++ b/server/src/com/vaadin/ui/VerticalLayout.java @@ -0,0 +1,25 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.ui; + +/** + * Vertical layout + * + * <code>VerticalLayout</code> is a component container, which shows the + * subcomponents in the order of their addition (vertically). A vertical layout + * is by default 100% wide. + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 5.3 + */ +@SuppressWarnings("serial") +public class VerticalLayout extends AbstractOrderedLayout { + + public VerticalLayout() { + setWidth("100%"); + } + +} diff --git a/server/src/com/vaadin/ui/VerticalSplitPanel.java b/server/src/com/vaadin/ui/VerticalSplitPanel.java new file mode 100644 index 0000000000..0630240e9c --- /dev/null +++ b/server/src/com/vaadin/ui/VerticalSplitPanel.java @@ -0,0 +1,30 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.ui; + +/** + * A vertical split panel contains two components and lays them vertically. The + * first component is above the second component. + * + * <pre> + * +--------------------------+ + * | | + * | The first component | + * | | + * +==========================+ <-- splitter + * | | + * | The second component | + * | | + * +--------------------------+ + * </pre> + * + */ +public class VerticalSplitPanel extends AbstractSplitPanel { + + public VerticalSplitPanel() { + super(); + setSizeFull(); + } + +} diff --git a/server/src/com/vaadin/ui/Video.java b/server/src/com/vaadin/ui/Video.java new file mode 100644 index 0000000000..d4f95a5be3 --- /dev/null +++ b/server/src/com/vaadin/ui/Video.java @@ -0,0 +1,81 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import com.vaadin.shared.ui.video.VideoState; +import com.vaadin.terminal.Resource; +import com.vaadin.terminal.gwt.server.ResourceReference; + +/** + * The Video component translates into an HTML5 <video> element and as + * such is only supported in browsers that support HTML5 media markup. Browsers + * that do not support HTML5 display the text or HTML set by calling + * {@link #setAltText(String)}. + * + * A flash-player fallback can be implemented by setting HTML content allowed ( + * {@link #setHtmlContentAllowed(boolean)} and calling + * {@link #setAltText(String)} with the flash player markup. An example of flash + * fallback can be found at the <a href= + * "https://developer.mozilla.org/En/Using_audio_and_video_in_Firefox#Using_Flash" + * >Mozilla Developer Network</a>. + * + * Multiple sources can be specified. Which of the sources is used is selected + * by the browser depending on which file formats it supports. See <a + * href="http://en.wikipedia.org/wiki/HTML5_video#Table">wikipedia</a> for a + * table of formats supported by different browsers. + * + * @author Vaadin Ltd + * @since 6.7.0 + */ +public class Video extends AbstractMedia { + + @Override + public VideoState getState() { + return (VideoState) super.getState(); + } + + public Video() { + this("", null); + } + + /** + * @param caption + * The caption for this video. + */ + public Video(String caption) { + this(caption, null); + } + + /** + * @param caption + * The caption for this video. + * @param source + * The Resource containing the video to play. + */ + public Video(String caption, Resource source) { + setCaption(caption); + setSource(source); + setShowControls(true); + } + + /** + * Sets the poster image, which is shown in place of the video before the + * user presses play. + * + * @param poster + */ + public void setPoster(Resource poster) { + getState().setPoster(ResourceReference.create(poster)); + requestRepaint(); + } + + /** + * @return The poster image. + */ + public Resource getPoster() { + return ResourceReference.getResource(getState().getPoster()); + } + +} diff --git a/server/src/com/vaadin/ui/Window.java b/server/src/com/vaadin/ui/Window.java new file mode 100644 index 0000000000..e413d35e6d --- /dev/null +++ b/server/src/com/vaadin/ui/Window.java @@ -0,0 +1,853 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.io.Serializable; +import java.lang.reflect.Method; +import java.util.Map; + +import com.vaadin.event.FieldEvents.BlurEvent; +import com.vaadin.event.FieldEvents.BlurListener; +import com.vaadin.event.FieldEvents.BlurNotifier; +import com.vaadin.event.FieldEvents.FocusEvent; +import com.vaadin.event.FieldEvents.FocusListener; +import com.vaadin.event.FieldEvents.FocusNotifier; +import com.vaadin.event.MouseEvents.ClickEvent; +import com.vaadin.event.ShortcutAction; +import com.vaadin.event.ShortcutAction.KeyCode; +import com.vaadin.event.ShortcutAction.ModifierKey; +import com.vaadin.event.ShortcutListener; +import com.vaadin.shared.MouseEventDetails; +import com.vaadin.shared.ui.window.WindowServerRpc; +import com.vaadin.shared.ui.window.WindowState; +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.PaintTarget; +import com.vaadin.terminal.Vaadin6Component; +import com.vaadin.terminal.gwt.client.ui.root.VRoot; + +/** + * A component that represents a floating popup window that can be added to a + * {@link Root}. A window is added to a {@code Root} using + * {@link Root#addWindow(Window)}. </p> + * <p> + * The contents of a window is set using {@link #setContent(ComponentContainer)} + * or by using the {@link #Window(String, ComponentContainer)} constructor. The + * contents can in turn contain other components. By default, a + * {@link VerticalLayout} is used as content. + * </p> + * <p> + * A window can be positioned on the screen using absolute coordinates (pixels) + * or set to be centered using {@link #center()} + * </p> + * <p> + * The caption is displayed in the window header. + * </p> + * <p> + * In Vaadin versions prior to 7.0.0, Window was also used as application level + * windows. This function is now covered by the {@link Root} class. + * </p> + * + * @author Vaadin Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +@SuppressWarnings("serial") +public class Window extends Panel implements FocusNotifier, BlurNotifier, + Vaadin6Component { + + private WindowServerRpc rpc = new WindowServerRpc() { + + @Override + public void click(MouseEventDetails mouseDetails) { + fireEvent(new ClickEvent(Window.this, mouseDetails)); + } + }; + + private int browserWindowWidth = -1; + + private int browserWindowHeight = -1; + + /** + * Creates a new unnamed window with a default layout. + */ + public Window() { + this("", null); + } + + /** + * Creates a new unnamed window with a default layout and given title. + * + * @param caption + * the title of the window. + */ + public Window(String caption) { + this(caption, null); + } + + /** + * Creates a new unnamed window with the given content and title. + * + * @param caption + * the title of the window. + * @param content + * the contents of the window + */ + public Window(String caption, ComponentContainer content) { + super(caption, content); + registerRpc(rpc); + setSizeUndefined(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.Panel#addComponent(com.vaadin.ui.Component) + */ + + @Override + public void addComponent(Component c) { + if (c instanceof Window) { + throw new IllegalArgumentException( + "Window cannot be added to another via addComponent. " + + "Use addWindow(Window) instead."); + } + super.addComponent(c); + } + + /* ********************************************************************* */ + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.Panel#paintContent(com.vaadin.terminal.PaintTarget) + */ + + @Override + public synchronized void paintContent(PaintTarget target) + throws PaintException { + if (bringToFront != null) { + target.addAttribute("bringToFront", bringToFront.intValue()); + bringToFront = null; + } + + // Contents of the window panel is painted + super.paintContent(target); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.Panel#changeVariables(java.lang.Object, java.util.Map) + */ + + @Override + public void changeVariables(Object source, Map<String, Object> variables) { + + // TODO Are these for top level windows or sub windows? + boolean sizeHasChanged = false; + // size is handled in super class, but resize events only in windows -> + // so detect if size change occurs before super.changeVariables() + if (variables.containsKey("height") + && (getHeightUnits() != Unit.PIXELS || (Integer) variables + .get("height") != getHeight())) { + sizeHasChanged = true; + } + if (variables.containsKey("width") + && (getWidthUnits() != Unit.PIXELS || (Integer) variables + .get("width") != getWidth())) { + sizeHasChanged = true; + } + Integer browserHeightVar = (Integer) variables + .get(VRoot.BROWSER_HEIGHT_VAR); + if (browserHeightVar != null + && browserHeightVar.intValue() != browserWindowHeight) { + browserWindowHeight = browserHeightVar.intValue(); + sizeHasChanged = true; + } + Integer browserWidthVar = (Integer) variables + .get(VRoot.BROWSER_WIDTH_VAR); + if (browserWidthVar != null + && browserWidthVar.intValue() != browserWindowWidth) { + browserWindowWidth = browserWidthVar.intValue(); + sizeHasChanged = true; + } + + super.changeVariables(source, variables); + + // Positioning + final Integer positionx = (Integer) variables.get("positionx"); + if (positionx != null) { + final int x = positionx.intValue(); + // This is information from the client so it is already using the + // position. No need to repaint. + setPositionX(x < 0 ? -1 : x, false); + } + final Integer positiony = (Integer) variables.get("positiony"); + if (positiony != null) { + final int y = positiony.intValue(); + // This is information from the client so it is already using the + // position. No need to repaint. + setPositionY(y < 0 ? -1 : y, false); + } + + if (isClosable()) { + // Closing + final Boolean close = (Boolean) variables.get("close"); + if (close != null && close.booleanValue()) { + close(); + } + } + + // fire event if size has really changed + if (sizeHasChanged) { + fireResize(); + } + + if (variables.containsKey(FocusEvent.EVENT_ID)) { + fireEvent(new FocusEvent(this)); + } else if (variables.containsKey(BlurEvent.EVENT_ID)) { + fireEvent(new BlurEvent(this)); + } + + } + + /** + * Method that handles window closing (from UI). + * + * <p> + * By default, sub-windows are removed from their respective parent windows + * and thus visually closed on browser-side. Browser-level windows also + * closed on the client-side, but they are not implicitly removed from the + * application. + * </p> + * + * <p> + * To explicitly close a sub-window, use {@link #removeWindow(Window)}. To + * react to a window being closed (after it is closed), register a + * {@link CloseListener}. + * </p> + */ + public void close() { + Root root = getRoot(); + + // Don't do anything if not attached to a root + if (root != null) { + // focus is restored to the parent window + root.focus(); + // subwindow is removed from the root + root.removeWindow(this); + } + } + + /** + * Gets the distance of Window left border in pixels from left border of the + * containing (main window). + * + * @return the Distance of Window left border in pixels from left border of + * the containing (main window). or -1 if unspecified. + * @since 4.0.0 + */ + public int getPositionX() { + return getState().getPositionX(); + } + + /** + * Sets the distance of Window left border in pixels from left border of the + * containing (main window). + * + * @param positionX + * the Distance of Window left border in pixels from left border + * of the containing (main window). or -1 if unspecified. + * @since 4.0.0 + */ + public void setPositionX(int positionX) { + setPositionX(positionX, true); + } + + /** + * Sets the distance of Window left border in pixels from left border of the + * containing (main window). + * + * @param positionX + * the Distance of Window left border in pixels from left border + * of the containing (main window). or -1 if unspecified. + * @param repaintRequired + * true if the window needs to be repainted, false otherwise + * @since 6.3.4 + */ + private void setPositionX(int positionX, boolean repaintRequired) { + getState().setPositionX(positionX); + getState().setCentered(false); + if (repaintRequired) { + requestRepaint(); + } + } + + /** + * Gets the distance of Window top border in pixels from top border of the + * containing (main window). + * + * @return Distance of Window top border in pixels from top border of the + * containing (main window). or -1 if unspecified . + * + * @since 4.0.0 + */ + public int getPositionY() { + return getState().getPositionY(); + } + + /** + * Sets the distance of Window top border in pixels from top border of the + * containing (main window). + * + * @param positionY + * the Distance of Window top border in pixels from top border of + * the containing (main window). or -1 if unspecified + * + * @since 4.0.0 + */ + public void setPositionY(int positionY) { + setPositionY(positionY, true); + } + + /** + * Sets the distance of Window top border in pixels from top border of the + * containing (main window). + * + * @param positionY + * the Distance of Window top border in pixels from top border of + * the containing (main window). or -1 if unspecified + * @param repaintRequired + * true if the window needs to be repainted, false otherwise + * + * @since 6.3.4 + */ + private void setPositionY(int positionY, boolean repaintRequired) { + getState().setPositionY(positionY); + getState().setCentered(false); + if (repaintRequired) { + requestRepaint(); + } + } + + private static final Method WINDOW_CLOSE_METHOD; + static { + try { + WINDOW_CLOSE_METHOD = CloseListener.class.getDeclaredMethod( + "windowClose", new Class[] { CloseEvent.class }); + } catch (final java.lang.NoSuchMethodException e) { + // This should never happen + throw new java.lang.RuntimeException( + "Internal error, window close method not found"); + } + } + + public class CloseEvent extends Component.Event { + + /** + * + * @param source + */ + public CloseEvent(Component source) { + super(source); + } + + /** + * Gets the Window. + * + * @return the window. + */ + public Window getWindow() { + return (Window) getSource(); + } + } + + /** + * An interface used for listening to Window close events. Add the + * CloseListener to a browser level window or a sub window and + * {@link CloseListener#windowClose(CloseEvent)} will be called whenever the + * user closes the window. + * + * <p> + * Since Vaadin 6.5, removing a window using {@link #removeWindow(Window)} + * fires the CloseListener. + * </p> + */ + public interface CloseListener extends Serializable { + /** + * Called when the user closes a window. Use + * {@link CloseEvent#getWindow()} to get a reference to the + * {@link Window} that was closed. + * + * @param e + * Event containing + */ + public void windowClose(CloseEvent e); + } + + /** + * Adds a CloseListener to the window. + * + * For a sub window the CloseListener is fired when the user closes it + * (clicks on the close button). + * + * For a browser level window the CloseListener is fired when the browser + * level window is closed. Note that closing a browser level window does not + * mean it will be destroyed. Also note that Opera does not send events like + * all other browsers and therefore the close listener might not be called + * if Opera is used. + * + * <p> + * Since Vaadin 6.5, removing windows using {@link #removeWindow(Window)} + * does fire the CloseListener. + * </p> + * + * @param listener + * the CloseListener to add. + */ + public void addListener(CloseListener listener) { + addListener(CloseEvent.class, listener, WINDOW_CLOSE_METHOD); + } + + /** + * Removes the CloseListener from the window. + * + * <p> + * For more information on CloseListeners see {@link CloseListener}. + * </p> + * + * @param listener + * the CloseListener to remove. + */ + public void removeListener(CloseListener listener) { + removeListener(CloseEvent.class, listener, WINDOW_CLOSE_METHOD); + } + + protected void fireClose() { + fireEvent(new Window.CloseEvent(this)); + } + + /** + * Method for the resize event. + */ + private static final Method WINDOW_RESIZE_METHOD; + static { + try { + WINDOW_RESIZE_METHOD = ResizeListener.class.getDeclaredMethod( + "windowResized", new Class[] { ResizeEvent.class }); + } catch (final java.lang.NoSuchMethodException e) { + // This should never happen + throw new java.lang.RuntimeException( + "Internal error, window resized method not found"); + } + } + + /** + * Resize events are fired whenever the client-side fires a resize-event + * (e.g. the browser window is resized). The frequency may vary across + * browsers. + */ + public class ResizeEvent extends Component.Event { + + /** + * + * @param source + */ + public ResizeEvent(Component source) { + super(source); + } + + /** + * Get the window form which this event originated + * + * @return the window + */ + public Window getWindow() { + return (Window) getSource(); + } + } + + /** + * Listener for window resize events. + * + * @see com.vaadin.ui.Window.ResizeEvent + */ + public interface ResizeListener extends Serializable { + public void windowResized(ResizeEvent e); + } + + /** + * Add a resize listener. + * + * @param listener + */ + public void addListener(ResizeListener listener) { + addListener(ResizeEvent.class, listener, WINDOW_RESIZE_METHOD); + } + + /** + * Remove a resize listener. + * + * @param listener + */ + public void removeListener(ResizeListener listener) { + removeListener(ResizeEvent.class, listener); + } + + /** + * Fire the resize event. + */ + protected void fireResize() { + fireEvent(new ResizeEvent(this)); + } + + /** + * Used to keep the right order of windows if multiple windows are brought + * to front in a single changeset. If this is not used, the order is quite + * random (depends on the order getting to dirty list. e.g. which window got + * variable changes). + */ + private Integer bringToFront = null; + + /** + * If there are currently several windows visible, calling this method makes + * this window topmost. + * <p> + * This method can only be called if this window connected a root. Else an + * illegal state exception is thrown. Also if there are modal windows and + * this window is not modal, and illegal state exception is thrown. + * <p> + */ + public void bringToFront() { + Root root = getRoot(); + if (root == null) { + throw new IllegalStateException( + "Window must be attached to parent before calling bringToFront method."); + } + int maxBringToFront = -1; + for (Window w : root.getWindows()) { + if (!isModal() && w.isModal()) { + throw new IllegalStateException( + "The root contains modal windows, non-modal window cannot be brought to front."); + } + if (w.bringToFront != null) { + maxBringToFront = Math.max(maxBringToFront, + w.bringToFront.intValue()); + } + } + bringToFront = Integer.valueOf(maxBringToFront + 1); + requestRepaint(); + } + + /** + * Sets sub-window modal, so that widgets behind it cannot be accessed. + * <b>Note:</b> affects sub-windows only. + * + * @param modal + * true if modality is to be turned on + */ + public void setModal(boolean modal) { + getState().setModal(modal); + center(); + requestRepaint(); + } + + /** + * @return true if this window is modal. + */ + public boolean isModal() { + return getState().isModal(); + } + + /** + * Sets sub-window resizable. <b>Note:</b> affects sub-windows only. + * + * @param resizable + * true if resizability is to be turned on + */ + public void setResizable(boolean resizable) { + getState().setResizable(resizable); + requestRepaint(); + } + + /** + * + * @return true if window is resizable by the end-user, otherwise false. + */ + public boolean isResizable() { + return getState().isResizable(); + } + + /** + * + * @return true if a delay is used before recalculating sizes, false if + * sizes are recalculated immediately. + */ + public boolean isResizeLazy() { + return getState().isResizeLazy(); + } + + /** + * Should resize operations be lazy, i.e. should there be a delay before + * layout sizes are recalculated. Speeds up resize operations in slow UIs + * with the penalty of slightly decreased usability. + * + * Note, some browser send false resize events for the browser window and + * are therefore always lazy. + * + * @param resizeLazy + * true to use a delay before recalculating sizes, false to + * calculate immediately. + */ + public void setResizeLazy(boolean resizeLazy) { + getState().setResizeLazy(resizeLazy); + requestRepaint(); + } + + /** + * Sets this window to be centered relative to its parent window. Affects + * sub-windows only. If the window is resized as a result of the size of its + * content changing, it will keep itself centered as long as its position is + * not explicitly changed programmatically or by the user. + * <p> + * <b>NOTE:</b> This method has several issues as currently implemented. + * Please refer to http://dev.vaadin.com/ticket/8971 for details. + */ + public void center() { + getState().setCentered(true); + requestRepaint(); + } + + /** + * Returns the closable status of the sub window. If a sub window is + * closable it typically shows an X in the upper right corner. Clicking on + * the X sends a close event to the server. Setting closable to false will + * remove the X from the sub window and prevent the user from closing the + * window. + * + * Note! For historical reasons readonly controls the closability of the sub + * window and therefore readonly and closable affect each other. Setting + * readonly to true will set closable to false and vice versa. + * <p/> + * Closable only applies to sub windows, not to browser level windows. + * + * @return true if the sub window can be closed by the user. + */ + public boolean isClosable() { + return !isReadOnly(); + } + + /** + * Sets the closable status for the sub window. If a sub window is closable + * it typically shows an X in the upper right corner. Clicking on the X + * sends a close event to the server. Setting closable to false will remove + * the X from the sub window and prevent the user from closing the window. + * + * Note! For historical reasons readonly controls the closability of the sub + * window and therefore readonly and closable affect each other. Setting + * readonly to true will set closable to false and vice versa. + * <p/> + * Closable only applies to sub windows, not to browser level windows. + * + * @param closable + * determines if the sub window can be closed by the user. + */ + public void setClosable(boolean closable) { + setReadOnly(!closable); + } + + /** + * Indicates whether a sub window can be dragged or not. By default a sub + * window is draggable. + * <p/> + * Draggable only applies to sub windows, not to browser level windows. + * + * @param draggable + * true if the sub window can be dragged by the user + */ + public boolean isDraggable() { + return getState().isDraggable(); + } + + /** + * Enables or disables that a sub window can be dragged (moved) by the user. + * By default a sub window is draggable. + * <p/> + * Draggable only applies to sub windows, not to browser level windows. + * + * @param draggable + * true if the sub window can be dragged by the user + */ + public void setDraggable(boolean draggable) { + getState().setDraggable(draggable); + requestRepaint(); + } + + /* + * Actions + */ + protected CloseShortcut closeShortcut; + + /** + * Makes is possible to close the window by pressing the given + * {@link KeyCode} and (optional) {@link ModifierKey}s.<br/> + * Note that this shortcut only reacts while the window has focus, closing + * itself - if you want to close a subwindow from a parent window, use + * {@link #addAction(com.vaadin.event.Action)} of the parent window instead. + * + * @param keyCode + * the keycode for invoking the shortcut + * @param modifiers + * the (optional) modifiers for invoking the shortcut, null for + * none + */ + public void setCloseShortcut(int keyCode, int... modifiers) { + if (closeShortcut != null) { + removeAction(closeShortcut); + } + closeShortcut = new CloseShortcut(this, keyCode, modifiers); + addAction(closeShortcut); + } + + /** + * Removes the keyboard shortcut previously set with + * {@link #setCloseShortcut(int, int...)}. + */ + public void removeCloseShortcut() { + if (closeShortcut != null) { + removeAction(closeShortcut); + closeShortcut = null; + } + } + + /** + * A {@link ShortcutListener} specifically made to define a keyboard + * shortcut that closes the window. + * + * <pre> + * <code> + * // within the window using helper + * subWindow.setCloseShortcut(KeyCode.ESCAPE, null); + * + * // or globally + * getWindow().addAction(new Window.CloseShortcut(subWindow, KeyCode.ESCAPE)); + * </code> + * </pre> + * + */ + public static class CloseShortcut extends ShortcutListener { + protected Window window; + + /** + * Creates a keyboard shortcut for closing the given window using the + * shorthand notation defined in {@link ShortcutAction}. + * + * @param window + * to be closed when the shortcut is invoked + * @param shorthandCaption + * the caption with shortcut keycode and modifiers indicated + */ + public CloseShortcut(Window window, String shorthandCaption) { + super(shorthandCaption); + this.window = window; + } + + /** + * Creates a keyboard shortcut for closing the given window using the + * given {@link KeyCode} and {@link ModifierKey}s. + * + * @param window + * to be closed when the shortcut is invoked + * @param keyCode + * KeyCode to react to + * @param modifiers + * optional modifiers for shortcut + */ + public CloseShortcut(Window window, int keyCode, int... modifiers) { + super(null, keyCode, modifiers); + this.window = window; + } + + /** + * Creates a keyboard shortcut for closing the given window using the + * given {@link KeyCode}. + * + * @param window + * to be closed when the shortcut is invoked + * @param keyCode + * KeyCode to react to + */ + public CloseShortcut(Window window, int keyCode) { + this(window, keyCode, null); + } + + @Override + public void handleAction(Object sender, Object target) { + window.close(); + } + } + + /** + * Note, that focus/blur listeners in Window class are only supported by sub + * windows. Also note that Window is not considered focused if its contained + * component currently has focus. + * + * @see com.vaadin.event.FieldEvents.FocusNotifier#addListener(com.vaadin.event.FieldEvents.FocusListener) + */ + + @Override + public void addListener(FocusListener listener) { + addListener(FocusEvent.EVENT_ID, FocusEvent.class, listener, + FocusListener.focusMethod); + } + + @Override + public void removeListener(FocusListener listener) { + removeListener(FocusEvent.EVENT_ID, FocusEvent.class, listener); + } + + /** + * Note, that focus/blur listeners in Window class are only supported by sub + * windows. Also note that Window is not considered focused if its contained + * component currently has focus. + * + * @see com.vaadin.event.FieldEvents.BlurNotifier#addListener(com.vaadin.event.FieldEvents.BlurListener) + */ + + @Override + public void addListener(BlurListener listener) { + addListener(BlurEvent.EVENT_ID, BlurEvent.class, listener, + BlurListener.blurMethod); + } + + @Override + public void removeListener(BlurListener listener) { + removeListener(BlurEvent.EVENT_ID, BlurEvent.class, listener); + } + + /** + * {@inheritDoc} + * + * If the window is a sub-window focusing will cause the sub-window to be + * brought on top of other sub-windows on gain keyboard focus. + */ + + @Override + public void focus() { + /* + * When focusing a sub-window it basically means it should be brought to + * the front. Instead of just moving the keyboard focus we focus the + * window and bring it top-most. + */ + super.focus(); + bringToFront(); + } + + @Override + public WindowState getState() { + return (WindowState) super.getState(); + } +} diff --git a/server/src/com/vaadin/ui/doc-files/component_class_hierarchy.gif b/server/src/com/vaadin/ui/doc-files/component_class_hierarchy.gif Binary files differnew file mode 100644 index 0000000000..936c220d11 --- /dev/null +++ b/server/src/com/vaadin/ui/doc-files/component_class_hierarchy.gif diff --git a/server/src/com/vaadin/ui/doc-files/component_interfaces.gif b/server/src/com/vaadin/ui/doc-files/component_interfaces.gif Binary files differnew file mode 100644 index 0000000000..44c99826bb --- /dev/null +++ b/server/src/com/vaadin/ui/doc-files/component_interfaces.gif diff --git a/server/src/com/vaadin/ui/package.html b/server/src/com/vaadin/ui/package.html new file mode 100644 index 0000000000..6b19a28fe7 --- /dev/null +++ b/server/src/com/vaadin/ui/package.html @@ -0,0 +1,76 @@ +<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN"> +<html> +<head> + +</head> + +<body bgcolor="white"> + +<!-- Package summary here --> + +<p>Provides interfaces and classes in Vaadin.</p> + +<h2>Package Specification</h2> + +<p><strong>Interface hierarchy</strong></p> + +<p>The general interface hierarchy looks like this:</p> + +<p style="text-align: center;"><img + src="doc-files/component_interfaces.gif" /></p> + +<p><i>Note that the above picture includes only the main +interfaces. This package includes several other lesser sub-interfaces +which are not significant in this scope. The interfaces not appearing +here are documented with the classes that define them.</i></p> + +<p>The {@link com.vaadin.ui.Component} interface is the top-level +interface which must be implemented by all user interface components in +Vaadin. It defines the common properties of the components and how the +framework will handle them. Most simple components, such as {@link +com.vaadin.ui.Button}, for example, do not need to implement the +lower-level interfaces described below. Notice that also the classes and +interfaces required by the component event framework are defined in +{@link com.vaadin.ui.Component}.</p> + +<p>The next level in the component hierarchy are the classes +implementing the {@link com.vaadin.ui.ComponentContainer} interface. It +adds the capacity to contain other components to {@link +com.vaadin.ui.Component} with a simple API.</p> + +<p>The third and last level is the {@link com.vaadin.ui.Layout}, +which adds the concept of location to the components contained in a +{@link com.vaadin.ui.ComponentContainer}. It can be used to create +containers which contents can be positioned.</p> + +<p><strong>Component class hierarchy</strong></p> + +<p>The actual component classes form a hierarchy like this:</p> + +<center><img src="doc-files/component_class_hierarchy.gif" /></center> +<br /> + +<center><i>Underlined classes are abstract.</i></center> + +<p>At the top level is {@link com.vaadin.ui.AbstractComponent} which +implements the {@link com.vaadin.ui.Component} interface. As the name +suggests it is abstract, but it does include a default implementation +for all methods defined in <code>Component</code> so that a component is +free to override only those functionalities it needs.</p> + +<p>As seen in the picture, <code>AbstractComponent</code> serves as +the superclass for several "real" components, but it also has a some +abstract extensions. {@link com.vaadin.ui.AbstractComponentContainer} +serves as the root class for all components (for example, panels and +windows) who can contain other components. {@link +com.vaadin.ui.AbstractField}, on the other hand, implements several +interfaces to provide a base class for components that are used for data +display and manipulation.</p> + + +<!-- Package spec here --> + +<!-- Put @see and @since tags down here. --> + +</body> +</html> diff --git a/server/src/com/vaadin/ui/themes/BaseTheme.java b/server/src/com/vaadin/ui/themes/BaseTheme.java new file mode 100644 index 0000000000..6f448746bf --- /dev/null +++ b/server/src/com/vaadin/ui/themes/BaseTheme.java @@ -0,0 +1,59 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.ui.themes; + +/** + * <p> + * The Base theme is the foundation for all Vaadin themes. Although it is not + * necessary to use it as the starting point for all other themes, it is heavily + * encouraged, since it abstracts and hides away many necessary style properties + * that the Vaadin terminal expects and needs. + * </p> + * <p> + * When creating your own theme, either extend this class and specify the styles + * implemented in your theme here, or extend some other theme that has a class + * file specified (e.g. Reindeer or Runo). + * </p> + * <p> + * All theme class files should follow the convention of specifying the theme + * name as a string constant <code>THEME_NAME</code>. + * + * @since 6.3.0 + * + */ +public class BaseTheme { + + public static final String THEME_NAME = "base"; + + /** + * Creates a button that looks like a regular hypertext link but still acts + * like a normal button. + */ + public static final String BUTTON_LINK = "link"; + + /** + * Removes extra decorations from the panel. + * + * @deprecated Base theme does not implement this style, but it is defined + * here since it has been a part of the framework before + * multiple themes were available. Use the constant provided by + * the theme you're using instead, e.g. + * {@link Reindeer#PANEL_LIGHT} or {@link Runo#PANEL_LIGHT}. + */ + @Deprecated + public static final String PANEL_LIGHT = "light"; + + /** + * Adds the connector lines between a parent node and its child nodes to + * indicate the tree hierarchy better. + */ + public static final String TREE_CONNECTORS = "connectors"; + + /** + * Clips the component so it will be constrained to its given size and not + * overflow. + */ + public static final String CLIP = "v-clip"; + +}
\ No newline at end of file diff --git a/server/src/com/vaadin/ui/themes/ChameleonTheme.java b/server/src/com/vaadin/ui/themes/ChameleonTheme.java new file mode 100644 index 0000000000..5ae8cd4e57 --- /dev/null +++ b/server/src/com/vaadin/ui/themes/ChameleonTheme.java @@ -0,0 +1,365 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.ui.themes; + +public class ChameleonTheme extends BaseTheme { + + public static final String THEME_NAME = "chameleon"; + + /*************************************************************************** + * Label styles + **************************************************************************/ + + /** + * Large font for main application headings + */ + public static final String LABEL_H1 = "h1"; + + /** + * Large font for different sections in the application + */ + public static final String LABEL_H2 = "h2"; + + /** + * Font for sub-section headers + */ + public static final String LABEL_H3 = "h3"; + + /** + * Font for paragraphs headers + */ + public static final String LABEL_H4 = "h4"; + + /** + * Big font for important or emphasized texts + */ + public static final String LABEL_BIG = "big"; + + /** + * Small and a little lighter font + */ + public static final String LABEL_SMALL = "small"; + + /** + * Very small and lighter font for things such as footnotes and component + * specific informations. Use carefully, since this style will usually + * reduce legibility. + */ + public static final String LABEL_TINY = "tiny"; + + /** + * Adds color to the text (usually the alternate color of the theme) + */ + public static final String LABEL_COLOR = "color"; + + /** + * Adds a warning icon on the left side and a yellow background to the label + */ + public static final String LABEL_WARNING = "warning"; + + /** + * Adds an error icon on the left side and a red background to the label + */ + public static final String LABEL_ERROR = "error"; + + /** + * Adds a spinner icon on the left side of the label + */ + public static final String LABEL_LOADING = "loading"; + + /*************************************************************************** + * Button styles + **************************************************************************/ + + /** + * Default action style for buttons (the button that gets activated when + * user presses 'enter' in a form). Use sparingly, only one default button + * per screen should be visible. + */ + public static final String BUTTON_DEFAULT = "default"; + + /** + * Small sized button, use for context specific actions for example + */ + public static final String BUTTON_SMALL = "small"; + + /** + * Big button, use to get more attention for the button action + */ + public static final String BUTTON_BIG = "big"; + + /** + * Adds more padding on the sides of the button. Makes it easier for the + * user to hit the button. + */ + public static final String BUTTON_WIDE = "wide"; + + /** + * Adds more padding on the top and on the bottom of the button. Makes it + * easier for the user to hit the button. + */ + public static final String BUTTON_TALL = "tall"; + + /** + * Removes all graphics from the button, leaving only the caption and the + * icon visible. Useful for making icon-only buttons and toolbar buttons. + */ + public static final String BUTTON_BORDERLESS = "borderless"; + + /** + * Places the button icon on top of the caption. By default the icon is on + * the left side of the button caption. + */ + public static final String BUTTON_ICON_ON_TOP = "icon-on-top"; + + /** + * Places the button icon on the right side of the caption. By default the + * icon is on the left side of the button caption. + */ + public static final String BUTTON_ICON_ON_RIGHT = "icon-on-right"; + + /** + * Removes the button caption and only shows its icon + */ + public static final String BUTTON_ICON_ONLY = "icon-only"; + + /** + * Makes the button look like it is pressed down. Useful for creating a + * toggle button. + */ + public static final String BUTTON_DOWN = "down"; + + /*************************************************************************** + * TextField styles + **************************************************************************/ + + /** + * Small sized text field with small font + */ + public static final String TEXTFIELD_SMALL = "small"; + + /** + * Large sized text field with big font + */ + public static final String TEXTFIELD_BIG = "big"; + + /** + * Adds a magnifier icon on the left side of the fields text + */ + public static final String TEXTFIELD_SEARCH = "search"; + + /*************************************************************************** + * Select styles + **************************************************************************/ + + /** + * Small sized select with small font + */ + public static final String SELECT_SMALL = "small"; + + /** + * Large sized select with big font + */ + public static final String SELECT_BIG = "big"; + + /** + * Adds a magnifier icon on the left side of the fields text + */ + public static final String COMBOBOX_SEARCH = "search"; + + /** + * Adds a magnifier icon on the left side of the fields text + */ + public static final String COMBOBOX_SELECT_BUTTON = "select-button"; + + /*************************************************************************** + * DateField styles + **************************************************************************/ + + /** + * Small sized date field with small font + */ + public static final String DATEFIELD_SMALL = "small"; + + /** + * Large sized date field with big font + */ + public static final String DATEFIELD_BIG = "big"; + + /*************************************************************************** + * Panel styles + **************************************************************************/ + + /** + * Removes borders and background color from the panel + */ + public static final String PANEL_BORDERLESS = "borderless"; + + /** + * Adds a more vibrant header for the panel, using the alternate color of + * the theme, and adds slight rounded corners (not supported in all + * browsers) + */ + public static final String PANEL_BUBBLE = "bubble"; + + /** + * Removes borders and background color from the panel + */ + public static final String PANEL_LIGHT = "light"; + + /*************************************************************************** + * SplitPanel styles + **************************************************************************/ + + /** + * Reduces the split handle to a minimal size (1 pixel) + */ + public static final String SPLITPANEL_SMALL = "small"; + + /*************************************************************************** + * TabSheet styles + **************************************************************************/ + + /** + * Removes borders and background color from the tab sheet + */ + public static final String TABSHEET_BORDERLESS = "borderless"; + + /*************************************************************************** + * Accordion styles + **************************************************************************/ + + /** + * Makes the accordion background opaque (non-transparent) + */ + public static final String ACCORDION_OPAQUE = "opaque"; + + /*************************************************************************** + * Table styles + **************************************************************************/ + + /** + * Removes borders and background color from the table + */ + public static final String TABLE_BORDERLESS = "borderless"; + + /** + * Makes the column header and content font size smaller inside the table + */ + public static final String TABLE_SMALL = "small"; + + /** + * Makes the column header and content font size bigger inside the table + */ + public static final String TABLE_BIG = "big"; + + /** + * Adds a light alternate background color to even rows in the table. + */ + public static final String TABLE_STRIPED = "striped"; + + /*************************************************************************** + * ProgressIndicator styles + **************************************************************************/ + + /** + * Reduces the height of the progress bar + */ + public static final String PROGRESS_INDICATOR_SMALL = "small"; + + /** + * Increases the height of the progress bar. If the indicator is in + * indeterminate mode, shows a bigger spinner than the regular indeterminate + * indicator. + */ + public static final String PROGRESS_INDICATOR_BIG = "big"; + + /** + * Displays an indeterminate progress indicator as a bar with animated + * background stripes. This style can be used in combination with the + * "small" and "big" styles. + */ + public static final String PROGRESS_INDICATOR_INDETERMINATE_BAR = "bar"; + + /*************************************************************************** + * Window styles + **************************************************************************/ + + /** + * Sub-window style that makes the window background opaque (i.e. not + * semi-transparent). + */ + public static final String WINDOW_OPAQUE = "opaque"; + + /*************************************************************************** + * Compound styles + **************************************************************************/ + + /** + * Creates a context for a segment button control. Place buttons inside the + * segment, and add "<code>first</code>" and "<code>last</code>" style names + * for the first and last button in the segment. Then use the + * {@link #BUTTON_DOWN} style to indicate button states. + * + * E.g. + * + * <pre> + * HorizontalLayout ("segment") + * + Button ("first down") + * + Button ("down") + * + Button + * ... + * + Button ("last") + * </pre> + * + * You can also use most of the different button styles for the contained + * buttons (e.g. {@link #BUTTON_BIG}, {@link #BUTTON_ICON_ONLY} etc.). + */ + public static final String COMPOUND_HORIZONTAL_LAYOUT_SEGMENT = "segment"; + + /** + * Use this mixin-style in combination with the + * {@link #COMPOUND_HORIZONTAL_LAYOUT_SEGMENT} style to make buttons with + * the "down" style use the themes alternate color (e.g. blue instead of + * gray). + * + * E.g. + * + * <pre> + * HorizontalLayout ("segment segment-alternate") + * + Button ("first down") + * + Button ("down") + * + Button + * ... + * + Button ("last") + * </pre> + */ + public static final String COMPOUND_HORIZONTAL_LAYOUT_SEGMENT_ALTERNATE = "segment-alternate"; + + /** + * Creates an iTunes-like menu from a CssLayout or a VerticalLayout. Place + * plain Labels and NativeButtons inside the layout, and you're all set. + * + * E.g. + * + * <pre> + * CssLayout ("sidebar-menu") + * + Label + * + NativeButton + * + NativeButton + * ... + * + Label + * + NativeButton + * </pre> + */ + public static final String COMPOUND_LAYOUT_SIDEBAR_MENU = "sidebar-menu"; + + /** + * Adds a toolbar-like background for the layout, and aligns Buttons and + * Segments horizontally. Feel free to use different buttons styles inside + * the toolbar, like {@link #BUTTON_ICON_ON_TOP} and + * {@link #BUTTON_BORDERLESS} + */ + public static final String COMPOUND_CSSLAYOUT_TOOLBAR = "toolbar"; +} diff --git a/server/src/com/vaadin/ui/themes/LiferayTheme.java b/server/src/com/vaadin/ui/themes/LiferayTheme.java new file mode 100644 index 0000000000..9b48306ac2 --- /dev/null +++ b/server/src/com/vaadin/ui/themes/LiferayTheme.java @@ -0,0 +1,31 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.ui.themes; + +public class LiferayTheme extends BaseTheme { + + public static final String THEME_NAME = "liferay"; + + /*************************************************************************** + * + * Panel styles + * + **************************************************************************/ + + /** + * Removes borders and background from the panel + */ + public static final String PANEL_LIGHT = "light"; + + /*************************************************************************** + * + * SplitPanel styles + * + **************************************************************************/ + + /** + * Reduces the split handle to a minimal size (1 pixel) + */ + public static final String SPLITPANEL_SMALL = "small"; +} diff --git a/server/src/com/vaadin/ui/themes/Reindeer.java b/server/src/com/vaadin/ui/themes/Reindeer.java new file mode 100644 index 0000000000..7aaae8faa2 --- /dev/null +++ b/server/src/com/vaadin/ui/themes/Reindeer.java @@ -0,0 +1,217 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.ui.themes; + +import com.vaadin.ui.CssLayout; +import com.vaadin.ui.FormLayout; +import com.vaadin.ui.GridLayout; +import com.vaadin.ui.HorizontalLayout; +import com.vaadin.ui.HorizontalSplitPanel; +import com.vaadin.ui.VerticalLayout; +import com.vaadin.ui.VerticalSplitPanel; + +public class Reindeer extends BaseTheme { + + public static final String THEME_NAME = "reindeer"; + + /*************************************************************************** + * + * Label styles + * + **************************************************************************/ + + /** + * Large font for main application headings + */ + public static final String LABEL_H1 = "h1"; + + /** + * Large font for different sections in the application + */ + public static final String LABEL_H2 = "h2"; + + /** + * Small and a little lighter font + */ + public static final String LABEL_SMALL = "light"; + + /** + * @deprecated Use {@link #LABEL_SMALL} instead. + */ + @Deprecated + public static final String LABEL_LIGHT = "small"; + + /*************************************************************************** + * + * Button styles + * + **************************************************************************/ + + /** + * Default action style for buttons (the button that should get activated + * when the user presses 'enter' in a form). Use sparingly, only one default + * button per view should be visible. + */ + public static final String BUTTON_DEFAULT = "primary"; + + /** + * @deprecated Use {@link #BUTTON_DEFAULT} instead + */ + @Deprecated + public static final String BUTTON_PRIMARY = BUTTON_DEFAULT; + + /** + * Small sized button, use for context specific actions for example + */ + public static final String BUTTON_SMALL = "small"; + + /*************************************************************************** + * + * TextField styles + * + **************************************************************************/ + + /** + * Small sized text field with small font + */ + public static final String TEXTFIELD_SMALL = "small"; + + /*************************************************************************** + * + * Panel styles + * + **************************************************************************/ + + /** + * Removes borders and background color from the panel + */ + public static final String PANEL_LIGHT = "light"; + + /*************************************************************************** + * + * SplitPanel styles + * + **************************************************************************/ + + /** + * Reduces the split handle to a minimal size (1 pixel) + */ + public static final String SPLITPANEL_SMALL = "small"; + + /*************************************************************************** + * + * TabSheet styles + * + **************************************************************************/ + + /** + * Removes borders from the default tab sheet style. + */ + public static final String TABSHEET_BORDERLESS = "borderless"; + + /** + * Removes borders and background color from the tab sheet, and shows the + * tabs as a small bar. + */ + public static final String TABSHEET_SMALL = "bar"; + + /** + * @deprecated Use {@link #TABSHEET_SMALL} instead. + */ + @Deprecated + public static final String TABSHEET_BAR = TABSHEET_SMALL; + + /** + * Removes borders and background color from the tab sheet. The tabs are + * presented with minimal lines indicating the selected tab. + */ + public static final String TABSHEET_MINIMAL = "minimal"; + + /** + * Makes the tab close buttons visible only when the user is hovering over + * the tab. + */ + public static final String TABSHEET_HOVER_CLOSABLE = "hover-closable"; + + /** + * Makes the tab close buttons visible only when the tab is selected. + */ + public static final String TABSHEET_SELECTED_CLOSABLE = "selected-closable"; + + /*************************************************************************** + * + * Table styles + * + **************************************************************************/ + + /** + * Removes borders from the table + */ + public static final String TABLE_BORDERLESS = "borderless"; + + /** + * Makes the table headers dark and more prominent. + */ + public static final String TABLE_STRONG = "strong"; + + /*************************************************************************** + * + * Layout styles + * + **************************************************************************/ + + /** + * Changes the background of a layout to white. Applies to + * {@link VerticalLayout}, {@link HorizontalLayout}, {@link GridLayout}, + * {@link FormLayout}, {@link CssLayout}, {@link VerticalSplitPanel} and + * {@link HorizontalSplitPanel}. + * <p> + * <em>Does not revert any contained components back to normal if some + * parent layout has style {@link #LAYOUT_BLACK} applied.</em> + */ + public static final String LAYOUT_WHITE = "white"; + + /** + * Changes the background of a layout to a shade of blue. Applies to + * {@link VerticalLayout}, {@link HorizontalLayout}, {@link GridLayout}, + * {@link FormLayout}, {@link CssLayout}, {@link VerticalSplitPanel} and + * {@link HorizontalSplitPanel}. + * <p> + * <em>Does not revert any contained components back to normal if some + * parent layout has style {@link #LAYOUT_BLACK} applied.</em> + */ + public static final String LAYOUT_BLUE = "blue"; + + /** + * <p> + * Changes the background of a layout to almost black, and at the same time + * transforms contained components to their black style correspondents when + * available. At least texts, buttons, text fields, selects, date fields, + * tables and a few other component styles should change. + * </p> + * <p> + * Applies to {@link VerticalLayout}, {@link HorizontalLayout}, + * {@link GridLayout}, {@link FormLayout} and {@link CssLayout}. + * </p> + * + */ + public static final String LAYOUT_BLACK = "black"; + + /*************************************************************************** + * + * Window styles + * + **************************************************************************/ + + /** + * Makes the whole window white and increases the font size of the title. + */ + public static final String WINDOW_LIGHT = "light"; + + /** + * Makes the whole window black, and changes contained components in the + * same way as {@link #LAYOUT_BLACK} does. + */ + public static final String WINDOW_BLACK = "black"; +} diff --git a/server/src/com/vaadin/ui/themes/Runo.java b/server/src/com/vaadin/ui/themes/Runo.java new file mode 100644 index 0000000000..28a19e8dcd --- /dev/null +++ b/server/src/com/vaadin/ui/themes/Runo.java @@ -0,0 +1,183 @@ +/* +@VaadinApache2LicenseForJavaFiles@ + */ +package com.vaadin.ui.themes; + +public class Runo extends BaseTheme { + + public static final String THEME_NAME = "runo"; + + public static String themeName() { + return THEME_NAME.toLowerCase(); + } + + /*************************************************************************** + * + * Button styles + * + **************************************************************************/ + + /** + * Small sized button, use for context specific actions for example + */ + public static final String BUTTON_SMALL = "small"; + + /** + * Big sized button, use to gather much attention for some particular action + */ + public static final String BUTTON_BIG = "big"; + + /** + * Default action style for buttons (the button that should get activated + * when the user presses 'enter' in a form). Use sparingly, only one default + * button per view should be visible. + */ + public static final String BUTTON_DEFAULT = "default"; + + /*************************************************************************** + * + * Panel styles + * + **************************************************************************/ + + /** + * Removes borders and background color from the panel + */ + public static final String PANEL_LIGHT = "light"; + + /*************************************************************************** + * + * TabSheet styles + * + **************************************************************************/ + + /** + * Smaller tabs, no border and background for content area + */ + public static final String TABSHEET_SMALL = "light"; + + /*************************************************************************** + * + * SplitPanel styles + * + **************************************************************************/ + + /** + * Reduces the width/height of the split handle. Useful when you don't want + * the split handle to touch the sides of the containing layout. + */ + public static final String SPLITPANEL_REDUCED = "rounded"; + + /** + * Reduces the visual size of the split handle to one pixel (the active drag + * size is still larger). + */ + public static final String SPLITPANEL_SMALL = "small"; + + /*************************************************************************** + * + * Label styles + * + **************************************************************************/ + + /** + * Largest title/header size. Use for main sections in your application. + */ + public static final String LABEL_H1 = "h1"; + + /** + * Similar style as in panel captions. Useful for sub-sections within a + * view. + */ + public static final String LABEL_H2 = "h2"; + + /** + * Small font size. Useful for contextual help texts and similar less + * frequently needed information. Use with modesty, since this style will be + * more harder to read due to its smaller size and contrast. + */ + public static final String LABEL_SMALL = "small"; + + /*************************************************************************** + * + * Layout styles + * + **************************************************************************/ + + /** + * An alternative background color for layouts. Use on top of white + * background (e.g. inside Panels, TabSheets and sub-windows). + */ + public static final String LAYOUT_DARKER = "darker"; + + /** + * Add a drop shadow around the layout and its contained components. + * Produces a rectangular shadow, even if the contained component would have + * a different shape. + * <p> + * Note: does not work in Internet Explorer 6 + */ + public static final String CSSLAYOUT_SHADOW = "box-shadow"; + + /** + * Adds necessary styles to the layout to make it look selectable (i.e. + * clickable). Add a click listener for the layout, and toggle the + * {@link #CSSLAYOUT_SELECTABLE_SELECTED} style for the same layout to make + * it look selected or not. + */ + public static final String CSSLAYOUT_SELECTABLE = "selectable"; + public static final String CSSLAYOUT_SELECTABLE_SELECTED = "selectable-selected"; + + /*************************************************************************** + * + * TextField styles + * + **************************************************************************/ + + /** + * Small sized text field with small font + */ + public static final String TEXTFIELD_SMALL = "small"; + + /*************************************************************************** + * + * Table styles + * + **************************************************************************/ + + /** + * Smaller header and item fonts. + */ + public static final String TABLE_SMALL = "small"; + + /** + * Removes the border and background color from the table. Removes + * alternating row background colors as well. + */ + public static final String TABLE_BORDERLESS = "borderless"; + + /*************************************************************************** + * + * Accordion styles + * + **************************************************************************/ + + /** + * A detached looking accordion, providing space around its captions and + * content. Doesn't necessarily need a Panel or other container to wrap it + * in order to make it look right. + */ + public static final String ACCORDION_LIGHT = "light"; + + /*************************************************************************** + * + * Window styles + * + **************************************************************************/ + + /** + * Smaller header and a darker background color for the window. Useful for + * smaller dialog-like windows. + */ + public static final String WINDOW_DIALOG = "dialog"; +} |