diff options
author | Henri Sara <henri.sara@itmill.com> | 2009-05-11 09:19:03 +0000 |
---|---|---|
committer | Henri Sara <henri.sara@itmill.com> | 2009-05-11 09:19:03 +0000 |
commit | adc8c0ad3573272c236040c3a76005b9e73a5737 (patch) | |
tree | a3860704dbd5b82dc6af38684b80f8ef79a32722 /src/com/vaadin/ui | |
parent | 5abc870dda584d0c2fc47fd5eec4ae3de3fa240e (diff) | |
download | vaadin-framework-adc8c0ad3573272c236040c3a76005b9e73a5737.tar.gz vaadin-framework-adc8c0ad3573272c236040c3a76005b9e73a5737.zip |
#2904: initial bulk rename "com.itmill.toolkit" -> "com.vaadin"
- com.itmill.toolkit.external not yet fully renamed
svn changeset:7715/svn branch:6.0
Diffstat (limited to 'src/com/vaadin/ui')
57 files changed, 24765 insertions, 0 deletions
diff --git a/src/com/vaadin/ui/AbsoluteLayout.java b/src/com/vaadin/ui/AbsoluteLayout.java new file mode 100644 index 0000000000..cf3f62eee4 --- /dev/null +++ b/src/com/vaadin/ui/AbsoluteLayout.java @@ -0,0 +1,357 @@ +package com.vaadin.ui; + +import java.io.Serializable; +import java.util.Collection; +import java.util.HashMap; +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.gwt.client.ui.IAbsoluteLayout; + +/** + * AbsoluteLayout is a layout implementation that mimics html absolute + * positioning. + * + */ +@SuppressWarnings("serial") +public class AbsoluteLayout extends AbstractLayout { + + private Collection<Component> components = new LinkedHashSet<Component>(); + private Map<Component, ComponentPosition> componentToCoordinates = new HashMap<Component, ComponentPosition>(); + + public AbsoluteLayout() { + setSizeFull(); + } + + @Override + public String getTag() { + return IAbsoluteLayout.TAGNAME; + } + + public Iterator<Component> getComponentIterator() { + return components.iterator(); + } + + public void replaceComponent(Component oldComponent, Component newComponent) { + ComponentPosition position = getPosition(oldComponent); + removeComponent(oldComponent); + addComponent(newComponent); + componentToCoordinates.put(newComponent, position); + } + + @Override + public void addComponent(Component c) { + components.add(c); + super.addComponent(c); + } + + @Override + public void removeComponent(Component c) { + components.remove(c); + super.removeComponent(c); + requestRepaint(); + } + + public void addComponent(Component c, String cssPosition) { + addComponent(c); + getPosition(c).setCSSString(cssPosition); + } + + public ComponentPosition getPosition(Component component) { + if (componentToCoordinates.containsKey(component)) { + return componentToCoordinates.get(component); + } else { + ComponentPosition coords = new ComponentPosition(); + componentToCoordinates.put(component, coords); + return coords; + } + } + + /** + * TODO symmetric getters and setters for fields to make this simpler to use + * in generic java tools + * + */ + public class ComponentPosition implements Serializable { + + private int zIndex = -1; + private float topValue = -1; + private float rightValue = -1; + private float bottomValue = -1; + private float leftValue = -1; + + private int topUnits; + private int rightUnits; + private int bottomUnits; + private int leftUnits; + + /** + * Sets the position attributes using CSS syntax. Example usage: + * + * <code><pre> + * setCSSString("top:10px;left:20%;z-index:16;"); + * </pre></code> + * + * @param css + */ + public void setCSSString(String css) { + 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]); + } else { + String value; + if (keyValuePair.length > 1) { + value = keyValuePair[1].trim(); + } else { + value = ""; + } + String unit = value.replaceAll("[0-9\\.]+", ""); + if (!unit.equals("")) { + value = value.substring(0, value.indexOf(unit)).trim(); + } + float v = Float.parseFloat(value); + int unitInt = parseCssUnit(unit); + if (key.equals("top")) { + topValue = v; + topUnits = unitInt; + } else if (key.equals("right")) { + rightValue = v; + rightUnits = unitInt; + } else if (key.equals("bottom")) { + bottomValue = v; + bottomUnits = unitInt; + } else if (key.equals("left")) { + leftValue = v; + leftUnits = unitInt; + } + } + } + requestRepaint(); + } + + private int parseCssUnit(String string) { + for (int i = 0; i < UNIT_SYMBOLS.length; i++) { + if (UNIT_SYMBOLS[i].equals(string)) { + return i; + } + } + return 0; // defaults to px (eg. top:0;) + } + + public String getCSSString() { + String s = ""; + if (topValue >= 0) { + s += "top:" + topValue + UNIT_SYMBOLS[topUnits] + ";"; + } + if (rightValue >= 0) { + s += "right:" + rightValue + UNIT_SYMBOLS[rightUnits] + ";"; + } + if (bottomValue >= 0) { + s += "bottom:" + bottomValue + UNIT_SYMBOLS[bottomUnits] + ";"; + } + if (leftValue >= 0) { + s += "left:" + leftValue + UNIT_SYMBOLS[leftUnits] + ";"; + } + if (zIndex >= 0) { + s += "z-index:" + zIndex + ";"; + } + return s; + } + + public void setTop(float topValue, int topUnits) { + validateLength(topValue, topUnits); + this.topValue = topValue; + this.topUnits = topUnits; + requestRepaint(); + } + + public void setRight(float rightValue, int rightUnits) { + validateLength(rightValue, rightUnits); + this.rightValue = rightValue; + this.rightUnits = rightUnits; + requestRepaint(); + } + + public void setBottom(float bottomValue, int units) { + validateLength(bottomValue, units); + this.bottomValue = bottomValue; + bottomUnits = units; + requestRepaint(); + } + + public void setLeft(float leftValue, int units) { + validateLength(leftValue, units); + this.leftValue = leftValue; + leftUnits = units; + requestRepaint(); + } + + public void setZIndex(int zIndex) { + this.zIndex = zIndex; + requestRepaint(); + } + + public void setTopValue(float topValue) { + validateLength(topValue, topUnits); + this.topValue = topValue; + requestRepaint(); + } + + public float getTopValue() { + return topValue; + } + + /** + * @return the rightValue + */ + public float getRightValue() { + return rightValue; + } + + /** + * @param rightValue + * the rightValue to set + */ + public void setRightValue(float rightValue) { + validateLength(rightValue, rightUnits); + this.rightValue = rightValue; + requestRepaint(); + } + + /** + * @return the bottomValue + */ + public float getBottomValue() { + return bottomValue; + } + + /** + * @param bottomValue + * the bottomValue to set + */ + public void setBottomValue(float bottomValue) { + validateLength(bottomValue, bottomUnits); + this.bottomValue = bottomValue; + requestRepaint(); + } + + /** + * @return the leftValue + */ + public float getLeftValue() { + return leftValue; + } + + /** + * @param leftValue + * the leftValue to set + */ + public void setLeftValue(float leftValue) { + validateLength(leftValue, leftUnits); + this.leftValue = leftValue; + requestRepaint(); + } + + /** + * @return the topUnits + */ + public int getTopUnits() { + return topUnits; + } + + /** + * @param topUnits + * the topUnits to set + */ + public void setTopUnits(int topUnits) { + validateLength(topValue, topUnits); + this.topUnits = topUnits; + requestRepaint(); + } + + /** + * @return the rightUnits + */ + public int getRightUnits() { + return rightUnits; + } + + /** + * @param rightUnits + * the rightUnits to set + */ + public void setRightUnits(int rightUnits) { + validateLength(rightValue, rightUnits); + this.rightUnits = rightUnits; + requestRepaint(); + } + + /** + * @return the bottomUnits + */ + public int getBottomUnits() { + return bottomUnits; + } + + /** + * @param bottomUnits + * the bottomUnits to set + */ + public void setBottomUnits(int bottomUnits) { + validateLength(bottomValue, bottomUnits); + this.bottomUnits = bottomUnits; + requestRepaint(); + } + + /** + * @return the leftUnits + */ + public int getLeftUnits() { + return leftUnits; + } + + /** + * @param leftUnits + * the leftUnits to set + */ + public void setLeftUnits(int leftUnits) { + validateLength(leftValue, leftUnits); + this.leftUnits = leftUnits; + requestRepaint(); + } + + /** + * @return the zIndex + */ + public int getZIndex() { + return zIndex; + } + + } + + @Override + public void paintContent(PaintTarget target) throws PaintException { + super.paintContent(target); + for (Component component : components) { + target.startTag("cc"); + target.addAttribute("css", getPosition(component).getCSSString()); + component.paint(target); + target.endTag("cc"); + } + } + + private static void validateLength(float topValue, int topUnits2) { + // TODO throw on invalid value + + } + +} diff --git a/src/com/vaadin/ui/AbstractComponent.java b/src/com/vaadin/ui/AbstractComponent.java new file mode 100644 index 0000000000..b26278b016 --- /dev/null +++ b/src/com/vaadin/ui/AbstractComponent.java @@ -0,0 +1,1296 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.io.Serializable; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.Locale; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import com.vaadin.Application; +import com.vaadin.event.EventRouter; +import com.vaadin.event.MethodEventSource; +import com.vaadin.terminal.ErrorMessage; +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.PaintTarget; +import com.vaadin.terminal.Resource; +import com.vaadin.terminal.Terminal; +import com.vaadin.terminal.gwt.server.ComponentSizeValidator; + +/** + * 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 a IT Mill + * Toolkit component. Most components in the toolkit do just that. + * + * @author IT Mill Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +@SuppressWarnings("serial") +public abstract class AbstractComponent implements Component, MethodEventSource { + + /* Private members */ + + /** + * Style names. + */ + private ArrayList styles; + + /** + * Caption text. + */ + private String caption; + + /** + * Application specific data object. The component does not use or modify + * this. + */ + private Object applicationData; + + /** + * Icon to be shown together with caption. + */ + private Resource icon; + + /** + * Is the component enabled (its normal usage is allowed). + */ + private boolean enabled = true; + + /** + * Is the component visible (it is rendered). + */ + private boolean visible = true; + + /** + * Is the component read-only ? + */ + private boolean readOnly = false; + + /** + * Description of the usage (XML). + */ + private String description = null; + + /** + * The container this component resides in. + */ + private Component parent = null; + + /** + * The EventRouter used for the event model. + */ + private EventRouter eventRouter = null; + + /** + * The internal error message of the component. + */ + private ErrorMessage componentError = null; + + /** + * Immediate mode: if true, all variable changes are required to be sent + * from the terminal immediately. + */ + private boolean immediate = false; + + /** + * Locale of this component. + */ + private Locale locale; + + /** + * List of repaint request listeners or null if not listened at all. + */ + private LinkedList repaintRequestListeners = null; + + /** + * Are all the repaint listeners notified about recent changes ? + */ + private boolean repaintRequestListenersNotified = false; + + private String testingId; + + /* Sizeable fields */ + + private float width = SIZE_UNDEFINED; + private float height = SIZE_UNDEFINED; + private int widthUnit = UNITS_PIXELS; + private int heightUnit = UNITS_PIXELS; + private static final Pattern sizePattern = Pattern + .compile("^(-?\\d+(\\.\\d+)?)(%|px|em|ex|in|cm|mm|pt|pc)?$"); + + private ComponentErrorHandler errorHandler = null; + + /* Constructor */ + + /** + * Constructs a new Component. + */ + public AbstractComponent() { + // ComponentSizeValidator.setCreationLocation(this); + } + + /* Get/Set component properties */ + + /** + * Gets the UIDL tag corresponding to the component. + * + * @return the component's UIDL tag as <code>String</code> + */ + public abstract String getTag(); + + public void setDebugId(String id) { + testingId = id; + } + + public String getDebugId() { + return testingId; + } + + /** + * 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 com.vaadin.terminal.Paintable.RepaintRequestEvent + * 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. + */ + public String getStyleName() { + String s = ""; + if (styles != null) { + for (final Iterator it = styles.iterator(); it.hasNext();) { + s += (String) 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. + */ + public void setStyleName(String style) { + if (style == null || "".equals(style)) { + styles = null; + requestRepaint(); + return; + } + if (styles == null) { + styles = new ArrayList(); + } + styles.clear(); + styles.add(style); + requestRepaint(); + } + + public void addStyleName(String style) { + if (style == null || "".equals(style)) { + return; + } + if (styles == null) { + styles = new ArrayList(); + } + if (!styles.contains(style)) { + styles.add(style); + requestRepaint(); + } + } + + public void removeStyleName(String style) { + if (styles != null) { + styles.remove(style); + requestRepaint(); + } + } + + /* + * Get's the component's caption. Don't add a JavaDoc comment here, we use + * the default documentation from implemented interface. + */ + public String getCaption() { + return caption; + } + + /** + * Sets the component's caption <code>String</code>. Caption is the visible + * name of the component. This method will trigger a + * {@link com.vaadin.terminal.Paintable.RepaintRequestEvent + * RepaintRequestEvent}. + * + * @param caption + * the new caption <code>String</code> for the component. + */ + public void setCaption(String caption) { + this.caption = caption; + requestRepaint(); + } + + /* + * Don't add a JavaDoc comment here, we use the default documentation from + * implemented interface. + */ + public Locale getLocale() { + if (locale != null) { + return locale; + } + if (parent != null) { + return parent.getLocale(); + } + final Application app = getApplication(); + if (app != null) { + return app.getLocale(); + } + return null; + } + + /** + * Sets the locale of this component. + * + * @param locale + * the locale to become this component's locale. + */ + public void setLocale(Locale locale) { + this.locale = locale; + } + + /* + * Gets the component's icon resource. Don't add a JavaDoc comment here, we + * use the default documentation from implemented interface. + */ + public Resource getIcon() { + return icon; + } + + /** + * Sets the component's icon. This method will trigger a + * {@link com.vaadin.terminal.Paintable.RepaintRequestEvent + * RepaintRequestEvent}. + * + * @param icon + * the icon to be shown with the component's caption. + */ + public void setIcon(Resource icon) { + this.icon = icon; + requestRepaint(); + } + + /* + * Tests if the component is enabled or not. Don't add a JavaDoc comment + * here, we use the default documentation from implemented interface. + */ + public boolean isEnabled() { + return enabled && (parent == null || parent.isEnabled()) && isVisible(); + } + + /* + * Enables or disables the component. Don't add a JavaDoc comment here, we + * use the default documentation from implemented interface. + */ + public void setEnabled(boolean enabled) { + if (this.enabled != enabled) { + boolean wasEnabled = isEnabled(); + this.enabled = enabled; + // don't repaint if ancestor is disabled + if (wasEnabled != isEnabled()) { + requestRepaint(); + } + } + } + + /* + * 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 immediate; + } + + /** + * Sets the component's immediate mode to the specified status. This method + * will trigger a + * {@link com.vaadin.terminal.Paintable.RepaintRequestEvent + * 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) { + this.immediate = immediate; + requestRepaint(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.Component#isVisible() + */ + public boolean isVisible() { + return visible && (getParent() == null || getParent().isVisible()); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.Component#setVisible(boolean) + */ + public void setVisible(boolean visible) { + + if (this.visible != visible) { + this.visible = visible; + // Instead of requesting repaint normally we + // fire the event directly to assure that the + // event goes through event in the component might + // now be invisible + fireRequestRepaintEvent(null); + } + } + + /** + * <p> + * Gets the component's description. 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 description; + } + + /** + * Sets the component's description. See {@link #getDescription()} for more + * information on what the description is. This method will trigger a + * {@link com.vaadin.terminal.Paintable.RepaintRequestEvent + * RepaintRequestEvent}. + * + * @param description + * the new description string for the component. + */ + public void setDescription(String description) { + this.description = description; + requestRepaint(); + } + + /* + * Gets the component's parent component. Don't add a JavaDoc comment here, + * we use the default documentation from implemented interface. + */ + public Component getParent() { + return parent; + } + + /* + * Sets the parent component. Don't add a JavaDoc comment here, we use the + * default documentation from implemented interface. + */ + public void setParent(Component parent) { + + // If the parent is not changed, don't do anything + if (parent == this.parent) { + return; + } + + if (parent != null && this.parent != null) { + throw new IllegalStateException("Component already has a parent."); + } + + // Send detach event if the component have been connected to a window + if (getApplication() != null) { + detach(); + } + + // Connect to new parent + this.parent = parent; + + // Send attach event if connected to a window + if (getApplication() != null) { + attach(); + } + } + + /** + * 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. + */ + public boolean isReadOnly() { + return readOnly; + } + + /* + * Sets the component's read-only mode. Don't add a JavaDoc comment here, we + * use the default documentation from implemented interface. + */ + public void setReadOnly(boolean readOnly) { + this.readOnly = readOnly; + requestRepaint(); + } + + /* + * Gets the parent window of the component. Don't add a JavaDoc comment + * here, we use the default documentation from implemented interface. + */ + public Window getWindow() { + if (parent == null) { + return null; + } else { + return parent.getWindow(); + } + } + + /* + * 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. + */ + public void attach() { + requestRepaint(); + } + + /* + * Detach the component from application. Don't add a JavaDoc comment here, + * we use the default documentation from implemented interface. + */ + public void detach() { + } + + /* + * Gets the parent application of the component. Don't add a JavaDoc comment + * here, we use the default documentation from implemented interface. + */ + public Application getApplication() { + if (parent == null) { + return null; + } else { + return parent.getApplication(); + } + } + + /* Component painting */ + + /* Documented in super interface */ + public void requestRepaintRequests() { + repaintRequestListenersNotified = false; + } + + /* + * Paints the component into a UIDL stream. Don't add a JavaDoc comment + * here, we use the default documentation from implemented interface. + */ + public final void paint(PaintTarget target) throws PaintException { + if (!target.startTag(this, getTag()) || repaintRequestListenersNotified) { + + // Paint the contents of the component + + // Only paint content of visible components. + if (isVisible()) { + if (getHeight() >= 0 + && (getHeightUnits() != UNITS_PERCENTAGE || ComponentSizeValidator + .parentCanDefineHeight(this))) { + target.addAttribute("height", "" + getCSSHeight()); + } + + if (getWidth() >= 0 + && (getWidthUnits() != UNITS_PERCENTAGE || ComponentSizeValidator + .parentCanDefineWidth(this))) { + target.addAttribute("width", "" + getCSSWidth()); + } + if (styles != null && styles.size() > 0) { + target.addAttribute("style", getStyle()); + } + if (isReadOnly()) { + target.addAttribute("readonly", true); + } + + if (isImmediate()) { + target.addAttribute("immediate", true); + } + if (!isEnabled()) { + target.addAttribute("disabled", true); + } + if (getCaption() != null) { + target.addAttribute("caption", getCaption()); + } + if (getIcon() != null) { + target.addAttribute("icon", getIcon()); + } + + if (getDescription() != null && getDescription().length() > 0) { + target.addAttribute("description", getDescription()); + } + + paintContent(target); + + final ErrorMessage error = getErrorMessage(); + if (error != null) { + error.paint(target); + } + } else { + target.addAttribute("invisible", true); + } + } else { + + // Contents have not changed, only cached presentation can be used + target.addAttribute("cached", true); + } + target.endTag(getTag()); + + repaintRequestListenersNotified = false; + } + + /** + * Build CSS compatible string representation of height. + * + * @return CSS height + */ + private String getCSSHeight() { + if (getHeightUnits() == UNITS_PIXELS) { + return ((int) getHeight()) + UNIT_SYMBOLS[getHeightUnits()]; + } else { + return getHeight() + UNIT_SYMBOLS[getHeightUnits()]; + } + } + + /** + * Build CSS compatible string representation of width. + * + * @return CSS width + */ + private String getCSSWidth() { + if (getWidthUnits() == UNITS_PIXELS) { + return ((int) getWidth()) + UNIT_SYMBOLS[getWidthUnits()]; + } else { + return getWidth() + UNIT_SYMBOLS[getWidthUnits()]; + } + } + + /** + * Paints any needed component-specific things to the given UIDL stream. The + * more general {@link #paint(PaintTarget)} method handles all general + * attributes common to all components, and it calls this method to paint + * any component-specific attributes to the UIDL stream. + * + * @param target + * the target UIDL stream where the component should paint itself + * to + * @throws PaintException + * if the paint operation failed. + */ + public void paintContent(PaintTarget target) throws PaintException { + + } + + /* Documentation copied from interface */ + public void requestRepaint() { + + // The effect of the repaint request is identical to case where a + // child requests repaint + childRequestedRepaint(null); + } + + /* Documentation copied from interface */ + public void childRequestedRepaint(Collection alreadyNotified) { + // Invisible components (by flag in this particular component) do not + // need repaints + if (!visible) { + return; + } + + fireRequestRepaintEvent(alreadyNotified); + } + + /** + * Fires the repaint request event. + * + * @param alreadyNotified + */ + private void fireRequestRepaintEvent(Collection alreadyNotified) { + // Notify listeners only once + if (!repaintRequestListenersNotified) { + // Notify the listeners + if (repaintRequestListeners != null + && !repaintRequestListeners.isEmpty()) { + final Object[] listeners = repaintRequestListeners.toArray(); + final RepaintRequestEvent event = new RepaintRequestEvent(this); + for (int i = 0; i < listeners.length; i++) { + if (alreadyNotified == null) { + alreadyNotified = new LinkedList(); + } + if (!alreadyNotified.contains(listeners[i])) { + ((RepaintRequestListener) listeners[i]) + .repaintRequested(event); + alreadyNotified.add(listeners[i]); + repaintRequestListenersNotified = true; + } + } + } + + // Notify the parent + final Component parent = getParent(); + if (parent != null) { + parent.childRequestedRepaint(alreadyNotified); + } + } + } + + /* Documentation copied from interface */ + public void addListener(RepaintRequestListener listener) { + if (repaintRequestListeners == null) { + repaintRequestListeners = new LinkedList(); + } + if (!repaintRequestListeners.contains(listener)) { + repaintRequestListeners.add(listener); + } + } + + /* Documentation copied from interface */ + public void removeListener(RepaintRequestListener listener) { + if (repaintRequestListeners != null) { + repaintRequestListeners.remove(listener); + if (repaintRequestListeners.isEmpty()) { + repaintRequestListeners = null; + } + } + } + + /* Component variable changes */ + + /* + * Invoked when the value of a variable has changed. Don't add a JavaDoc + * comment here, we use the default documentation from implemented + * interface. + */ + public void changeVariables(Object source, Map variables) { + } + + /* General event framework */ + + private static final Method COMPONENT_EVENT_METHOD; + + static { + try { + COMPONENT_EVENT_METHOD = Component.Listener.class + .getDeclaredMethod("componentEvent", + new Class[] { Component.Event.class }); + } catch (final java.lang.NoSuchMethodException e) { + // This should never happen + throw new java.lang.RuntimeException( + "Internal error finding methods in AbstractComponent"); + } + } + + /** + * <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 object + * the object instance who owns the activation method. + * @param method + * the activation method. + */ + public void addListener(Class eventType, Object object, Method method) { + if (eventRouter == null) { + eventRouter = new EventRouter(); + } + eventRouter.addListener(eventType, object, 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 object + * the object instance who owns the activation method. + * @param methodName + * the name of the activation method. + */ + public void addListener(Class eventType, Object object, String methodName) { + if (eventRouter == null) { + eventRouter = new EventRouter(); + } + eventRouter.addListener(eventType, object, 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. + */ + 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>. + */ + 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>. + */ + public void removeListener(Class eventType, Object target, String methodName) { + if (eventRouter != null) { + eventRouter.removeListener(eventType, target, methodName); + } + } + + /** + * 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. + */ + public void addListener(Component.Listener listener) { + if (eventRouter == null) { + eventRouter = new EventRouter(); + } + + eventRouter.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. + */ + public void removeListener(Component.Listener listener) { + if (eventRouter != null) { + eventRouter.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() + */ + public float getHeight() { + return height; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.terminal.Sizeable#getHeightUnits() + */ + public int getHeightUnits() { + return heightUnit; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.terminal.Sizeable#getWidth() + */ + public float getWidth() { + return width; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.terminal.Sizeable#getWidthUnits() + */ + public int getWidthUnits() { + return widthUnit; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.terminal.Sizeable#setHeight(float) + */ + @Deprecated + public void setHeight(float height) { + setHeight(height, getHeightUnits()); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.terminal.Sizeable#setHeightUnits(int) + */ + @Deprecated + public void setHeightUnits(int unit) { + setHeight(getHeight(), unit); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.terminal.Sizeable#setHeight(float, int) + */ + public void setHeight(float height, int unit) { + this.height = height; + heightUnit = unit; + requestRepaint(); + // ComponentSizeValidator.setHeightLocation(this); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.terminal.Sizeable#setSizeFull() + */ + public void setSizeFull() { + setWidth(100, UNITS_PERCENTAGE); + setHeight(100, UNITS_PERCENTAGE); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.terminal.Sizeable#setSizeUndefined() + */ + public void setSizeUndefined() { + setWidth(-1, UNITS_PIXELS); + setHeight(-1, UNITS_PIXELS); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.terminal.Sizeable#setWidth(float) + */ + @Deprecated + public void setWidth(float width) { + setWidth(width, getWidthUnits()); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.terminal.Sizeable#setWidthUnits(int) + */ + @Deprecated + public void setWidthUnits(int unit) { + setWidth(getWidth(), unit); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.terminal.Sizeable#setWidth(float, int) + */ + public void setWidth(float width, int unit) { + this.width = width; + widthUnit = unit; + requestRepaint(); + // ComponentSizeValidator.setWidthLocation(this); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.terminal.Sizeable#setWidth(java.lang.String) + */ + public void setWidth(String width) { + float[] p = parseStringSize(width); + setWidth(p[0], (int) p[1]); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.terminal.Sizeable#setHeight(java.lang.String) + */ + public void setHeight(String height) { + float[] p = parseStringSize(height); + setHeight(p[0], (int) p[1]); + } + + /* + * Returns array with size in index 0 unit in index 1. Null or empty string + * will produce {-1,UNITS_PIXELS} + */ + private static float[] parseStringSize(String s) { + float[] values = { -1, UNITS_PIXELS }; + if (s == null) { + return values; + } + s = s.trim(); + if ("".equals(s)) { + return values; + } + + Matcher matcher = sizePattern.matcher(s); + if (matcher.find()) { + values[0] = Float.parseFloat(matcher.group(1)); + if (values[0] < 0) { + values[0] = -1; + } else { + String unit = matcher.group(3); + if (unit == null) { + values[1] = UNITS_PIXELS; + } else if (unit.equals("px")) { + values[1] = UNITS_PIXELS; + } else if (unit.equals("%")) { + values[1] = UNITS_PERCENTAGE; + } else if (unit.equals("em")) { + values[1] = UNITS_EM; + } else if (unit.equals("ex")) { + values[1] = UNITS_EX; + } else if (unit.equals("in")) { + values[1] = UNITS_INCH; + } else if (unit.equals("cm")) { + values[1] = UNITS_CM; + } else if (unit.equals("mm")) { + values[1] = UNITS_MM; + } else if (unit.equals("pt")) { + values[1] = UNITS_POINTS; + } else if (unit.equals("pc")) { + values[1] = UNITS_PICAS; + } + } + } else { + throw new IllegalArgumentException("Invalid size argument: \"" + s + + "\" (should match " + sizePattern.pattern() + ")"); + } + return values; + } + + 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; + + } + +}
\ No newline at end of file diff --git a/src/com/vaadin/ui/AbstractComponentContainer.java b/src/com/vaadin/ui/AbstractComponentContainer.java new file mode 100644 index 0000000000..517dff14ac --- /dev/null +++ b/src/com/vaadin/ui/AbstractComponentContainer.java @@ -0,0 +1,274 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.lang.reflect.Method; +import java.util.Iterator; +import java.util.LinkedList; + +/** + * 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 IT Mill 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. + */ + public void removeAllComponents() { + final LinkedList l = new LinkedList(); + + // Adds all components + for (final Iterator i = getComponentIterator(); i.hasNext();) { + l.add(i.next()); + } + + // Removes all component + for (final Iterator i = l.iterator(); i.hasNext();) { + removeComponent((Component) 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. + */ + public void moveComponentsFrom(ComponentContainer source) { + final LinkedList components = new LinkedList(); + for (final Iterator i = source.getComponentIterator(); i.hasNext();) { + components.add(i.next()); + } + + for (final Iterator i = components.iterator(); i.hasNext();) { + final Component c = (Component) i.next(); + source.removeComponent(c); + addComponent(c); + } + } + + /** + * Notifies all contained components that the container is attached to a + * window. + * + * @see com.vaadin.ui.Component#attach() + */ + @Override + public void attach() { + super.attach(); + + for (final Iterator i = getComponentIterator(); i.hasNext();) { + ((Component) i.next()).attach(); + } + } + + /** + * Notifies all contained components that the container is detached from a + * window. + * + * @see com.vaadin.ui.Component#detach() + */ + @Override + public void detach() { + super.detach(); + + for (final Iterator i = getComponentIterator(); i.hasNext();) { + ((Component) i.next()).detach(); + } + } + + /* 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 */ + public void addListener(ComponentAttachListener listener) { + addListener(ComponentContainer.ComponentAttachEvent.class, listener, + COMPONENT_ATTACHED_METHOD); + } + + /* documented in interface */ + public void addListener(ComponentDetachListener listener) { + addListener(ComponentContainer.ComponentDetachEvent.class, listener, + COMPONENT_DETACHED_METHOD); + } + + /* documented in interface */ + public void removeListener(ComponentAttachListener listener) { + removeListener(ComponentContainer.ComponentAttachEvent.class, listener, + COMPONENT_ATTACHED_METHOD); + } + + /* documented in interface */ + 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) + */ + 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) + */ + public void removeComponent(Component c) { + if (c.getParent() == this) { + c.setParent(null); + fireComponentDetachEvent(c); + } + } + + @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(); + } + } + + @Override + public void setWidth(float width, int unit) { + if (getWidth() < 0 && width >= 0) { + // width becoming defined -> relative width children currently + // painted undefined may become defined + // TODO could be optimized(subtree of only those components + // which have undefined height due this component), currently just + // repaints whole subtree + requestRepaintAll(); + } else if (getWidth() >= 0 && width < 0) { + requestRepaintAll(); + } + super.setWidth(width, unit); + } + + @Override + public void setHeight(float height, int unit) { + float currentHeight = getHeight(); + if (currentHeight < 0.0f && height >= 0.0f) { + // height becoming defined -> relative height childs currently + // painted undefined may become defined + // TODO this could be optimized (subtree of only those components + // which have undefined width due this component), currently just + // repaints whole + // subtree + requestRepaintAll(); + } else if (currentHeight >= 0 && height < 0) { + requestRepaintAll(); + } + super.setHeight(height, unit); + } + + public void requestRepaintAll() { + requestRepaint(); + for (Iterator childIterator = getComponentIterator(); childIterator + .hasNext();) { + Component c = (Component) childIterator.next(); + if (c instanceof Form) { + // Form has children in layout, but is not ComponentContainer + c.requestRepaint(); + ((Form) c).getLayout().requestRepaintAll(); + } else if (c instanceof Table) { + ((Table) c).requestRepaintAll(); + } else if (c instanceof ComponentContainer) { + ((ComponentContainer) c).requestRepaintAll(); + } else { + c.requestRepaint(); + } + } + } + +}
\ No newline at end of file diff --git a/src/com/vaadin/ui/AbstractField.java b/src/com/vaadin/ui/AbstractField.java new file mode 100644 index 0000000000..9d1420a78d --- /dev/null +++ b/src/com/vaadin/ui/AbstractField.java @@ -0,0 +1,1157 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.io.Serializable; +import java.lang.reflect.Method; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.Map; + +import com.vaadin.Application; +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.terminal.CompositeErrorMessage; +import com.vaadin.terminal.ErrorMessage; +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.PaintTarget; + +/** + * <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 IT Mill Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +@SuppressWarnings("serial") +public abstract class AbstractField extends AbstractComponent implements Field, + Property.ReadOnlyStatusChangeNotifier { + + /* Private members */ + + private boolean delayedFocus; + + /** + * Value of the abstract field. + */ + private Object value; + + /** + * Connected data-source. + */ + private Property dataSource = null; + + /** + * The list of validators. + */ + private LinkedList validators = null; + + /** + * Auto commit mode. + */ + private boolean writeTroughMode = true; + + /** + * Reads the value from data-source, when it is not modified. + */ + private boolean readTroughMode = true; + + /** + * Is the field modified but not committed. + */ + private boolean modified = 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 tab order number of this field. + */ + private int tabIndex = 0; + + /** + * Required field. + */ + private boolean required = false; + + /** + * The error message for the exception that is thrown when the field is + * required but empty. + */ + private String requiredError = ""; + + /** + * Is automatic validation enabled. + */ + private boolean validationVisible = true; + + /* Component basics */ + + /* + * Paints the field. Don't add a JavaDoc comment here, we use the default + * documentation from the implemented interface. + */ + @Override + public void paintContent(PaintTarget target) throws PaintException { + + // The tab ordering number + if (tabIndex != 0) { + target.addAttribute("tabindex", tabIndex); + } + + // If the field is modified, but not committed, set modified attribute + if (isModified()) { + target.addAttribute("modified", true); + } + + // Adds the required attribute + if (!isReadOnly() && isRequired()) { + target.addAttribute("required", true); + } + + // Hide the error indicator if needed + if (isRequired() && isEmpty() && getComponentError() == null + && getErrorMessage() != null) { + target.addAttribute("hideErrors", true); + } + } + + /* + * Gets the field type Don't add a JavaDoc comment here, we use the default + * documentation from the implemented interface. + */ + public abstract Class 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() + */ + public boolean isInvalidCommitted() { + return invalidCommitted; + } + + /** + * Sets if the invalid data should be committed to datasource. + * + * @see com.vaadin.data.BufferedValidatable#setInvalidCommitted(boolean) + */ + 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. + */ + public void commit() throws Buffered.SourceException, InvalidValueException { + if (dataSource != null && !dataSource.isReadOnly()) { + if ((isInvalidCommitted() || isValid())) { + final Object newValue = getValue(); + try { + + // Commits the value to datasource. + dataSource.setValue(newValue); + + } catch (final Throwable e) { + + // Sets the buffering state. + currentBufferedSourceException = new Buffered.SourceException( + this, e); + requestRepaint(); + + // Throws the source exception. + throw currentBufferedSourceException; + } + } else { + /* An invalid value and we don't allow them, throw the exception */ + validate(); + } + } + + boolean repaintNeeded = false; + + // The abstract field is not modified anymore + if (modified) { + modified = false; + repaintNeeded = true; + } + + // If successful, remove set the buffering state to be ok + if (currentBufferedSourceException != null) { + currentBufferedSourceException = null; + repaintNeeded = true; + } + + if (repaintNeeded) { + requestRepaint(); + } + } + + /* + * Updates the value from the data source. Don't add a JavaDoc comment here, + * we use the default documentation from the implemented interface. + */ + public void discard() throws Buffered.SourceException { + if (dataSource != null) { + + // Gets the correct value from datasource + Object newValue; + try { + + // Discards buffer by overwriting from datasource + newValue = String.class == getType() ? dataSource.toString() + : dataSource.getValue(); + + // If successful, remove set the buffering state to be ok + if (currentBufferedSourceException != null) { + currentBufferedSourceException = null; + requestRepaint(); + } + } catch (final Throwable e) { + + // Sets the buffering state + currentBufferedSourceException = new Buffered.SourceException( + this, e); + requestRepaint(); + + // Throws the source exception + throw currentBufferedSourceException; + } + + final boolean wasModified = isModified(); + modified = false; + + // If the new value differs from the previous one + if ((newValue == null && value != null) + || (newValue != null && !newValue.equals(value))) { + setInternalValue(newValue); + fireValueChange(false); + } + + // If the value did not change, but the modification status did + else if (wasModified) { + requestRepaint(); + } + } + } + + /* + * 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. + */ + public boolean isModified() { + return modified; + } + + /* + * 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. + */ + public boolean isWriteThrough() { + return writeTroughMode; + } + + /* + * Sets the field's write-through mode to the specified status Don't add a + * JavaDoc comment here, we use the default documentation from the + * implemented interface. + */ + public void setWriteThrough(boolean writeTrough) + throws Buffered.SourceException, InvalidValueException { + if (writeTroughMode == writeTrough) { + return; + } + writeTroughMode = writeTrough; + if (writeTroughMode) { + 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. + */ + public boolean isReadThrough() { + return readTroughMode; + } + + /* + * Sets the field's read-through mode to the specified status Don't add a + * JavaDoc comment here, we use the default documentation from the + * implemented interface. + */ + public void setReadThrough(boolean readTrough) + throws Buffered.SourceException { + if (readTroughMode == readTrough) { + return; + } + readTroughMode = readTrough; + if (!isModified() && readTroughMode && dataSource != null) { + setInternalValue(String.class == getType() ? dataSource.toString() + : dataSource.getValue()); + fireValueChange(false); + } + } + + /* Property interface implementation */ + + /** + * Returns the value of the Property in human readable textual format. + * + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + final Object value = getValue(); + if (value == null) { + return null; + } + return getValue().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. In the read-through mode, the abstract buffer is + * also updated and validation is performed. + * </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 datasources + * native type, use getPropertyDatasource().getValue() instead. + * </p> + * + * <p> + * Note that when you extend AbstractField, you must reimplement this method + * if datasource.getValue() is not assignable to class returned by getType() + * AND getType() is not String. In case of Strings, getValue() calls + * datasource.toString() instead of datasource.getValue(). + * </p> + * + * @return the current value of the field. + */ + public Object getValue() { + + // Give the value from abstract buffers if the field if possible + if (dataSource == null || !isReadThrough() || isModified()) { + return value; + } + + Object newValue = String.class == getType() ? dataSource.toString() + : dataSource.getValue(); + if ((newValue == null && value != null) + || (newValue != null && !newValue.equals(value))) { + setInternalValue(newValue); + fireValueChange(false); + } + + return newValue; + } + + /** + * Sets the value of the field. + * + * @param newValue + * the New value of the field. + * @throws Property.ReadOnlyException + * @throws Property.ConversionException + */ + public void setValue(Object newValue) throws Property.ReadOnlyException, + Property.ConversionException { + setValue(newValue, false); + } + + /** + * Sets the value of the field. + * + * @param newValue + * the New value of the field. + * @param repaintIsNotNeeded + * True iff caller is sure that repaint is not needed. + * @throws Property.ReadOnlyException + * @throws Property.ConversionException + */ + protected void setValue(Object newValue, boolean repaintIsNotNeeded) + throws Property.ReadOnlyException, Property.ConversionException { + + if ((newValue == null && value != null) + || (newValue != null && !newValue.equals(value))) { + + // 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)) { + repaintIsNotNeeded = false; + } + + // If invalid values are not allowed, the value must be checked + if (!isInvalidAllowed()) { + final Collection v = getValidators(); + if (v != null) { + for (final Iterator i = v.iterator(); i.hasNext();) { + ((Validator) i.next()).validate(newValue); + } + } + } + + // Changes the value + setInternalValue(newValue); + modified = dataSource != null; + + // In write trough mode , try to commit + if (isWriteThrough() && dataSource != null + && (isInvalidCommitted() || isValid())) { + try { + + // Commits the value to datasource + dataSource.setValue(newValue); + + // The buffer is now unmodified + modified = false; + + } catch (final Throwable e) { + + // Sets the buffering state + currentBufferedSourceException = new Buffered.SourceException( + this, e); + requestRepaint(); + + // Throws the source exception + throw currentBufferedSourceException; + } + } + + // If successful, remove set the buffering state to be ok + if (currentBufferedSourceException != null) { + currentBufferedSourceException = null; + requestRepaint(); + } + + // Fires the value change + fireValueChange(repaintIsNotNeeded); + } + } + + /* 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. + */ + public Property getPropertyDataSource() { + return dataSource; + } + + /** + * <p> + * Sets the specified Property as the data source for the field. All + * uncommitted changes to the field are discarded and the value is refreshed + * 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> + * + * @param newDataSource + * the new data source Property. + */ + public void setPropertyDataSource(Property newDataSource) { + + // Saves the old value + final Object oldValue = value; + + // Discards all changes to old datasource + try { + discard(); + } catch (final Buffered.SourceException ignored) { + } + + // 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; + + // Gets the value from source + try { + if (dataSource != null) { + setInternalValue(String.class == getType() ? dataSource + .toString() : dataSource.getValue()); + } + modified = false; + } catch (final Throwable e) { + currentBufferedSourceException = new Buffered.SourceException(this, + e); + modified = true; + } + + // Listens the new data source if possible + if (dataSource instanceof Property.ValueChangeNotifier) { + ((Property.ValueChangeNotifier) dataSource).addListener(this); + } + + // Copy the validators from the data source + if (dataSource instanceof Validatable) { + final Collection validators = ((Validatable) dataSource) + .getValidators(); + if (validators != null) { + for (final Iterator i = validators.iterator(); i.hasNext();) { + addValidator((Validator) i.next()); + } + } + } + + // Fires value change if the value has changed + if ((value != oldValue) + && ((value != null && !value.equals(oldValue)) || value == null)) { + fireValueChange(false); + } + } + + /* 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. + */ + public void addValidator(Validator validator) { + if (validators == null) { + validators = new LinkedList(); + } + validators.add(validator); + requestRepaint(); + } + + /** + * Gets the validators of the field. + * + * @return the Unmodifiable collection that holds all validators for the + * field. + */ + public Collection getValidators() { + if (validators == null || validators.isEmpty()) { + return null; + } + return Collections.unmodifiableCollection(validators); + } + + /** + * Removes the validator from the field. + * + * @param validator + * the validator to remove. + */ + public void removeValidator(Validator validator) { + if (validators != null) { + validators.remove(validator); + } + requestRepaint(); + } + + /** + * Tests the current value against all registered validators. + * + * @return <code>true</code> if all registered validators claim that the + * current value is valid, <code>false</code> otherwise. + */ + public boolean isValid() { + + if (isEmpty()) { + if (isRequired()) { + return false; + } else { + return true; + } + } + + if (validators == null) { + return true; + } + + final Object value = getValue(); + for (final Iterator i = validators.iterator(); i.hasNext();) { + if (!((Validator) i.next()).isValid(value)) { + return false; + } + } + + return true; + } + + /** + * Checks the validity of the Validatable by validating the field with all + * attached validators. + * + * The "required" validation is a built-in validation feature. If the field + * is required, but empty, validation will throw an EmptyValueException with + * the error message set with setRequiredError(). + * + * @see com.vaadin.data.Validatable#validate() + */ + public void validate() throws Validator.InvalidValueException { + + if (isEmpty()) { + if (isRequired()) { + throw new Validator.EmptyValueException(requiredError); + } else { + return; + } + } + + // If there is no validator, there can not be any errors + if (validators == null) { + return; + } + + // Initialize temps + Validator.InvalidValueException firstError = null; + LinkedList errors = null; + final Object value = getValue(); + + // Gets all the validation errors + for (final Iterator i = validators.iterator(); i.hasNext();) { + try { + ((Validator) i.next()).validate(value); + } catch (final Validator.InvalidValueException e) { + if (firstError == null) { + firstError = e; + } else { + if (errors == null) { + errors = new LinkedList(); + errors.add(firstError); + } + errors.add(e); + } + } + } + + // If there were no error + if (firstError == null) { + return; + } + + // If only one error occurred, throw it forwards + if (errors == null) { + throw firstError; + } + + // Creates composite validator + final Validator.InvalidValueException[] exceptions = new Validator.InvalidValueException[errors + .size()]; + int index = 0; + for (final Iterator i = errors.iterator(); i.hasNext();) { + exceptions[index++] = (Validator.InvalidValueException) i.next(); + } + + throw new Validator.InvalidValueException(null, exceptions); + } + + /** + * 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() + */ + 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) + */ + 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. + */ + ErrorMessage 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 + && currentBufferedSourceException == null) { + return null; + } + + // Throw combination of the error types + return new CompositeErrorMessage(new ErrorMessage[] { superError, + validationError, currentBufferedSourceException }); + + } + + /* 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. + */ + 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. + */ + 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"); + } + } + + /** + * An <code>Event</code> object specifying the Property whose read-only + * status has changed. + * + * @author IT Mill Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ + public 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. + */ + 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. + */ + 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. + */ + 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. + * + * @param event + * the value change event telling the data source contents have + * changed. + */ + public void valueChange(Property.ValueChangeEvent event) { + if (isReadThrough() || !isModified()) { + fireValueChange(false); + } + } + + @Override + public void changeVariables(Object source, Map variables) { + super.changeVariables(source, variables); + + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.Component.Focusable#focus() + */ + public void focus() { + final Application app = getApplication(); + if (app != null) { + getWindow().setFocusedComponent(this); + delayedFocus = false; + } else { + delayedFocus = true; + } + } + + /** + * Creates abstract field by the type of the property. + * + * <p> + * This returns most suitable field type for editing property of given type. + * </p> + * + * @param propertyType + * the Type of the property, that needs to be edited. + */ + public static AbstractField constructField(Class propertyType) { + + // Null typed properties can not be edited + if (propertyType == null) { + return null; + } + + // Date field + if (Date.class.isAssignableFrom(propertyType)) { + return new DateField(); + } + + // Boolean field + if (Boolean.class.isAssignableFrom(propertyType)) { + final Button button = new Button(""); + button.setSwitchMode(true); + button.setImmediate(false); + return button; + } + + // Text field is used by default + return new TextField(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.Component.Focusable#getTabIndex() + */ + public int getTabIndex() { + return tabIndex; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.Component.Focusable#setTabIndex(int) + */ + public void setTabIndex(int tabIndex) { + this.tabIndex = tabIndex; + } + + /** + * 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 overriden by the inheriting classes to update all dependent + * variables. + * + * @param newValue + * the new value to be set. + */ + protected void setInternalValue(Object 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 (delayedFocus) { + focus(); + } + } + + /** + * 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>. + */ + public boolean isRequired() { + return required; + } + + /** + * 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. + */ + public void setRequired(boolean required) { + this.required = 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. + */ + public void setRequiredError(String requiredMessage) { + requiredError = requiredMessage; + requestRepaint(); + } + + public String getRequiredError() { + return requiredError; + } + + /** + * 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 (getValue() == 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(); + } + +}
\ No newline at end of file diff --git a/src/com/vaadin/ui/AbstractLayout.java b/src/com/vaadin/ui/AbstractLayout.java new file mode 100644 index 0000000000..3604e5ea1d --- /dev/null +++ b/src/com/vaadin/ui/AbstractLayout.java @@ -0,0 +1,92 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.PaintTarget; +import com.vaadin.ui.Layout.MarginHandler; + +/** + * An abstract class that defines default implementation for the {@link Layout} + * interface. + * + * @author IT Mill Ltd. + * @version + * @VERSION@ + * @since 5.0 + */ +@SuppressWarnings("serial") +public abstract class AbstractLayout extends AbstractComponentContainer + implements Layout, MarginHandler { + + protected MarginInfo margins = new MarginInfo(false); + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.AbstractComponent#getTag() + */ + @Override + public abstract String getTag(); + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.Layout#setMargin(boolean) + */ + public void setMargin(boolean enabled) { + margins.setMargins(enabled); + requestRepaint(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.Layout.MarginHandler#getMargin() + */ + public MarginInfo getMargin() { + return margins; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.Layout.MarginHandler#setMargin(MarginInfo) + */ + public void setMargin(MarginInfo marginInfo) { + margins.setMargins(marginInfo); + requestRepaint(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.Layout#setMargin(boolean, boolean, boolean, + * boolean) + */ + public void setMargin(boolean topEnabled, boolean rightEnabled, + boolean bottomEnabled, boolean leftEnabled) { + margins + .setMargins(topEnabled, rightEnabled, bottomEnabled, + leftEnabled); + requestRepaint(); + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.ui.AbstractComponent#paintContent(com.vaadin + * .terminal.PaintTarget) + */ + @Override + public void paintContent(PaintTarget target) throws PaintException { + + // Add margin info. Defaults to false. + target.addAttribute("margins", margins.getBitMask()); + + } + +} diff --git a/src/com/vaadin/ui/AbstractOrderedLayout.java b/src/com/vaadin/ui/AbstractOrderedLayout.java new file mode 100644 index 0000000000..e83a71eeaf --- /dev/null +++ b/src/com/vaadin/ui/AbstractOrderedLayout.java @@ -0,0 +1,366 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.Map; +import java.util.Map.Entry; + +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.PaintTarget; +import com.vaadin.terminal.Sizeable; + +@SuppressWarnings("serial") +public abstract class AbstractOrderedLayout extends AbstractLayout implements + Layout.AlignmentHandler, Layout.SpacingHandler { + + private 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). + */ + private final Map<Component, Alignment> componentToAlignment = new HashMap<Component, Alignment>(); + + private final Map<Component, Float> componentToExpandRatio = new HashMap<Component, Float>(); + + /** + * Is spacing between contained components enabled. Defaults to false. + */ + private boolean spacing = false; + + /** + * Gets the component UIDL tag. + * + * @return the Component UIDL tag as string. + */ + @Override + public String getTag() { + return "orderedlayout"; + } + + /** + * 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) { + super.addComponent(c); + components.add(c); + requestRepaint(); + } + + /** + * 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) { + super.addComponent(c); + components.addFirst(c); + requestRepaint(); + } + + /** + * 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) { + super.addComponent(c); + components.add(index, c); + requestRepaint(); + } + + /** + * Removes the component from this container. + * + * @param c + * the component to be removed. + */ + @Override + public void removeComponent(Component c) { + super.removeComponent(c); + components.remove(c); + componentToAlignment.remove(c); + componentToExpandRatio.remove(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. + */ + public Iterator getComponentIterator() { + return components.iterator(); + } + + /** + * 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 { + super.paintContent(target); + + // Add spacing attribute (omitted if false) + if (spacing) { + target.addAttribute("spacing", spacing); + } + + final String[] alignmentsArray = new String[components.size()]; + final Integer[] expandRatioArray = new Integer[components.size()]; + float sum = getExpandRatioSum(); + boolean equallyDivided = false; + int realSum = 0; + if (sum == 0 && components.size() > 0) { + // no component has been expanded, all components have same expand + // rate + equallyDivided = true; + float equalSize = 1 / (float) components.size(); + int myRatio = Math.round(equalSize * 1000); + for (int i = 0; i < expandRatioArray.length; i++) { + expandRatioArray[i] = myRatio; + } + realSum = myRatio * components.size(); + } + + // Adds all items in all the locations + int index = 0; + for (final Iterator i = components.iterator(); i.hasNext();) { + final Component c = (Component) i.next(); + if (c != null) { + // Paint child component UIDL + c.paint(target); + alignmentsArray[index] = String + .valueOf(getComponentAlignment(c).getBitMask()); + if (!equallyDivided) { + int myRatio = Math.round((getExpandRatio(c) / sum) * 1000); + expandRatioArray[index] = myRatio; + realSum += myRatio; + } + index++; + } + } + + // correct possible rounding error + if (expandRatioArray.length > 0) { + expandRatioArray[0] -= realSum - 1000; + } + + // Add child component alignment info to layout tag + target.addAttribute("alignments", alignmentsArray); + target.addAttribute("expandRatios", expandRatioArray); + } + + private float getExpandRatioSum() { + float sum = 0; + for (Iterator<Entry<Component, Float>> iterator = componentToExpandRatio + .entrySet().iterator(); iterator.hasNext();) { + sum += iterator.next().getValue(); + } + return sum; + } + + /* Documented in superclass */ + public void replaceComponent(Component oldComponent, Component newComponent) { + + // Gets the locations + int oldLocation = -1; + int newLocation = -1; + int location = 0; + for (final Iterator i = components.iterator(); i.hasNext();) { + final Component 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); + componentToAlignment.remove(newComponent); + components.add(oldLocation, newComponent); + } else { + components.remove(newComponent); + components.add(oldLocation, newComponent); + components.remove(oldComponent); + componentToAlignment.remove(oldComponent); + components.add(newLocation, oldComponent); + } + + requestRepaint(); + } + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.ui.Layout.AlignmentHandler#setComponentAlignment(com + * .itmill.toolkit.ui.Component, int, int) + */ + public void setComponentAlignment(Component childComponent, + int horizontalAlignment, int verticalAlignment) { + if (components.contains(childComponent)) { + // Alignments are bit masks + componentToAlignment.put(childComponent, new Alignment( + horizontalAlignment + verticalAlignment)); + requestRepaint(); + } else { + throw new IllegalArgumentException( + "Component must be added to layout before using setComponentAlignment()"); + } + } + + public void setComponentAlignment(Component childComponent, + Alignment alignment) { + if (components.contains(childComponent)) { + componentToAlignment.put(childComponent, alignment); + requestRepaint(); + } else { + throw new IllegalArgumentException( + "Component must be added to layout before using setComponentAlignment()"); + } + + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.ui.Layout.AlignmentHandler#getComponentAlignment(com + * .itmill.toolkit.ui.Component) + */ + public Alignment getComponentAlignment(Component childComponent) { + Alignment alignment = componentToAlignment.get(childComponent); + if (alignment == null) { + return ALIGNMENT_DEFAULT; + } else { + return alignment; + } + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.Layout.SpacingHandler#setSpacing(boolean) + */ + public void setSpacing(boolean enabled) { + spacing = enabled; + requestRepaint(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.Layout.SpacingHandler#isSpacing() + */ + @Deprecated + public boolean isSpacingEnabled() { + return spacing; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.Layout.SpacingHandler#isSpacing() + */ + public boolean isSpacing() { + return spacing; + } + + /** + * <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) { + if (components.contains(component)) { + componentToExpandRatio.put(component, ratio); + requestRepaint(); + } else { + throw new IllegalArgumentException( + "Component must be added to layout before using setExpandRatio()"); + } + }; + + /** + * 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) { + Float ratio = componentToExpandRatio.get(component); + return (ratio == null) ? 0 : ratio.floatValue(); + } + + public void setComponentAlignment(Component component, String alignment) { + AlignmentUtils.setComponentAlignment(this, component, alignment); + } + +} diff --git a/src/com/vaadin/ui/AbstractSelect.java b/src/com/vaadin/ui/AbstractSelect.java new file mode 100644 index 0000000000..4719a98371 --- /dev/null +++ b/src/com/vaadin/ui/AbstractSelect.java @@ -0,0 +1,1679 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +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.terminal.KeyMapper; +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 IT Mill Ltd. + * @version + * @VERSION@ + * @since 5.0 + */ +@SuppressWarnings("serial") +public abstract class AbstractSelect extends AbstractField implements + Container, Container.Viewer, Container.PropertySetChangeListener, + Container.PropertySetChangeNotifier, Container.ItemSetChangeNotifier, + Container.ItemSetChangeListener { + + /** + * Item caption mode: Item's ID's <code>String</code> representation is used + * as caption. + */ + public static final int ITEM_CAPTION_MODE_ID = 0; + /** + * Item caption mode: Item's <code>String</code> representation is used as + * caption. + */ + public static final int ITEM_CAPTION_MODE_ITEM = 1; + /** + * 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. + */ + public static final int ITEM_CAPTION_MODE_INDEX = 2; + /** + * 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>. + */ + public static final int ITEM_CAPTION_MODE_EXPLICIT_DEFAULTS_ID = 3; + /** + * Item caption mode: Captions must be explicitly specified. + */ + public static final int ITEM_CAPTION_MODE_EXPLICIT = 4; + /** + * Item caption mode: Only icons are shown, captions are hidden. + */ + public static final int ITEM_CAPTION_MODE_ICON_ONLY = 5; + /** + * Item caption mode: Item captions are read from property specified with + * <code>setItemCaptionPropertyId</code>. + */ + public static final int ITEM_CAPTION_MODE_PROPERTY = 6; + + /** + * 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(); + + } + + /** + * 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 itemIdMapper = new KeyMapper(); + + /** + * Item icons. + */ + private final HashMap itemIcons = new HashMap(); + + /** + * Item captions. + */ + private final HashMap itemCaptions = new HashMap(); + + /** + * Item caption mode. + */ + private int itemCaptionMode = ITEM_CAPTION_MODE_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 propertySetEventListeners = null; + + /** + * List of item set change event listeners. + */ + private Set 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 field properties + super.paintContent(target); + + // 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())) { + // Gets the option attribute values + final Object id = getNullSelectionItemId(); + final String key = itemIdMapper.key(id); + final String caption = getItemCaption(id); + final Resource icon = getItemIcon(id); + // Paints option + target.startTag("so"); + if (icon != null) { + target.addAttribute("icon", icon); + } + target.addAttribute("caption", caption); + target.addAttribute("nullselection", true); + target.addAttribute("key", key); + if (isSelected(id)) { + target.addAttribute("selected", true); + selectedKeys[keyIndex++] = key; + } + 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); + final String caption = getItemCaption(id); + // add listener for each item, to cause repaint if an item changes + getCaptionChangeListener().addNotifierForItem(id); + final Resource icon = getItemIcon(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"); + // == + + // Paint variables + target.addVariable(this, "selected", selectedKeys); + if (isNewItemsAllowed()) { + target.addVariable(this, "newitem", ""); + } + + } + + /** + * 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 variables) { + super.changeVariables(source, variables); + + // New option entered (and it is allowed) + final String newitem = (String) variables.get("newitem"); + if (newitem != null && newitem.length() > 0) { + getNewItemHandler().addNewItem(newitem); + } + + // Selection change + if (variables.containsKey("selected")) { + final String[] ka = (String[]) variables.get("selected"); + + // Multiselect mode + if (isMultiSelect()) { + + // TODO Optimize by adding repaintNotNeeded when applicable + + // Converts the key-array to id-set + final LinkedList s = new LinkedList(); + 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; + } + + // Limits the deselection to the set of visible items + // (non-visible items can not be deselected) + final Collection visible = getVisibleItemIds(); + if (visible != null) { + Set newsel = (Set) getValue(); + if (newsel == null) { + newsel = new HashSet(); + } else { + newsel = new HashSet(newsel); + } + newsel.removeAll(visible); + newsel.addAll(s); + setValue(newsel, true); + } + } else { + // Single select mode + if (!isNullSelectionAllowed() + && (ka.length == 0 || ka[0] == null || ka[0] == getNullSelectionItemId())) { + requestRepaint(); + return; + } + 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 (!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 { + 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) { + try { + getContainerProperty(newItemCaption, + getItemCaptionPropertyId()).setValue( + newItemCaption); + } catch (final Property.ConversionException ignored) { + /* + * The conversion exception is safely ignored, the + * caption is just missing + */ + } + } + if (isMultiSelect()) { + Set values = new HashSet((Collection) getValue()); + values.add(newItemCaption); + setValue(values); + } else { + setValue(newItemCaption); + } + } + } + } + + /** + * Gets the component UIDL tag. + * + * @return the Component UIDL tag as string. + */ + @Override + public String getTag() { + return "select"; + } + + /** + * 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() { + if (isVisible()) { + return getItemIds(); + } + return null; + } + + /* 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(); + } + if (retValue instanceof Set) { + return Collections.unmodifiableSet((Set) retValue); + } else if (retValue instanceof Collection) { + return new HashSet((Collection) retValue); + } else { + final Set s = new HashSet(); + 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, + Property.ConversionException { + if (newValue == null) { + newValue = getNullSelectionItemId(); + } + + 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, Property.ConversionException { + + if (isMultiSelect()) { + if (newValue == null) { + super.setValue(new HashSet(), repaintIsNotNeeded); + } else if (Collection.class.isAssignableFrom(newValue.getClass())) { + super.setValue(new HashSet((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. + */ + public Item getItem(Object itemId) { + return items.getItem(itemId); + } + + /** + * Gets the item Id collection from the container. + * + * @return the Collection of item ids. + */ + public Collection getItemIds() { + return items.getItemIds(); + } + + /** + * Gets the property Id collection from the container. + * + * @return the Collection of property ids. + */ + 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) + */ + 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() + */ + 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. + */ + 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) + */ + 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) + */ + 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() + */ + 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() + */ + 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) + */ + public Item addItem(Object itemId) throws UnsupportedOperationException { + + final Item retval = items.addItem(itemId); + if (retval != null + && !(items instanceof Container.ItemSetChangeNotifier)) { + fireItemSetChange(); + } + return retval; + } + + /** + * Removes the item identified by Id from the container. This functionality + * is optional. If the function is not implemented, the functions allways + * returns false. + * + * @return True if the operation succeeded. + * @see com.vaadin.data.Container#removeItem(java.lang.Object) + */ + 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) + */ + 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 as data-source for viewing. + * + * @param newDataSource + * the new data source. + */ + 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); + } + } + + requestRepaint(); + } + } + + /** + * Gets the viewing data-source container. + * + * @see com.vaadin.data.Container.Viewer#getContainerDataSource() + */ + 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 loose + * selection information: if selected items set contains one or more + * selected items, only one of the selected items is kept as selected. + * + * @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 s = new HashSet(); + 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 ITEM_CAPTION_MODE_ID: + caption = itemId.toString(); + break; + + case ITEM_CAPTION_MODE_INDEX: + if (items instanceof Container.Indexed) { + caption = String.valueOf(((Container.Indexed) items) + .indexOfId(itemId)); + } else { + caption = "ERROR: Container is not indexed"; + } + break; + + case ITEM_CAPTION_MODE_ITEM: + final Item i = getItem(itemId); + if (i != null) { + caption = i.toString(); + } + break; + + case ITEM_CAPTION_MODE_EXPLICIT: + caption = (String) itemCaptions.get(itemId); + break; + + case ITEM_CAPTION_MODE_EXPLICIT_DEFAULTS_ID: + caption = (String) itemCaptions.get(itemId); + if (caption == null) { + caption = itemId.toString(); + } + break; + + case ITEM_CAPTION_MODE_PROPERTY: + final Property p = getContainerProperty(itemId, + getItemCaptionPropertyId()); + if (p != null) { + caption = p.toString(); + } + break; + } + + // All items must have some captions + return caption != null ? caption : ""; + } + + /** + * Sets the icon for an item. + * + * @param itemId + * the id of the item to be assigned an icon. + * @param icon + * the New icon. + */ + 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 = (Resource) 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(int mode) { + if (ITEM_CAPTION_MODE_ID <= mode && mode <= ITEM_CAPTION_MODE_PROPERTY) { + 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 int 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> + * 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 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> + * . + * + * @param propertyId + * the Id of the property that specifies icons for items. + */ + public void setItemIconPropertyId(Object propertyId) { + if ((propertyId != null) + && Resource.class.isAssignableFrom(getType(propertyId))) { + itemIconPropertyId = propertyId; + } else { + itemIconPropertyId = null; + } + 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 tem 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 s = new HashSet((Set) getValue()); + s.add(itemId); + setValue(s); + } + } + + /** + * Unselects an item. + * + * @param itemId + * the Item to be unselected. + * @see #getNullSelectionItemId() + * @see #setNullSelectionItemId(Object) + * + */ + public void unselect(Object itemId) { + if (isSelected(itemId)) { + if (isMultiSelect()) { + final Set s = new HashSet((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) + */ + 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) + */ + public void addListener(Container.PropertySetChangeListener listener) { + if (propertySetEventListeners == null) { + propertySetEventListeners = new LinkedHashSet(); + } + propertySetEventListeners.add(listener); + } + + /** + * Removes a previously registered Property set change listener. + * + * @see com.vaadin.data.Container.PropertySetChangeNotifier#removeListener(com.vaadin.data.Container.PropertySetChangeListener) + */ + 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) + */ + public void addListener(Container.ItemSetChangeListener listener) { + if (itemSetEventListeners == null) { + itemSetEventListeners = new LinkedHashSet(); + } + itemSetEventListeners.add(listener); + } + + /** + * Removes the Item set change listener from the object. + * + * @see com.vaadin.data.Container.ItemSetChangeNotifier#removeListener(com.vaadin.data.Container.ItemSetChangeListener) + */ + public void removeListener(Container.ItemSetChangeListener listener) { + if (itemSetEventListeners != null) { + itemSetEventListeners.remove(listener); + if (itemSetEventListeners.isEmpty()) { + itemSetEventListeners = null; + } + } + } + + /** + * Lets the listener know a Containers Item set has changed. + * + * @see com.vaadin.data.Container.ItemSetChangeListener#containerItemSetChange(com.vaadin.data.Container.ItemSetChangeEvent) + */ + 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() + */ + 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() + */ + public Container getContainer() { + return AbstractSelect.this; + } + + } + + /** + * Allow of disallow empty selection. 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. + * + * @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 final 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 + * idetified 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 { + + HashSet captionChangeNotifiers = new HashSet(); + + public void addNotifierForItem(Object itemId) { + switch (getItemCaptionMode()) { + case ITEM_CAPTION_MODE_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 ITEM_CAPTION_MODE_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 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()); + } + } + } + + public void valueChange( + com.vaadin.data.Property.ValueChangeEvent event) { + requestRepaint(); + } + + public void itemPropertySetChange( + com.vaadin.data.Item.PropertySetChangeEvent event) { + requestRepaint(); + } + + } + +} diff --git a/src/com/vaadin/ui/Accordion.java b/src/com/vaadin/ui/Accordion.java new file mode 100644 index 0000000000..73b4c03743 --- /dev/null +++ b/src/com/vaadin/ui/Accordion.java @@ -0,0 +1,11 @@ +package com.vaadin.ui; + +@SuppressWarnings("serial") +public class Accordion extends TabSheet { + + @Override + public String getTag() { + return "accordion"; + } + +} diff --git a/src/com/vaadin/ui/Alignment.java b/src/com/vaadin/ui/Alignment.java new file mode 100644 index 0000000000..12d4c00110 --- /dev/null +++ b/src/com/vaadin/ui/Alignment.java @@ -0,0 +1,155 @@ +package com.vaadin.ui; + +import java.io.Serializable; + +import com.vaadin.terminal.gwt.client.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/src/com/vaadin/ui/AlignmentUtils.java b/src/com/vaadin/ui/AlignmentUtils.java new file mode 100644 index 0000000000..28076596df --- /dev/null +++ b/src/com/vaadin/ui/AlignmentUtils.java @@ -0,0 +1,145 @@ +package com.vaadin.ui; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; + +import com.vaadin.ui.Layout.AlignmentHandler; + +/** + * Helper class for setting alignments using a short notation. + * + * Supported notation is: + * + * t,top for top alignment + * + * m,middle for vertical center alignment + * + * b,bottom for bottom alignment + * + * l,left for left alignment + * + * c,center for horizontal center alignment + * + * r,right for right alignment + * + */ +@SuppressWarnings("serial") +public class AlignmentUtils implements Serializable { + + private static int horizontalMask = AlignmentHandler.ALIGNMENT_LEFT + | AlignmentHandler.ALIGNMENT_HORIZONTAL_CENTER + | AlignmentHandler.ALIGNMENT_RIGHT; + private static int verticalMask = AlignmentHandler.ALIGNMENT_TOP + | AlignmentHandler.ALIGNMENT_VERTICAL_CENTER + | AlignmentHandler.ALIGNMENT_BOTTOM; + + private static Map<String, Integer> alignmentStrings = new HashMap(); + + private static void addMapping(int alignment, String... values) { + for (String s : values) { + alignmentStrings.put(s, alignment); + } + } + + static { + addMapping(AlignmentHandler.ALIGNMENT_TOP, "t", "top"); + addMapping(AlignmentHandler.ALIGNMENT_BOTTOM, "b", "bottom"); + addMapping(AlignmentHandler.ALIGNMENT_VERTICAL_CENTER, "m", "middle"); + + addMapping(AlignmentHandler.ALIGNMENT_LEFT, "l", "left"); + addMapping(AlignmentHandler.ALIGNMENT_RIGHT, "r", "right"); + addMapping(AlignmentHandler.ALIGNMENT_HORIZONTAL_CENTER, "c", "center"); + } + + /** + * Set the alignment for the component using short notation + * + * @param parent + * @param component + * @param alignment + * String containing one or two alignment strings. If short + * notation "r","t",etc is used valid strings include + * "r","rt","tr","t". If the longer notation is used the + * alignments should be separated by a space e.g. + * "right","right top","top right","top". It is valid to mix + * short and long notation but they must be separated by a space + * e.g. "r top". + * @throws IllegalArgumentException + */ + public static void setComponentAlignment(AlignmentHandler parent, + Component component, String alignment) + throws IllegalArgumentException { + if (alignment == null || alignment.length() == 0) { + throw new IllegalArgumentException( + "alignment for setComponentAlignment() cannot be null or empty"); + } + + Integer currentAlignment = parent.getComponentAlignment(component) + .getBitMask(); + + if (alignment.length() == 1) { + // Use short form "t","l",... + currentAlignment = parseAlignment(alignment.substring(0, 1), + currentAlignment); + } else if (alignment.length() == 2) { + // Use short form "tr","lb",... + currentAlignment = parseAlignment(alignment.substring(0, 1), + currentAlignment); + currentAlignment = parseAlignment(alignment.substring(1, 2), + currentAlignment); + } else { + // Alignments are separated by space + String[] strings = alignment.split(" "); + if (strings.length > 2) { + throw new IllegalArgumentException( + "alignment for setComponentAlignment() should not contain more than 2 alignments"); + } + for (String alignmentString : strings) { + currentAlignment = parseAlignment(alignmentString, + currentAlignment); + } + } + + int horizontalAlignment = currentAlignment & horizontalMask; + int verticalAlignment = currentAlignment & verticalMask; + parent.setComponentAlignment(component, new Alignment( + horizontalAlignment + verticalAlignment)); + } + + /** + * Parse alignmentString which contains one alignment (horizontal or + * vertical) and return and updated version of the passed alignment where + * the alignment in one direction has been changed. If the passed + * alignmentString is unknown an exception is thrown + * + * @param alignmentString + * @param alignment + * @return + * @throws IllegalArgumentException + */ + private static int parseAlignment(String alignmentString, int alignment) + throws IllegalArgumentException { + Integer parsed = alignmentStrings.get(alignmentString.toLowerCase()); + + if (parsed == null) { + throw new IllegalArgumentException( + "Could not parse alignment string '" + alignmentString + + "'"); + } + + if ((parsed & horizontalMask) != 0) { + // Get the vertical alignment from the current alignment + int vertical = (alignment & verticalMask); + // Add the parsed horizontal alignment + alignment = (vertical | parsed); + } else { + // Get the horizontal alignment from the current alignment + int horizontal = (alignment & horizontalMask); + // Add the parsed vertical alignment + alignment = (horizontal | parsed); + } + + return alignment; + } +} diff --git a/src/com/vaadin/ui/BaseFieldFactory.java b/src/com/vaadin/ui/BaseFieldFactory.java new file mode 100644 index 0000000000..66a153a287 --- /dev/null +++ b/src/com/vaadin/ui/BaseFieldFactory.java @@ -0,0 +1,151 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.util.Date; + +import com.vaadin.data.Container; +import com.vaadin.data.Item; +import com.vaadin.data.Property; + +/** + * Default implementation of the the following Field types are used by default: + * <p> + * <b>Boolean</b>: Button(switchMode:true).<br/> + * <b>Date</b>: DateField(resolution: day).<br/> + * <b>Item</b>: Form. <br/> + * <b>default field type</b>: TextField. + * <p> + * + * @author IT Mill Ltd. + * @version + * @VERSION@ + * @since 3.1 + */ + +@SuppressWarnings("serial") +public class BaseFieldFactory implements FieldFactory { + + /** + * Creates the field based on type of data. + * + * + * @param type + * the type of data presented in field. + * @param uiContext + * the context where the Field is presented. + * + * @see com.vaadin.ui.FieldFactory#createField(Class, Component) + */ + public Field createField(Class type, Component uiContext) { + // 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)) { + final Button button = new Button(); + button.setSwitchMode(true); + button.setImmediate(false); + return button; + } + + // Nested form is used by default + return new TextField(); + } + + /** + * Creates the field based on the datasource property. + * + * @see com.vaadin.ui.FieldFactory#createField(Property, Component) + */ + public Field createField(Property property, Component uiContext) { + if (property != null) { + return createField(property.getType(), uiContext); + } else { + return null; + } + } + + /** + * Creates the field based on the item and property id. + * + * @see com.vaadin.ui.FieldFactory#createField(Item, Object, + * Component) + */ + public Field createField(Item item, Object propertyId, Component uiContext) { + if (item != null && propertyId != null) { + final Field f = createField(item.getItemProperty(propertyId), + uiContext); + if (f instanceof AbstractComponent) { + String name = propertyId.toString(); + if (name.length() > 0) { + + // If name follows method naming conventions, convert the + // name to spaced uppercased text. For example, convert + // "firstName" to "First Name" + 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(); + } + + ((AbstractComponent) f).setCaption(name); + } + } + return f; + } else { + return null; + } + } + + /** + * @see com.vaadin.ui.FieldFactory#createField(com.vaadin.data.Container, + * java.lang.Object, java.lang.Object, com.vaadin.ui.Component) + */ + public Field createField(Container container, Object itemId, + Object propertyId, Component uiContext) { + return createField(container.getContainerProperty(itemId, propertyId), + uiContext); + } + +} diff --git a/src/com/vaadin/ui/Button.java b/src/com/vaadin/ui/Button.java new file mode 100644 index 0000000000..872285f8fc --- /dev/null +++ b/src/com/vaadin/ui/Button.java @@ -0,0 +1,350 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.io.IOException; +import java.io.Serializable; +import java.lang.reflect.Method; +import java.util.Map; + +import com.vaadin.data.Property; +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.PaintTarget; + +/** + * A generic button component. + * + * @author IT Mill Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +@SuppressWarnings("serial") +public class Button extends AbstractField { + + /* Private members */ + + boolean switchMode = false; + + /** + * Creates a new push button. The value of the push button is false and it + * is immediate by default. + * + */ + public Button() { + setValue(new Boolean(false)); + setSwitchMode(false); + } + + /** + * Creates a new push button. + * + * The value of the push button is false and it is immediate by default. + * + * @param caption + * the Button caption. + */ + public Button(String caption) { + this(); + setCaption(caption); + } + + /** + * Creates a new push button with click listener. + * + * @param caption + * the Button caption. + * @param listener + * the Button click listener. + */ + public Button(String caption, ClickListener listener) { + this(caption); + addListener(listener); + } + + /** + * Creates a new push button with a method listening button clicks. Using + * this method is discouraged because it cannot be checked during + * compilation. Use + * {@link #Button(String, com.vaadin.ui.Button.ClickListener)} + * instead. The method must have either no parameters, or only one parameter + * of Button.ClickEvent type. + * + * @param caption + * the Button caption. + * @param target + * the Object having the method for listening button clicks. + * @param methodName + * the name of the method in target object, that receives button + * click events. + */ + public Button(String caption, Object target, String methodName) { + this(caption); + addListener(ClickEvent.class, target, methodName); + } + + /** + * Creates a new switch button with initial value. + * + * @param state + * the Initial state of the switch-button. + * @param initialState + */ + public Button(String caption, boolean initialState) { + setCaption(caption); + setValue(new Boolean(initialState)); + setSwitchMode(true); + } + + /** + * Creates a new switch button that is connected to a boolean property. + * + * @param state + * the Initial state of the switch-button. + * @param dataSource + */ + public Button(String caption, Property dataSource) { + setCaption(caption); + setSwitchMode(true); + setPropertyDataSource(dataSource); + } + + /** + * Gets component UIDL tag. + * + * @return the Component UIDL tag as string. + */ + @Override + public String getTag() { + return "button"; + } + + /** + * Paints the content of this component. + * + * @param event + * the PaintEvent. + * @throws IOException + * if the writing failed due to input/output error. + * @throws PaintException + * if the paint operation failed. + */ + @Override + public void paintContent(PaintTarget target) throws PaintException { + super.paintContent(target); + + if (isSwitchMode()) { + target.addAttribute("type", "switch"); + } + target.addVariable(this, "state", booleanValue()); + } + + /** + * Invoked when the value of a variable has changed. Button listeners are + * notified if the button is clicked. + * + * @param source + * @param variables + */ + @Override + public void changeVariables(Object source, Map variables) { + super.changeVariables(source, variables); + + if (!isReadOnly() && variables.containsKey("state")) { + // Gets the new and old button states + final Boolean newValue = (Boolean) variables.get("state"); + final Boolean oldValue = (Boolean) getValue(); + + if (isSwitchMode()) { + + // For switch button, the event is only sent if the + // switch state is changed + if (newValue != null && !newValue.equals(oldValue) + && !isReadOnly()) { + setValue(newValue); + fireClick(); + } + } else { + + // Only send click event if the button is pushed + if (newValue.booleanValue()) { + fireClick(); + } + + // If the button is true for some reason, release it + if (oldValue.booleanValue()) { + setValue(new Boolean(false)); + } + } + } + } + + /** + * Checks if it is switchMode. + * + * @return <code>true</code> if it is in Switch Mode, otherwise + * <code>false</code>. + */ + public boolean isSwitchMode() { + return switchMode; + } + + /** + * Sets the switchMode. + * + * @param switchMode + * The switchMode to set. + */ + public void setSwitchMode(boolean switchMode) { + this.switchMode = switchMode; + if (!switchMode) { + setImmediate(true); + if (booleanValue()) { + setValue(new Boolean(false)); + } + } + } + + /** + * Get the boolean value of the button state. + * + * @return True iff the button is pressed down or checked. + */ + public boolean booleanValue() { + boolean state; + try { + state = ((Boolean) getValue()).booleanValue(); + } catch (final NullPointerException e) { + state = false; + } + return state; + } + + /** + * Sets immediate mode. Push buttons can not be set in non-immediate mode. + * + * @see com.vaadin.ui.AbstractComponent#setImmediate(boolean) + */ + @Override + public void setImmediate(boolean immediate) { + // Push buttons are always immediate + super.setImmediate(!isSwitchMode() || immediate); + } + + /** + * The type of the button as a property. + * + * @see com.vaadin.data.Property#getType() + */ + @Override + public Class getType() { + return Boolean.class; + } + + /* Click event */ + + private static final Method BUTTON_CLICK_METHOD; + + /* Button style with no decorations. Looks like a link, acts like a button */ + public static final String STYLE_LINK = "link"; + + static { + try { + BUTTON_CLICK_METHOD = ClickListener.class.getDeclaredMethod( + "buttonClick", new Class[] { ClickEvent.class }); + } catch (final java.lang.NoSuchMethodException e) { + // This should never happen + throw new java.lang.RuntimeException( + "Internal error finding methods in Button"); + } + } + + /** + * Click event. This event is thrown, when the button is clicked. + * + * @author IT Mill Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ + public class ClickEvent extends Component.Event { + + /** + * New instance of text change event. + * + * @param source + * the Source of the event. + */ + public ClickEvent(Component source) { + super(source); + } + + /** + * Gets the Button where the event occurred. + * + * @return the Source of the event. + */ + public Button getButton() { + return (Button) getSource(); + } + } + + /** + * Button click listener + * + * @author IT Mill Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ + public interface ClickListener extends Serializable { + + /** + * Button has been pressed. + * + * @param event + * Button click event. + */ + 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, BUTTON_CLICK_METHOD); + } + + /** + * Removes the button click listener. + * + * @param listener + * the Listener to be removed. + */ + public void removeListener(ClickListener listener) { + removeListener(ClickEvent.class, listener, BUTTON_CLICK_METHOD); + } + + /** + * Emits the options change event. + */ + protected void fireClick() { + fireEvent(new Button.ClickEvent(this)); + } + + @Override + protected void setInternalValue(Object newValue) { + // Make sure only booleans get through + if (!(newValue instanceof Boolean)) { + throw new IllegalArgumentException(getClass().getSimpleName() + + " only accepts Boolean values"); + } + super.setInternalValue(newValue); + } + +} diff --git a/src/com/vaadin/ui/CheckBox.java b/src/com/vaadin/ui/CheckBox.java new file mode 100644 index 0000000000..32b47412a3 --- /dev/null +++ b/src/com/vaadin/ui/CheckBox.java @@ -0,0 +1,103 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.lang.reflect.Method; + +import com.vaadin.data.Property; + +@SuppressWarnings("serial") +public class CheckBox extends Button { + /** + * Creates a new switch button. + */ + public CheckBox() { + setSwitchMode(true); + } + + /** + * Creates a new switch button with a caption and a set initial state. + * + * @param caption + * the caption of the switch button + * @param initialState + * the initial state of the switch button + */ + public CheckBox(String caption, boolean initialState) { + super(caption, initialState); + } + + /** + * Creates a new switch button with a caption and a click listener. + * + * @param caption + * the caption of the switch button + * @param listener + * the click listener + */ + public CheckBox(String caption, ClickListener listener) { + super(caption, listener); + setSwitchMode(true); + } + + /** + * Convenience method for creating a new switch button with a method + * listening button clicks. 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. + * The method must have either no parameters, or only one parameter of + * Button.ClickEvent type. + * + * @param caption + * the Button caption. + * @param target + * the Object having the method for listening button clicks. + * @param methodName + * the name of the method in target object, that receives button + * click events. + */ + public CheckBox(String caption, Object target, String methodName) { + super(caption, target, methodName); + setSwitchMode(true); + } + + /** + * Creates a new switch button that is connected to a boolean property. + * + * @param state + * the Initial state of the switch-button. + * @param dataSource + */ + public CheckBox(String caption, Property dataSource) { + super(caption, dataSource); + setSwitchMode(true); + } + + /** + * Creates a new push button with a set caption. + * + * The value of the push button is always false and they are immediate by + * default. + * + * @param caption + * the Button caption. + */ + + public CheckBox(String caption) { + super(caption, false); + } + + @Override + public void setSwitchMode(boolean switchMode) + throws UnsupportedOperationException { + if (this.switchMode && !switchMode) { + throw new UnsupportedOperationException( + "CheckBox is always in switch mode (consider using a Button)"); + } + super.setSwitchMode(true); + } + +} diff --git a/src/com/vaadin/ui/ComboBox.java b/src/com/vaadin/ui/ComboBox.java new file mode 100644 index 0000000000..68400986da --- /dev/null +++ b/src/com/vaadin/ui/ComboBox.java @@ -0,0 +1,86 @@ +/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.ui;
+
+import java.util.Collection;
+
+import com.vaadin.data.Container;
+import com.vaadin.terminal.PaintException;
+import com.vaadin.terminal.PaintTarget;
+
+/**
+ * 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;
+
+ public ComboBox() {
+ setMultiSelect(false);
+ setNewItemsAllowed(false);
+ }
+
+ public ComboBox(String caption, Collection options) {
+ super(caption, options);
+ setMultiSelect(false);
+ setNewItemsAllowed(false);
+ }
+
+ public ComboBox(String caption, Container dataSource) {
+ super(caption, dataSource);
+ setMultiSelect(false);
+ setNewItemsAllowed(false);
+ }
+
+ public ComboBox(String caption) {
+ super(caption);
+ setMultiSelect(false);
+ setNewItemsAllowed(false);
+ }
+
+ @Override
+ public void setMultiSelect(boolean multiSelect) {
+ if (multiSelect && !isMultiSelect()) {
+ throw new UnsupportedOperationException("Multiselect not supported");
+ }
+ super.setMultiSelect(multiSelect);
+ }
+
+ /**
+ * 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;
+ }
+
+ @Override
+ public void paintContent(PaintTarget target) throws PaintException {
+ if (inputPrompt != null) {
+ target.addAttribute("prompt", inputPrompt);
+ }
+ super.paintContent(target);
+ }
+
+}
diff --git a/src/com/vaadin/ui/Component.java b/src/com/vaadin/ui/Component.java new file mode 100644 index 0000000000..6b5ead80d2 --- /dev/null +++ b/src/com/vaadin/ui/Component.java @@ -0,0 +1,437 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.io.Serializable; +import java.util.Collection; +import java.util.EventListener; +import java.util.EventObject; +import java.util.Locale; + +import com.vaadin.Application; +import com.vaadin.terminal.ErrorMessage; +import com.vaadin.terminal.Paintable; +import com.vaadin.terminal.Resource; +import com.vaadin.terminal.Sizeable; +import com.vaadin.terminal.VariableOwner; + +/** + * The top-level component interface which must be implemented by all UI + * components that use IT Mill Toolkit. + * + * @author IT Mill Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +public interface Component extends Paintable, VariableOwner, Sizeable, + Serializable { + + /** + * Gets style for component. Multiple styles are joined with spaces. + * + * @return the component's styleValue of property style. + */ + public String getStyleName(); + + /** + * Sets and replaces all previous style names of the component. This method + * will trigger a + * {@link com.vaadin.terminal.Paintable.RepaintRequestEvent + * RepaintRequestEvent}. + * + * @param style + * the new style of the component. + */ + public void setStyleName(String style); + + /** + * Adds style name to component. Handling additional style names is terminal + * specific, but in web browser environment they will most likely become CSS + * classes as given on server side. + * + * This method will trigger a + * {@link com.vaadin.terminal.Paintable.RepaintRequestEvent + * RepaintRequestEvent}. + * + * @param style + * the new style to be added to the component + */ + public void addStyleName(String style); + + /** + * Removes given style name from component. + * + * @param style + * the style to be removed + */ + public void removeStyleName(String style); + + /** + * <p> + * Tests if the component is enabled or not. All the variable change events + * are blocked from disabled components. Also the component should visually + * indicate that it is disabled (by shading the component for example). All + * hidden (isVisible() == false) components must return false. + * </p> + * + * <p> + * <b>Note</b> The component is considered disabled if it's parent is + * disabled. + * </p> + * + * <p> + * Components should be enabled by default. + * </p> + * + * @return <code>true</code> if the component, and it's parent, is enabled + * <code>false</code> otherwise. + * @see VariableOwner#isEnabled() + */ + public boolean isEnabled(); + + /** + * Enables or disables the component. Being enabled means that the component + * can be edited. This method will trigger a + * {@link com.vaadin.terminal.Paintable.RepaintRequestEvent + * RepaintRequestEvent}. + * + * <p> + * <b>Note</b> that after enabling a component, {@link #isEnabled()} might + * still return false if the parent is disabled. + * </p> + * + * <p> + * <b>Also note</b> that if the component contains child-components, it + * should recursively call requestRepaint() for all descendant components. + * </p> + * + * @param enabled + * the boolean value specifying if the component should be + * enabled after the call or not + */ + public void setEnabled(boolean enabled); + + /** + * Tests the components visibility. Visibility defines if the component is + * drawn when updating UI. Default is <code>true</code>. + * + * <p> + * <b>Note</b> that to return true, this component and all its parents must + * be visible. + * + * <p> + * <b>Also note</b> that this method does not check if component is attached + * and shown to user. Component and all its parents may be visible, but not + * necessary attached to application. To test if component will be drawn, + * check its visibility and that {@link Component#getApplication()} does not + * return <code>null</code>. + * + * @return <code>true</code> if the component is visible in the UI, + * <code>false</code> if not + */ + public boolean isVisible(); + + /** + * Sets this components visibility status. Visibility defines if the + * component is shown in the UI or not. + * <p> + * <b>Note</b> that to be shown in UI this component and all its parents + * must be visible. + * + * @param visible + * the boolean value specifying if the component should be + * visible after the call or not. + */ + public void setVisible(boolean visible); + + /** + * Gets the visual parent of the component. The components can be nested but + * one component can have only one parent. + * + * @return the parent component. + */ + public Component getParent(); + + /** + * Sets the component's parent component. + * + * <p> + * This method calls automatically {@link #attach()} if the parent is + * attached to a window (or is itself a window}, and {@link #detach()} if + * <code>parent</code> is set <code>null</code>, but the component was in + * the application. + * </p> + * + * <p> + * This method is rarely called directly. Instead the + * {@link ComponentContainer#addComponent(Component)} method is used to add + * components to container, which call this method implicitly. + * + * @param parent + * the new parent component. + */ + public void setParent(Component parent); + + /** + * Tests if the component is in read-only mode. + * + * @return <code>true</code> if the component is in read-only mode, + * <code>false</code> if not. + */ + public boolean isReadOnly(); + + /** + * Sets the component's to read-only mode to the specified state. This + * method will trigger a + * {@link com.vaadin.terminal.Paintable.RepaintRequestEvent + * RepaintRequestEvent}. + * + * @param readOnly + * the boolean value specifying if the component should be in + * read-only mode after the call or not. + */ + public void setReadOnly(boolean readOnly); + + /** + * Gets the caption of the component. Caption is the visible name of the + * component. + * + * @return the component's caption <code>String</code>. + */ + public String getCaption(); + + /** + * Sets the component's caption <code>String</code>. Caption is the visible + * name of the component. This method will trigger a + * {@link com.vaadin.terminal.Paintable.RepaintRequestEvent + * RepaintRequestEvent}. + * + * @param caption + * the new caption <code>String</code> for the component. + */ + public void setCaption(String caption); + + /** + * Gets the component's icon. A component may have a graphical icon + * associated with it, this method retrieves it if it is defined. + * + * @return the component's icon or <code>null</code> if it not defined. + */ + public Resource getIcon(); + + /** + * Sets the component's icon. This method will trigger a + * {@link com.vaadin.terminal.Paintable.RepaintRequestEvent + * RepaintRequestEvent}. + * + * @param icon + * the icon to be shown with the component's caption. + */ + public void setIcon(Resource icon); + + /** + * Gets the component's parent window. If the component does not yet belong + * to a window <code>null</code> is returned. + * + * @return the parent window of the component or <code>null</code>. + */ + public Window getWindow(); + + /** + * Gets the component's parent application. If the component does not yet + * belong to a application <code>null</code> is returned. + * + * @return the parent application of the component or <code>null</code>. + */ + public Application getApplication(); + + /** + * <p> + * Notifies the component that it is connected to an application. This + * method is always called before the component is first time painted and is + * suitable to be extended. The <code>getApplication</code> and + * <code>getWindow</code> methods might return <code>null</code> before this + * method is called. + * </p> + * + * <p> + * The caller of this method is {@link #setParent(Component)} if the parent + * is already in the application. If the parent is not in the application, + * it must call the {@link #attach()} for all its children when it will be + * added to the application. + * </p> + */ + public void attach(); + + /** + * Notifies the component that it is detached from the application. + * <p> + * The {@link #getApplication()} and {@link #getWindow()} methods might + * return <code>null</code> after this method is called. + * </p> + * + * <p> + * The caller of this method is {@link #setParent(Component)} if the parent + * is in the application. When the parent is detached from the application + * it is its response to call {@link #detach()} for all the children and to + * detach itself from the terminal. + * </p> + */ + public void detach(); + + /** + * Gets the locale of this component. + * + * @return This component's locale. If this component does not have a + * locale, the locale of its parent is returned. Eventually locale + * of application is returned. If application does not have its own + * locale the locale is determined by + * <code>Locale.getDefautlt</code>. Returns null if the component + * does not have its own locale and has not yet been added to a + * containment hierarchy such that the locale can be determined from + * the containing parent. + */ + public Locale getLocale(); + + /** + * The children must call this method when they need repainting. The call + * must be made event in the case the children sent the repaint request + * themselves. + * + * @param alreadyNotified + * the collection of repaint request listeners that have been + * already notified by the child. This component should not + * renotify the listed listeners again. The container given as + * parameter must be modifiable as the component might modify it + * and pass it forwards. Null parameter is interpreted as empty + * collection. + */ + public void childRequestedRepaint(Collection alreadyNotified); + + /* Component event framework */ + + /** + * Superclass of all component originated <code>Event</code>s. + */ + public class Event extends EventObject { + + /** + * Constructs a new event with a specified source component. + * + * @param source + * the source component of the event. + */ + public Event(Component source) { + super(source); + } + } + + /** + * Listener interface for receiving <code>Component.Event</code>s. + */ + public interface Listener extends EventListener, Serializable { + + /** + * Notifies the listener of a component event. + * + * @param event + * the event that has occured. + */ + public void componentEvent(Component.Event event); + } + + /** + * Registers a new component event listener for this component. + * + * @param listener + * the new Listener to be registered. + */ + public void addListener(Component.Listener listener); + + /** + * Removes a previously registered component event listener from this + * component. + * + * @param listener + * the listener to be removed. + */ + public void removeListener(Component.Listener listener); + + /** + * Class of all component originated <code>ErrorEvent</code>s. + */ + @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); + } + + /** + * Interface implemented by components which can obtain input focus. + */ + public interface Focusable extends Component { + + /** + * Sets the focus to this component. + */ + public void focus(); + + /** + * Gets the Tabulator index of this Focusable component. + * + * @return tab index set for this Focusable component + */ + public int getTabIndex(); + + /** + * Sets the tab index of this field. The tab index property is used to + * specify the natural tab order of fields. + * + * @param tabIndex + * the tab order of this component. Indexes usually start + * from 1. Negative value means that field is not wanted to + * tabbing sequence. + */ + public void setTabIndex(int tabIndex); + + } +} diff --git a/src/com/vaadin/ui/ComponentContainer.java b/src/com/vaadin/ui/ComponentContainer.java new file mode 100644 index 0000000000..aa591c9742 --- /dev/null +++ b/src/com/vaadin/ui/ComponentContainer.java @@ -0,0 +1,232 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.io.Serializable; +import java.util.Iterator; + +/** + * 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 IT Mill Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +public interface ComponentContainer extends Component { + + /** + * 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 added. + */ + 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 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. + */ + public Iterator getComponentIterator(); + + /** + * Causes a repaint of this component, and all components below it. + * + * This should only be used in special cases, e.g when the state of a + * descendant depends on the state of a ancestor. + * + */ + public void requestRepaintAll(); + + /** + * 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/src/com/vaadin/ui/CustomComponent.java b/src/com/vaadin/ui/CustomComponent.java new file mode 100644 index 0000000000..19fb081047 --- /dev/null +++ b/src/com/vaadin/ui/CustomComponent.java @@ -0,0 +1,225 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.io.Serializable; +import java.util.Iterator; + +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.PaintTarget; + +/** + * 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 IT Mill Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +@SuppressWarnings("serial") +public class CustomComponent extends AbstractComponentContainer { + + /** + * The root component implementing the custom component. + */ + private Component root = null; + + /** + * Type of the component. + */ + private String componentType = 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 final 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 final 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 ------------------------------------------ */ + + @Override + public void paintContent(PaintTarget target) throws PaintException { + if (root == null) { + throw new IllegalStateException("Composition root must be set to" + + " non-null value before the " + getClass().getName() + + " can be painted"); + } + + if (getComponentType() != null) { + target.addAttribute("type", getComponentType()); + } + root.paint(target); + } + + /** + * Gets the component type. + * + * The component type is textual type of the component. This is included in + * the UIDL as component tag attribute. + * + * @return the component type. + */ + public String getComponentType() { + return componentType; + } + + /** + * Sets the component type. + * + * The component type is textual type of the component. This is included in + * the UIDL as component tag attribute. + * + * @param componentType + * the componentType to set. + */ + public void setComponentType(String componentType) { + this.componentType = componentType; + } + + @Override + public String getTag() { + return "customcomponent"; + } + + private class ComponentIterator implements Iterator, Serializable { + boolean first = getCompositionRoot() != null; + + public boolean hasNext() { + return first; + } + + public Object next() { + first = false; + return root; + } + + public void remove() { + throw new UnsupportedOperationException(); + } + } + + @SuppressWarnings("unchecked") + public Iterator getComponentIterator() { + return new ComponentIterator(); + } + + /** + * This method is not supported by CustomComponent. + * + * @see com.vaadin.ui.ComponentContainer#replaceComponent(com.vaadin.ui.Component, + * com.vaadin.ui.Component) + */ + 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/src/com/vaadin/ui/CustomLayout.java b/src/com/vaadin/ui/CustomLayout.java new file mode 100644 index 0000000000..6ec7986a29 --- /dev/null +++ b/src/com/vaadin/ui/CustomLayout.java @@ -0,0 +1,310 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +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 com.vaadin.terminal.PaintException; +import com.vaadin.terminal.PaintTarget; + +/** + * <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 Toolkit 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 IT Mill Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +@SuppressWarnings("serial") +public class CustomLayout extends AbstractLayout { + + private static final int BUFFER_SIZE = 10000; + + /** + * Custom layout slots containing the components. + */ + private final HashMap slots = new HashMap(); + + private String templateContents = null; + + private String templateName = null; + + /** + * 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 { + + InputStreamReader reader = new InputStreamReader(templateStream, + "UTF-8"); + StringBuffer b = new StringBuffer(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; + } + } + + templateContents = b.toString(); + setWidth(100, UNITS_PERCENTAGE); + } + + /** + * Constructor for custom layout with given template name. Template file is + * fetched from "<theme>/layout/<templateName>". + */ + public CustomLayout(String template) { + templateName = template; + setWidth(100, UNITS_PERCENTAGE); + } + + /** + * Gets the component UIDL tag. + * + * @return the Component UIDL tag as string. + */ + @Override + public String getTag() { + return "customlayout"; + } + + /** + * 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 = (Component) slots.get(location); + if (old != null) { + removeComponent(old); + } + slots.put(location, c); + 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); + c.setParent(null); + fireComponentDetachEvent(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((Component) 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. + */ + public Iterator getComponentIterator() { + return slots.values().iterator(); + } + + /** + * 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 (Component) slots.get(location); + } + + /** + * Paints the content of this component. + * + * @param target + * @throws PaintException + * if the paint operation failed. + */ + @Override + public void paintContent(PaintTarget target) throws PaintException { + super.paintContent(target); + + if (templateName != null) { + target.addAttribute("template", templateName); + } else { + target.addAttribute("templateContents", templateContents); + } + // Adds all items in all the locations + for (final Iterator i = slots.keySet().iterator(); i.hasNext();) { + // Gets the (location,component) + final String location = (String) i.next(); + final Component c = (Component) slots.get(location); + if (c != null) { + // Writes the item + target.startTag("location"); + target.addAttribute("name", location); + c.paint(target); + target.endTag("location"); + } + } + } + + /* Documented in superclass */ + public void replaceComponent(Component oldComponent, Component newComponent) { + + // Gets the locations + String oldLocation = null; + String newLocation = null; + for (final Iterator i = slots.keySet().iterator(); i.hasNext();) { + final String location = (String) i.next(); + final Component 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); + requestRepaint(); + } + } + + /** + * CustomLayout's template selecting was previously implemented with + * setStyle. Overriding to improve backwards compatibility. + * + * @param name + * template name + */ + @Override + public void setStyle(String name) { + setTemplateName(name); + } + + /** Get the name of the template */ + public String getTemplateName() { + return templateName; + } + + /** + * Set the name of the template used to draw custom layout. + * + * With GWT-adapter, the template with name 'templatename' is loaded from + * ITMILL/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) { + this.templateName = templateName; + templateContents = 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."); + } + +} diff --git a/src/com/vaadin/ui/DateField.java b/src/com/vaadin/ui/DateField.java new file mode 100644 index 0000000000..fc88187c06 --- /dev/null +++ b/src/com/vaadin/ui/DateField.java @@ -0,0 +1,527 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.Locale; +import java.util.Map; + +import com.vaadin.data.Property; +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.PaintTarget; + +/** + * <p> + * A date editor component that can be bound to any bindable 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. 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 IT Mill Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +@SuppressWarnings("serial") +public class DateField extends AbstractField { + + /* Private members */ + + /** + * Resolution identifier: milliseconds. + */ + public static final int RESOLUTION_MSEC = 0; + + /** + * Resolution identifier: seconds. + */ + public static final int RESOLUTION_SEC = 1; + + /** + * Resolution identifier: minutes. + */ + public static final int RESOLUTION_MIN = 2; + + /** + * Resolution identifier: hours. + */ + public static final int RESOLUTION_HOUR = 3; + + /** + * Resolution identifier: days. + */ + public static final int RESOLUTION_DAY = 4; + + /** + * Resolution identifier: months. + */ + public static final int RESOLUTION_MONTH = 5; + + /** + * Resolution identifier: years. + */ + public static final int RESOLUTION_YEAR = 6; + + /** + * Popup date selector (calendar). + */ + protected static final String TYPE_POPUP = "popup"; + + /** + * Inline date selector (calendar). + */ + protected static final String TYPE_INLINE = "inline"; + + /** + * Specified widget type. + */ + protected String type = TYPE_POPUP; + + /** + * Specified smallest modifiable unit. + */ + private int resolution = RESOLUTION_MSEC; + + /** + * Specified largest modifiable unit. + */ + private static final int largestModifiable = RESOLUTION_YEAR; + + /** + * The internal calendar to be used in java.utl.Date conversions. + */ + private Calendar calendar; + + /** + * Overridden format string + */ + private String dateFormat; + + /** + * Read-only content of an ITextualDate field - null for other types of date + * fields. + */ + private String dateString; + + /* 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 { + super.paintContent(target); + + // Adds the locale as attribute + final Locale l = getLocale(); + if (l != null) { + target.addAttribute("locale", l.toString()); + } + + if (getDateFormat() != null) { + target.addAttribute("format", dateFormat); + } + + target.addAttribute("type", type); + + // Gets the calendar + final Calendar calendar = getCalendar(); + final Date currentDate = (Date) getValue(); + + for (int r = resolution; r <= largestModifiable; r++) { + switch (r) { + case RESOLUTION_MSEC: + target.addVariable(this, "msec", currentDate != null ? calendar + .get(Calendar.MILLISECOND) : -1); + break; + case RESOLUTION_SEC: + target.addVariable(this, "sec", currentDate != null ? calendar + .get(Calendar.SECOND) : -1); + break; + case RESOLUTION_MIN: + target.addVariable(this, "min", currentDate != null ? calendar + .get(Calendar.MINUTE) : -1); + break; + case RESOLUTION_HOUR: + target.addVariable(this, "hour", currentDate != null ? calendar + .get(Calendar.HOUR_OF_DAY) : -1); + break; + case RESOLUTION_DAY: + target.addVariable(this, "day", currentDate != null ? calendar + .get(Calendar.DAY_OF_MONTH) : -1); + break; + case RESOLUTION_MONTH: + target.addVariable(this, "month", + currentDate != null ? calendar.get(Calendar.MONTH) + 1 + : -1); + break; + case RESOLUTION_YEAR: + target.addVariable(this, "year", currentDate != null ? calendar + .get(Calendar.YEAR) : -1); + break; + } + } + } + + /* + * Gets the components UIDL tag string. Don't add a JavaDoc comment here, we + * use the default documentation from implemented interface. + */ + @Override + public String getTag() { + return "datefield"; + } + + /* + * 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 variables) { + super.changeVariables(source, 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 = (Date) getValue(); + final String oldDateString = dateString; + Date newDate = null; + + // this enables analyzing invalid input on the server + Object o = variables.get("dateString"); + if (o != null) { + dateString = o.toString(); + } else { + dateString = null; + } + + // Gets the new date in parts + // Null values are converted to negative values. + int year = variables.containsKey("year") ? (variables.get("year") == null ? -1 + : ((Integer) variables.get("year")).intValue()) + : -1; + int month = variables.containsKey("month") ? (variables + .get("month") == null ? -1 : ((Integer) variables + .get("month")).intValue() - 1) : -1; + int day = variables.containsKey("day") ? (variables.get("day") == null ? -1 + : ((Integer) variables.get("day")).intValue()) + : -1; + int hour = variables.containsKey("hour") ? (variables.get("hour") == null ? -1 + : ((Integer) variables.get("hour")).intValue()) + : -1; + int min = variables.containsKey("min") ? (variables.get("min") == null ? -1 + : ((Integer) variables.get("min")).intValue()) + : -1; + int sec = variables.containsKey("sec") ? (variables.get("sec") == null ? -1 + : ((Integer) variables.get("sec")).intValue()) + : -1; + int msec = variables.containsKey("msec") ? (variables.get("msec") == null ? -1 + : ((Integer) variables.get("msec")).intValue()) + : -1; + + // If all of the components is < 0 use the previous value + if (year < 0 && month < 0 && day < 0 && hour < 0 && min < 0 + && sec < 0 && msec < 0) { + newDate = null; + } else { + + // Clone the calendar for date operation + final Calendar cal = getCalendar(); + + // Make sure that meaningful values exists + // Use the previous value if some of the variables + // have not been changed. + year = year < 0 ? cal.get(Calendar.YEAR) : year; + month = month < 0 ? cal.get(Calendar.MONTH) : month; + day = day < 0 ? cal.get(Calendar.DAY_OF_MONTH) : day; + hour = hour < 0 ? cal.get(Calendar.HOUR_OF_DAY) : hour; + min = min < 0 ? cal.get(Calendar.MINUTE) : min; + sec = sec < 0 ? cal.get(Calendar.SECOND) : sec; + msec = msec < 0 ? cal.get(Calendar.MILLISECOND) : msec; + + // Sets the calendar fields + cal.set(Calendar.YEAR, year); + cal.set(Calendar.MONTH, month); + cal.set(Calendar.DAY_OF_MONTH, day); + cal.set(Calendar.HOUR_OF_DAY, hour); + cal.set(Calendar.MINUTE, min); + cal.set(Calendar.SECOND, sec); + cal.set(Calendar.MILLISECOND, msec); + + // Assigns the date + newDate = cal.getTime(); + } + + if (newDate != oldDate + && (newDate == null || !newDate.equals(oldDate))) { + setValue(newDate, true); // Don't require a repaint, client + // updates itself + } else if (dateString != null && !"".equals(dateString) + && !dateString.equals(oldDateString)) { + setValue(handleUnparsableDateString(dateString)); + } + } + } + + /** + * This method is called to handle the date string from the client if the + * client could not parse it as a Date. + * + * By default, null is returned. If an exception is thrown, the current + * value will not be modified. + * + * This can be overridden to handle conversions or to throw an exception, or + * to fire an event. + * + * The default behavior is likely to change in the next major version of the + * toolkit - a Property.ConversionException will be thrown. + * + * @param dateString + * @return parsed Date + * @throws Property.ConversionException + * to keep the old value and indicate an error + */ + protected Date handleUnparsableDateString(String dateString) + throws Property.ConversionException { + // TODO in the next major version, this should throw an exception to be + // consistent with other fields + // throw new Property.ConversionException(); + return null; + } + + /* 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 getType() { + return Date.class; + } + + /* + * Returns the value of the property in human readable textual format. Don't + * add a JavaDoc comment here, we use the default documentation from + * implemented interface. + */ + @Override + public String toString() { + final Date value = (Date) getValue(); + if (value != null) { + return value.toString(); + } + return null; + } + + /* + * Sets the value of the property. Don't add a JavaDoc comment here, we use + * the default documentation from implemented interface. + */ + @Override + public void setValue(Object newValue) throws Property.ReadOnlyException, + Property.ConversionException { + setValue(newValue, false); + } + + @Override + public void setValue(Object newValue, boolean repaintIsNotNeeded) + throws Property.ReadOnlyException, Property.ConversionException { + + // Allows setting dates directly + if (newValue == null || newValue instanceof Date) { + super.setValue(newValue, repaintIsNotNeeded); + } else { + + // Try to parse as string + try { + final SimpleDateFormat parser = new SimpleDateFormat(); + final Date val = parser.parse(newValue.toString()); + super.setValue(val, repaintIsNotNeeded); + } catch (final ParseException e) { + throw new Property.ConversionException(e.getMessage()); + } + } + } + + /** + * Sets the DateField datasource. Datasource type must assignable to Date. + * + * @see com.vaadin.data.Property.Viewer#setPropertyDataSource(Property) + */ + @Override + public void setPropertyDataSource(Property newDataSource) { + if (newDataSource == null + || Date.class.isAssignableFrom(newDataSource.getType())) { + super.setPropertyDataSource(newDataSource); + } else { + throw new IllegalArgumentException( + "DateField only supports Date properties"); + } + } + + /** + * Gets the resolution. + * + * @return int + */ + public int getResolution() { + return resolution; + } + + /** + * Sets the resolution of the DateField. + * + * @param resolution + * the resolution to set. + */ + public void setResolution(int resolution) { + this.resolution = resolution; + } + + /** + * 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(); + } + + // Clone the instance + final Calendar newCal = (Calendar) calendar.clone(); + + // Assigns the current time tom calendar. + final Date currentDate = (Date) getValue(); + if (currentDate != null) { + newCal.setTime(currentDate); + } + + 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 Toolkit issue #2200. + * + * @param dateFormat + * the dateFormat to set + * + * @see com.vaadin.ui.AbstractComponent#setLocale(Locale)) + */ + public void setDateFormat(String dateFormat) { + this.dateFormat = dateFormat; + } + + /** + * Reterns 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; + } + +} diff --git a/src/com/vaadin/ui/Embedded.java b/src/com/vaadin/ui/Embedded.java new file mode 100644 index 0000000000..3b0acfee5e --- /dev/null +++ b/src/com/vaadin/ui/Embedded.java @@ -0,0 +1,425 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.util.Hashtable; +import java.util.Iterator; + +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.PaintTarget; +import com.vaadin.terminal.Resource; + +/** + * Component for embedding external objects. + * + * @author IT Mill Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +@SuppressWarnings("serial") +public class Embedded extends AbstractComponent { + + /** + * 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 parameteres. + */ + private final Hashtable parameters = new Hashtable(); + + /** + * Applet or other client side runnable properties. + */ + private String codebase = null; + + private String codetype = null; + + private String classId = null; + + private String archive = null; + + /** + * Creates a new empty Embedded object. + */ + public Embedded() { + } + + /** + * Creates a new empty Embedded object with caption. + * + * @param caption + */ + public Embedded(String caption) { + 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) { + setCaption(caption); + setSource(source); + } + + /** + * Gets the component UIDL tag. + * + * @return the Component UIDL tag as string. + */ + @Override + public String getTag() { + return "embedded"; + } + + /** + * 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); + } + + // Params + for (final Iterator i = getParameterNames(); i.hasNext();) { + target.startTag("embeddedparam"); + final String key = (String) i.next(); + target.addAttribute("name", key); + target.addAttribute("value", getParameter(key)); + target.endTag("embeddedparam"); + } + } + + /** + * 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 (String) 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 getParameterNames() { + return parameters.keySet().iterator(); + } + + /** + * Gets the codebase, the root-path used to access resources with relative + * paths. + * + * @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; + } + + /** + * Gets the standby text displayed when the object is loading. + * + * @return the standby text. + */ + public String getStandby() { + return standby; + } + + /** + * Sets the codebase, the root-path used to access resources with relative + * paths. + * + * @param codebase + * the codebase to set. + */ + public void setCodebase(String codebase) { + if (codebase != this.codebase + || (codebase != null && !codebase.equals(this.codebase))) { + this.codebase = codebase; + requestRepaint(); + } + } + + /** + * Sets the codetype, the MIME-Type of the code. + * + * @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; + requestRepaint(); + } + } + + /** + * Sets the standby, the text to display while loading the object. + * + * @param standby + * the standby to set. + */ + public void setStandby(String standby) { + if (standby != this.standby + || (standby != null && !standby.equals(this.standby))) { + this.standby = standby; + requestRepaint(); + } + } + + /** + * Gets the classId attribute. + * + * @return the class id. + */ + public String getClassId() { + return classId; + } + + /** + * Sets the classId attribute. + * + * @param classId + * the classId to set. + */ + public void setClassId(String classId) { + if (classId != this.classId + || (classId != null && !classId.equals(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 + * </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(); + } + } + + /** + * Gets the archive attribute. + * + * @return the archive attribute. + */ + public String getArchive() { + return archive; + } + + /** + * Sets the archive attribute. + * + * @param archive + * the archive string to set. + */ + public void setArchive(String archive) { + if (archive != this.archive + || (archive != null && !archive.equals(this.archive))) { + this.archive = archive; + requestRepaint(); + } + } + +} diff --git a/src/com/vaadin/ui/ExpandLayout.java b/src/com/vaadin/ui/ExpandLayout.java new file mode 100644 index 0000000000..7f988a47c2 --- /dev/null +++ b/src/com/vaadin/ui/ExpandLayout.java @@ -0,0 +1,101 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +/** + * A layout that will give one of it's components as much space as possible, + * while still showing the other components in the layout. The other components + * will in effect be given a fixed sized space, while the space given to the + * expanded component will grow/shrink to fill the rest of the space available - + * for instance when re-sizing the window. + * + * Note that this layout is 100% in both directions by default ({link + * {@link #setSizeFull()}). Remember to set the units if you want to specify a + * fixed size. If the layout fails to show up, check that the parent layout is + * actually giving some space. + * + * @deprecated Deprecated in favor of the new OrderedLayout + */ +@SuppressWarnings("serial") +@Deprecated +public class ExpandLayout extends OrderedLayout { + + private Component expanded = null; + + public ExpandLayout() { + this(ORIENTATION_VERTICAL); + } + + public ExpandLayout(int orientation) { + super(orientation); + + setSizeFull(); + } + + /** + * @param c + * Component which container will be maximized + */ + public void expand(Component c) { + if (expanded != null) { + try { + setExpandRatio(expanded, 0.0f); + } catch (IllegalArgumentException e) { + // Ignore error if component has been removed + } + } + + expanded = c; + if (expanded != null) { + setExpandRatio(expanded, 1.0f); + } + + requestRepaint(); + } + + @Override + public void addComponent(Component c, int index) { + super.addComponent(c, index); + if (expanded == null) { + expand(c); + } + } + + @Override + public void addComponent(Component c) { + super.addComponent(c); + if (expanded == null) { + expand(c); + } + } + + @Override + public void addComponentAsFirst(Component c) { + super.addComponentAsFirst(c); + if (expanded == null) { + expand(c); + } + } + + @Override + public void removeComponent(Component c) { + super.removeComponent(c); + if (c == expanded) { + if (getComponentIterator().hasNext()) { + expand((Component) getComponentIterator().next()); + } else { + expand(null); + } + } + } + + @Override + public void replaceComponent(Component oldComponent, Component newComponent) { + super.replaceComponent(oldComponent, newComponent); + if (oldComponent == expanded) { + expand(newComponent); + } + } +} diff --git a/src/com/vaadin/ui/Field.java b/src/com/vaadin/ui/Field.java new file mode 100644 index 0000000000..72dc3130c3 --- /dev/null +++ b/src/com/vaadin/ui/Field.java @@ -0,0 +1,105 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import com.vaadin.data.BufferedValidatable; +import com.vaadin.data.Property; +import com.vaadin.ui.Component.Focusable; + +/** + * @author IT Mill Ltd. + * + */ +public interface Field extends Component, BufferedValidatable, Property, + Property.ValueChangeNotifier, Property.ValueChangeListener, + Property.Editor, Focusable { + + /** + * Sets the Caption. + * + * @param caption + */ + void setCaption(String caption); + + String getDescription(); + + /** + * Sets the Description. + * + * @param caption + */ + void setDescription(String caption); + + /** + * 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 IT Mill Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ + @SuppressWarnings("serial") + public 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. + */ + public Property getProperty() { + return (Property) getSource(); + } + } +} diff --git a/src/com/vaadin/ui/FieldFactory.java b/src/com/vaadin/ui/FieldFactory.java new file mode 100644 index 0000000000..f0f96386e3 --- /dev/null +++ b/src/com/vaadin/ui/FieldFactory.java @@ -0,0 +1,76 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.io.Serializable; + +import com.vaadin.data.Container; +import com.vaadin.data.Item; +import com.vaadin.data.Property; + +/** + * Factory for creating new Field-instances based on type, datasource and/or + * context. + * + * @author IT Mill Ltd. + * @version + * @VERSION@ + * @since 3.1 + */ +public interface FieldFactory extends Serializable { + + /** + * Creates a field based on type of data. + * + * @param type + * the type of data presented in field. + * @param uiContext + * the component where the field is presented. + * @return Field the field suitable for editing the specified data. + * + */ + Field createField(Class type, Component uiContext); + + /** + * Creates a field based on the property datasource. + * + * @param property + * the property datasource. + * @param uiContext + * the component where the field is presented. + * @return Field the field suitable for editing the specified data. + */ + Field createField(Property property, Component uiContext); + + /** + * Creates a field based on the item and property id. + * + * @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. + * @return Field the field suitable for editing the specified data. + */ + Field createField(Item item, Object propertyId, Component uiContext); + + /** + * Creates a field based on the container item id and property id. + * + * @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 Field the field suitable for editing the specified data. + */ + Field createField(Container container, Object itemId, Object propertyId, + Component uiContext); + +}
\ No newline at end of file diff --git a/src/com/vaadin/ui/Form.java b/src/com/vaadin/ui/Form.java new file mode 100644 index 0000000000..3922028af0 --- /dev/null +++ b/src/com/vaadin/ui/Form.java @@ -0,0 +1,1179 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedList; + +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.util.BeanItem; +import com.vaadin.terminal.CompositeErrorMessage; +import com.vaadin.terminal.ErrorMessage; +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.PaintTarget; + +/** + * 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 IT Mill Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +@SuppressWarnings("serial") +public class Form extends AbstractField implements Item.Editor, Buffered, Item, + Validatable { + + private Object propertyValue; + + /** + * Layout of the form. + */ + private Layout layout; + + /** + * Item connected to this form as datasource. + */ + private Item itemDatasource; + + /** + * Ordered list of property ids in this editor. + */ + private final LinkedList propertyIds = new LinkedList(); + + /** + * 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 fields = new HashMap(); + + /** + * Field factory for this form. + */ + private FieldFactory fieldFactory; + + /** + * Visible item properties. + */ + private Collection visibleItemProperties; + + /** + * Form needs to repaint itself if child fields value changes due possible + * change in form validity. + */ + private final ValueChangeListener fieldValueChangeListener = new ValueChangeListener() { + public void valueChange( + com.vaadin.data.Property.ValueChangeEvent event) { + requestRepaint(); + } + }; + + private Layout formFooter; + + /** + * 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; + + /** + * Contructs a new form with default layout. + * + * <p> + * By default the form uses <code>OrderedLayout</code> with + * <code>form</code>-style. + * </p> + * + * @param formLayout + * the layout of the form. + */ + public Form() { + this(null); + setValidationVisible(false); + } + + /** + * Contructs a new form with given layout. + * + * @param formLayout + * the layout of the form. + */ + public Form(Layout formLayout) { + this(formLayout, new BaseFieldFactory()); + } + + /** + * Contructs a new form with given layout and FieldFactory. + * + * @param formLayout + * the layout of the form. + * @param fieldFactory + * the FieldFactory of the form. + */ + public Form(Layout formLayout, FieldFactory fieldFactory) { + super(); + setLayout(formLayout); + setFieldFactory(fieldFactory); + setValidationVisible(false); + setWidth(100, UNITS_PERCENTAGE); + } + + /* Documented in interface */ + @Override + public String getTag() { + return "form"; + } + + /* Documented in interface */ + @Override + public void paintContent(PaintTarget target) throws PaintException { + super.paintContent(target); + layout.paint(target); + if (formFooter != null) { + formFooter.paint(target); + } + } + + /** + * 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 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 Validator.InvalidValueException( + 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 Validator.InvalidValueException( + 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, + 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 { + + LinkedList 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 i = propertyIds.iterator(); i.hasNext();) { + try { + final Field f = ((Field) 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(); + } + 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 i = problems.iterator(); i.hasNext();) { + causes[index++] = (Throwable) 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 problems = null; + + // Try to discard all changes + for (final Iterator i = propertyIds.iterator(); i.hasNext();) { + try { + ((Field) fields.get(i.next())).discard(); + } catch (final Buffered.SourceException e) { + if (problems == null) { + problems = new LinkedList(); + } + 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 i = problems.iterator(); i.hasNext();) { + causes[index++] = (Throwable) 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 i = propertyIds.iterator(); i.hasNext();) { + final Field f = (Field) 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 + 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 + 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 i = propertyIds.iterator(); i.hasNext();) { + ((Field) 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 i = propertyIds.iterator(); i.hasNext();) { + ((Field) fields.get(i.next())).setWriteThrough(writeThrough); + } + } + } + + /** + * Adds a new property to form and create corresponding field. + * + * @see com.vaadin.data.Item#addItemProperty(Object, Property) + */ + 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; + } + + // Gets suitable field + final Field field = fieldFactory.createField(property, this); + if (field == null) { + return false; + } + + // Configures the field + try { + field.setPropertyDataSource(property); + String caption = id.toString(); + if (caption.length() > 50) { + caption = caption.substring(0, 47) + "..."; + } + if (caption.length() > 0) { + caption = "" + Character.toUpperCase(caption.charAt(0)) + + caption.substring(1, caption.length()); + } + field.setCaption(caption); + } catch (final Throwable ignored) { + return false; + } + + addField(id, field); + + return true; + } + + /** + * Adds the field to form. + * + * <p> + * The property id must not be already used in the form. + * </p> + * + * <p> + * This field is added to the form layout in the default position (the + * position used by {@link Layout#addComponent(Component)} method. In the + * special case that the underlying layout is a custom layout, string + * representation of the property id is used instead of the default + * location. + * </p> + * + * @param propertyId + * the Property id the the field. + * @param field + * the New field added to the form. + */ + public void addField(Object propertyId, Field field) { + + if (propertyId != null && field != null) { + fields.put(propertyId, field); + field.addListener(fieldValueChangeListener); + propertyIds.addLast(propertyId); + field.setReadThrough(readThrough); + field.setWriteThrough(writeThrough); + if (isImmediate() && field instanceof AbstractComponent) { + ((AbstractComponent) field).setImmediate(true); + } + if (layout instanceof CustomLayout) { + ((CustomLayout) layout).addComponent(field, propertyId + .toString()); + } else { + layout.addComponent(field); + } + + requestRepaint(); + } + } + + /** + * 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) + */ + public Property getItemProperty(Object id) { + final Field field = (Field) fields.get(id); + if (field == null) { + return null; + } + 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 (Field) fields.get(propertyId); + } + + /* Documented in interface */ + public Collection getItemPropertyIds() { + return Collections.unmodifiableCollection(propertyIds); + } + + /** + * Removes the property and corresponding field from the form. + * + * @see com.vaadin.data.Item#removeItemProperty(Object) + */ + public boolean removeItemProperty(Object id) { + + final Field field = (Field) fields.get(id); + + if (field != null) { + propertyIds.remove(id); + fields.remove(id); + layout.removeComponent(field); + field.removeListener(fieldValueChangeListener); + return true; + } + + return false; + } + + /** + * 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 */ + 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) + */ + 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 (layout instanceof GridLayout) { + GridLayout gl = (GridLayout) layout; + 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) { + 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) { + f.setPropertyDataSource(property); + addField(id, f); + } + } + } + } + + /** + * 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; + } + + /** + * Sets the layout of the form. + * + * <p> + * By default form uses <code>OrderedLayout</code> with <code>form</code> + * -style. + * </p> + * + * @param newLayout + * the Layout of the form. + */ + public void setLayout(Layout newLayout) { + + // Use orderedlayout by default + if (newLayout == null) { + newLayout = new FormLayout(); + } + + // reset cursor memory + gridlayoutCursorX = -1; + gridlayoutCursorY = -1; + + // Move fields from previous layout + if (layout != null) { + final Object[] properties = propertyIds.toArray(); + for (int i = 0; i < properties.length; i++) { + Field f = getField(properties[i]); + layout.removeComponent(f); + if (newLayout instanceof CustomLayout) { + ((CustomLayout) newLayout).addComponent(f, properties[i] + .toString()); + } else { + newLayout.addComponent(f); + } + } + + layout.setParent(null); + } + + // Replace the previous layout + newLayout.setParent(this); + layout = newLayout; + + } + + /** + * 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> + * + * @param propertyId + * the id of the property. + * @param values + * @param descriptions + * @return the select property generated + */ + public Select 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 = (Field) 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 Select newField = new Select(); + if (isMultiselect) { + newField.setMultiSelect(true); + } + 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]; + if (id == null) { + id = new Object(); + newField.setNullSelectionItemId(id); + } + final Item 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 + layout.replaceComponent(oldField, newField); + fields.put(propertyId, newField); + newField.addListener(fieldValueChangeListener); + oldField.removeListener(fieldValueChangeListener); + + return newField; + } + + /** + * Notifies the component that it is connected to an application + * + * @see com.vaadin.ui.Component#attach() + */ + @Override + public void attach() { + super.attach(); + layout.attach(); + } + + /** + * Notifies the component that it is detached from the application. + * + * @see com.vaadin.ui.Component#detach() + */ + @Override + public void detach() { + super.detach(); + layout.detach(); + } + + /** + * Tests the current value of the object against all registered validators + * + * @see com.vaadin.data.Validatable#isValid() + */ + @Override + public boolean isValid() { + boolean valid = true; + for (final Iterator i = propertyIds.iterator(); i.hasNext();) { + valid &= ((Field) fields.get(i.next())).isValid(); + } + return valid && super.isValid(); + } + + /** + * Checks the validity of the validatable. + * + * @see com.vaadin.data.Validatable#validate() + */ + @Override + public void validate() throws InvalidValueException { + super.validate(); + for (final Iterator i = propertyIds.iterator(); i.hasNext();) { + ((Field) 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();) { + ((Field) fields.get(i.next())).setReadOnly(readOnly); + } + } + + /** + * Sets the field factory of Form. + * + * <code>FieldFactory</code> is used to create fields for form properties. + * By default the form uses BaseFieldFactory to create Field instances. + * + * @param fieldFactory + * the New factory used to create the fields. + * @see Field + * @see FieldFactory + */ + public void setFieldFactory(FieldFactory fieldFactory) { + this.fieldFactory = fieldFactory; + } + + /** + * Get the field factory of the form. + * + * @return the FieldFactory Factory used to create the fields. + */ + public FieldFactory getFieldFactory() { + 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 field in form. + * + * @return the Field. + */ + private Field getFirstField() { + Object id = null; + if (getItemPropertyIds() != null) { + 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(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 v = new LinkedList(); + 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 = getFirstField(); + 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 i = fields.values().iterator(); i.hasNext();) { + Field f = (Field) 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 i = fields.values().iterator(); i.hasNext();) { + Field f = (Field) 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() { + if (formFooter == null) { + formFooter = new OrderedLayout(OrderedLayout.ORIENTATION_HORIZONTAL); + formFooter.setParent(this); + } + return formFooter; + } + + /** + * Sets the layout that is rendered below normal form contens. + * + * @param newFormFooter + * the new Layout + */ + public void setFooter(Layout newFormFooter) { + if (formFooter != null) { + formFooter.setParent(null); + } + formFooter = newFormFooter; + formFooter.setParent(this); + } + + @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(); + } + } + +} diff --git a/src/com/vaadin/ui/FormLayout.java b/src/com/vaadin/ui/FormLayout.java new file mode 100644 index 0000000000..1fbfcf2ee4 --- /dev/null +++ b/src/com/vaadin/ui/FormLayout.java @@ -0,0 +1,36 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +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. + * + */ +@SuppressWarnings( { "deprecation", "serial" }) +public class FormLayout extends OrderedLayout { + + public FormLayout() { + super(); + setSpacing(true); + setMargin(true, false, true, false); + } + + @Override + public String getTag() { + return "formlayout"; + } + +} diff --git a/src/com/vaadin/ui/GridLayout.java b/src/com/vaadin/ui/GridLayout.java new file mode 100644 index 0000000000..fbbc0e3ffa --- /dev/null +++ b/src/com/vaadin/ui/GridLayout.java @@ -0,0 +1,1313 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +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.terminal.PaintException; +import com.vaadin.terminal.PaintTarget; + +/** + * <p> + * A container that consists of components with certain coordinates (cell + * position) on a grid. It also maintains cursor for adding component in left to + * right, top to bottom order. + * </p> + * + * <p> + * Each component in a <code>GridLayout</code> uses a certain + * {@link GridLayout.Area area} (column1,row1,column2,row2) from the grid. One + * should not add components that would overlap with the existing components + * because in such case an {@link OverlapsException} is thrown. Adding component + * with cursor automatically extends the grid by increasing the grid height. + * </p> + * + * @author IT Mill Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +@SuppressWarnings("serial") +public class GridLayout extends AbstractLayout implements + Layout.AlignmentHandler, Layout.SpacingHandler { + + /** + * Initial grid columns. + */ + private int cols = 0; + + /** + * Initial grid rows. + */ + private int rows = 0; + + /** + * 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 areas = new LinkedList(); + + /** + * Mapping from components to their respective areas. + */ + private final LinkedList components = new LinkedList(); + + /** + * Mapping from components to alignments (horizontal + vertical). + */ + private Map<Component, Alignment> componentToAlignment = new HashMap<Component, Alignment>(); + + /** + * Is spacing between contained components enabled. Defaults to false. + */ + private boolean spacing = false; + + 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 grid of given size (number of cells). Note that grid's + * final size depends on the items that are added into the grid. Grid grows + * if you add components outside the grid's 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); + } + + /** + * Constructs an empty grid layout that is extended as needed. + */ + public GridLayout() { + this(1, 1); + } + + /** + * <p> + * Adds a component with a specified area to the grid. The area the new + * component should take is defined by specifying the upper left corner + * (column1, row1) and the lower right corner (column2, row2) of the area. + * </p> + * + * <p> + * If the new component overlaps with any of the existing components already + * present in the grid the operation will fail and an + * {@link OverlapsException} is thrown. + * </p> + * + * @param c + * the component to be added. + * @param column1 + * the column of the upper left corner of the area <code>c</code> + * is supposed to occupy. + * @param row1 + * the row of the upper left corner of the area <code>c</code> is + * supposed to occupy. + * @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 >= cols || row2 >= rows) { + throw new OutOfBoundsException(area); + } + + // Checks that newItem does not overlap with existing items + checkExistingOverlaps(area); + + // first attemt to add to super + super.addComponent(component); + + // Inserts the component to right place at the list + // Respect top-down, left-right ordering + // component.setParent(this); + final Iterator i = areas.iterator(); + int index = 0; + boolean done = false; + while (!done && i.hasNext()) { + final Area existingArea = (Area) 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); + } + + // 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 >= cols) { + // 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 i = areas.iterator(); i.hasNext();) { + final Area existingArea = (Area) i.next(); + if (existingArea.overlaps(area)) { + // Component not added, overlaps with existing component + throw new OverlapsException(existingArea); + } + } + } + + /** + * Adds the component into this container to cells column1,row1 (NortWest + * corner of the area.) End coordinates (SouthEast corner of the area) are + * the same as column1,row1. Component width and height is 1. + * + * @param c + * the component to be added. + * @param column + * the column index. + * @param row + * the row index. + * @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 c, int column, int row) + throws OverlapsException, OutOfBoundsException { + this.addComponent(c, column, row, column, row); + } + + /** + * Force the next component to be added to the beginning of the next line. + * By calling this function user can ensure that no more components are + * added to the right of the previous component. + * + * @see #space() + */ + public void newLine() { + cursorX = 0; + cursorY++; + } + + /** + * Moves the cursor forwards by one. If the cursor goes out of the right + * grid border, move it to next line. + * + * @see #newLine() + */ + public void space() { + cursorX++; + if (cursorX >= cols) { + 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 c + * 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 + cols = cursorX >= cols ? cursorX + 1 : cols; + rows = cursorY >= rows ? cursorY + 1 : rows; + + addComponent(component, cursorX, cursorY); + } + + /** + * Removes the given component from this container. + * + * @param c + * 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; + } + + super.removeComponent(component); + + Area area = null; + for (final Iterator i = areas.iterator(); area == null && i.hasNext();) { + final Area a = (Area) i.next(); + if (a.getComponent() == component) { + area = a; + } + } + + components.remove(component); + if (area != null) { + areas.remove(area); + } + + componentToAlignment.remove(component); + + requestRepaint(); + } + + /** + * Removes the component specified with it's cell index. + * + * @param column + * the Component's column. + * @param row + * the Component's row. + */ + public void removeComponent(int column, int row) { + + // Finds the area + for (final Iterator i = areas.iterator(); i.hasNext();) { + final Area area = (Area) i.next(); + if (area.getColumn1() == column && area.getRow1() == row) { + removeComponent(area.getComponent()); + return; + } + } + } + + /** + * Gets an Iterator to the component container contents. Using the Iterator + * it's possible to step through the contents of the container. + * + * @return the Iterator of the components inside the container. + */ + public Iterator getComponentIterator() { + return Collections.unmodifiableCollection(components).iterator(); + } + + /** + * 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 { + + super.paintContent(target); + + // TODO refactor attribute names in future release. + target.addAttribute("h", rows); + target.addAttribute("w", cols); + + target.addAttribute("structuralChange", structuralChange); + structuralChange = false; + + if (spacing) { + target.addAttribute("spacing", spacing); + } + + // Area iterator + final Iterator 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 cellUsed = new HashMap(); + + // Empty cell collector + int emptyCells = 0; + + final String[] alignmentsArray = new String[components.size()]; + final Integer[] columnExpandRatioArray = new Integer[cols]; + final Integer[] rowExpandRatioArray = new Integer[rows]; + + 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) cols; + int myRatio = Math.round(equalSize * 1000); + for (int i = 0; i < cols; i++) { + columnExpandRatioArray[i] = myRatio; + } + realColExpandRatioSum = myRatio * cols; + } else { + for (int i = 0; i < cols; 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) rows; + int myRatio = Math.round(equalSize * 1000); + for (int i = 0; i < rows; i++) { + rowExpandRatioArray[i] = myRatio; + } + realRowExpandRatioSum = myRatio * rows; + } + + int index = 0; + + // Iterates every applicable row + for (int cury = 0; cury < rows; 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 < cols; 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); + } + area.getComponent().paint(target); + + alignmentsArray[index++] = String + .valueOf(getComponentAlignment(area.getComponent()) + .getBitMask()); + + target.endTag("gc"); + + // Fetch next item + if (areaiterator.hasNext()) { + area = (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 = ((Integer) 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(new Integer(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", cols - 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 + * .itmill.toolkit.ui.Component) + */ + public Alignment getComponentAlignment(Component childComponent) { + Alignment alignment = componentToAlignment.get(childComponent); + if (alignment == null) { + return ALIGNMENT_DEFAULT; + } else { + return alignment; + } + } + + /** + * Gets the components UIDL tag. + * + * @return the Component UIDL tag as string. + * @see com.vaadin.ui.AbstractComponent#getTag() + */ + @Override + public String getTag() { + return "gridlayout"; + } + + /** + * This class defines an area on a grid. An Area is defined by the cells of + * its upper left corner (column1,row1) and lower right corner + * (column2,row2). + * + * @author IT Mill 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 on 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 + * <code>c</code> is supposed to occupy. + * @param row1 + * The row of the upper left corner cell of the area + * <code>c</code> is supposed to occupy. + * @param column2 + * The column of the lower right corner cell of the area + * <code>c</code> is supposed to occupy. + * @param row2 + * The row of the lower right corner cell 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 + */ + 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 the given Area overlaps with an another Area. + * + * @param other + * the Another Area that's to be tested for overlap with this + * area. + * @return <code>true</code> if <code>other</code> overlaps with this + * area, <code>false</code> if it doesn't. + */ + 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 datastructure 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 getColumn1() instead. + * + * @see com.vaadin.ui.GridLayout#getColumn1() + */ + @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 getColumn2() instead. + * + * @see com.vaadin.ui.GridLayout#getColumn2() + */ + @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 getRow1() instead. + * + * @see com.vaadin.ui.GridLayout#getRow1() + */ + @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 getRow2() instead. + * + * @see com.vaadin.ui.GridLayout#getRow2() + */ + @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; + } + + } + + /** + * An <code>Exception</code> object which is thrown when two Items occupy + * the same space on a grid. + * + * @author IT Mill 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; + } + + /** + * 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 IT Mill 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 (cols == columns) { + return; + } + + // Checks for overlaps + if (cols > columns) { + for (final Iterator i = areas.iterator(); i.hasNext();) { + final Area area = (Area) i.next(); + if (area.column2 >= columns) { + throw new OutOfBoundsException(area); + } + } + } + + cols = columns; + + requestRepaint(); + } + + /** + * Get the number of columns in the grid. + * + * @return the number of columns in the grid. + */ + public final int getColumns() { + return cols; + } + + /** + * 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 (this.rows == rows) { + return; + } + + // Checks for overlaps + if (this.rows > rows) { + for (final Iterator i = areas.iterator(); i.hasNext();) { + final Area area = (Area) i.next(); + if (area.row2 >= rows) { + throw new OutOfBoundsException(area); + } + } + } + + this.rows = rows; + + requestRepaint(); + } + + /** + * Get the number of rows in the grid. + * + * @return the number of rows in the grid. + */ + public final int getRows() { + return rows; + } + + /** + * Gets the current cursor x-position. 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. + * + * @return the grid column the Cursor is on. + */ + 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 cursor y-position. 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. + * + * @return the grid row the Cursor is on. + */ + public int getCursorY() { + return cursorY; + } + + /** + * Sets the current cursor y-position. This is usually handled automatically + * by GridLayout. + * + * @param cursorY + */ + public void setCursorY(int cursorY) { + this.cursorY = cursorY; + } + + /* Documented in superclass */ + public void replaceComponent(Component oldComponent, Component newComponent) { + + // Gets the locations + Area oldLocation = null; + Area newLocation = null; + for (final Iterator i = areas.iterator(); i.hasNext();) { + final Area location = (Area) 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(); + cursorX = 0; + cursorY = 0; + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.ui.Layout.AlignmentHandler#setComponentAlignment(com + * .itmill.toolkit.ui.Component, int, int) + */ + public void setComponentAlignment(Component childComponent, + int horizontalAlignment, int verticalAlignment) { + componentToAlignment.put(childComponent, new Alignment( + horizontalAlignment + verticalAlignment)); + requestRepaint(); + } + + public void setComponentAlignment(Component childComponent, + Alignment alignment) { + componentToAlignment.put(childComponent, alignment); + requestRepaint(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.Layout.SpacingHandler#setSpacing(boolean) + */ + public void setSpacing(boolean enabled) { + spacing = enabled; + requestRepaint(); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.Layout.SpacingHandler#isSpacing() + */ + @Deprecated + public boolean isSpacingEnabled() { + return spacing; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.Layout.SpacingHandler#isSpacing() + */ + public boolean isSpacing() { + return spacing; + } + + /** + * Inserts an empty row at the chosen position in the grid. + * + * @param row + * Number of the row the new row will be inserted before + */ + public void insertRow(int row) { + if (row > rows) { + throw new IllegalArgumentException("Cannot insert row at " + row + + " in a gridlayout with height " + rows); + } + + for (Iterator i = areas.iterator(); i.hasNext();) { + Area existingArea = (Area) 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(rows + 1); + structuralChange = true; + requestRepaint(); + } + + /** + * Removes row and all components in the row. Components which span over + * several rows are removed if the selected row is the component's first + * row. + * + * @param row + * The row number to remove + */ + public void removeRow(int row) { + if (row >= rows) { + throw new IllegalArgumentException("Cannot delete row " + row + + " from a gridlayout with height " + rows); + } + + // 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 i = areas.iterator(); i.hasNext();) { + Area existingArea = (Area) i.next(); + if (existingArea.row2 >= row) { + existingArea.row2--; + + if (existingArea.row1 > row) { + existingArea.row1--; + } + } + } + + setRows(rows - 1); + if (cursorY > row) { + cursorY--; + } + + structuralChange = true; + requestRepaint(); + + } + + /** + * Sets the expand ratio of given column. Expand ratio defines how excess + * space is distributed among columns. Excess space means the space not + * consumed by non relatively sized components. + * + * <p> + * By default excess space is distributed evenly. + * + * <p> + * Note, that width needs to be defined for this method to have any effect. + * + * @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. Expand ratio defines how excess space + * is distributed among rows. Excess space means the space not consumed by + * non relatively sized components. + * + * <p> + * By default excess space is distributed evenly. + * + * <p> + * Note, that height needs to be defined for this method to have any effect. + * + * @see #setHeight(float, int) + * + * @param rowIndex + * @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 + * @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 + * x-index + * @param y + * y-index + * @return Component in given cell or null if empty + */ + public Component getComponent(int x, int y) { + for (final Iterator iterator = areas.iterator(); iterator.hasNext();) { + final Area 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 layed in the + * GridLayout. + * + * @param component + * the component whose area information is requested. + * @return an Area object that contains information how component is layed + * in the grid + */ + public Area getComponentArea(Component component) { + for (final Iterator iterator = areas.iterator(); iterator.hasNext();) { + final Area area = (Area) iterator.next(); + if (area.getComponent() == component) { + return area; + } + } + return null; + } + + public void setComponentAlignment(Component component, String alignment) { + AlignmentUtils.setComponentAlignment(this, component, alignment); + } + +} diff --git a/src/com/vaadin/ui/HorizontalLayout.java b/src/com/vaadin/ui/HorizontalLayout.java new file mode 100644 index 0000000000..820b0c29de --- /dev/null +++ b/src/com/vaadin/ui/HorizontalLayout.java @@ -0,0 +1,26 @@ +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 IT Mill Ltd. + * @version + * @VERSION@ + * @since 5.3 + */ +@SuppressWarnings("serial") +public class HorizontalLayout extends AbstractOrderedLayout { + + public HorizontalLayout() { + + } + + @Override + public String getTag() { + return "horizontallayout"; + } + +} diff --git a/src/com/vaadin/ui/InlineDateField.java b/src/com/vaadin/ui/InlineDateField.java new file mode 100644 index 0000000000..88f0c3e0b4 --- /dev/null +++ b/src/com/vaadin/ui/InlineDateField.java @@ -0,0 +1,52 @@ +/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+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 IT Mill Ltd.
+ * @version
+ * @VERSION@
+ * @since 5.0
+ */
+@SuppressWarnings("serial")
+public class InlineDateField extends DateField {
+
+ public InlineDateField() {
+ super();
+ type = TYPE_INLINE;
+ }
+
+ public InlineDateField(Property dataSource) throws IllegalArgumentException {
+ super(dataSource);
+ type = TYPE_INLINE;
+ }
+
+ public InlineDateField(String caption, Date value) {
+ super(caption, value);
+ type = TYPE_INLINE;
+ }
+
+ public InlineDateField(String caption, Property dataSource) {
+ super(caption, dataSource);
+ type = TYPE_INLINE;
+ }
+
+ public InlineDateField(String caption) {
+ super(caption);
+ type = TYPE_INLINE;
+ }
+
+}
diff --git a/src/com/vaadin/ui/Label.java b/src/com/vaadin/ui/Label.java new file mode 100644 index 0000000000..3b24a07930 --- /dev/null +++ b/src/com/vaadin/ui/Label.java @@ -0,0 +1,546 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.lang.reflect.Method; + +import com.vaadin.data.Property; +import com.vaadin.data.util.ObjectProperty; +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.PaintTarget; + +/** + * Label component for showing non-editable short texts. + * + * The label content can be set to the modes specified by the final members + * CONTENT_* + * + * <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 IT Mill Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +@SuppressWarnings("serial") +public class Label extends AbstractComponent implements Property, + Property.Viewer, Property.ValueChangeListener, + Property.ValueChangeNotifier, Comparable { + + /** + * 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; + + /** + * Formatted content mode, where the contents is XML restricted to the UIDL + * 1.0 formatting markups. + * + * @deprecated Use CONTENT_XML instead. + */ + @Deprecated + public static final int CONTENT_UIDL = 2; + + /** + * Content mode, where the label contains XHTML. Contents is then enclosed + * in DIV elements having namespace of + * "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd". + */ + public static final int CONTENT_XHTML = 3; + + /** + * Content mode, where the label contains well-formed or well-balanced XML. + * Each of the root elements must have their default namespace specified. + */ + public static final int CONTENT_XML = 4; + + /** + * Content mode, where the label contains RAW output. Output is not required + * to comply to with XML. In Web Adapter output is inserted inside the + * resulting HTML document as-is. This is useful for some specific purposes + * where possibly broken HTML content needs to be shown, but in most cases + * XHTML mode should be preferred. + */ + public static final int CONTENT_RAW = 5; + + /** + * The default content mode is plain text. + */ + public static final int CONTENT_DEFAULT = CONTENT_TEXT; + + /** Array of content mode names that are rendered in UIDL as mode attribute. */ + private static final String[] CONTENT_MODE_NAME = { "text", "pre", "uidl", + "xhtml", "xml", "raw" }; + + private static final String DATASOURCE_MUST_BE_SET = "Datasource must be set"; + + private Property dataSource; + + private int contentMode = CONTENT_DEFAULT; + + /** + * Creates an empty Label. + */ + public Label() { + this(""); + } + + /** + * Creates a new instance of Label with text-contents. + * + * @param content + */ + public Label(String content) { + this(content, CONTENT_DEFAULT); + } + + /** + * Creates a new instance of Label with text-contents read from given + * datasource. + * + * @param contentSource + */ + public Label(Property contentSource) { + this(contentSource, CONTENT_DEFAULT); + } + + /** + * Creates a new instance of Label with text-contents. + * + * @param content + * @param contentMode + */ + public Label(String content, int contentMode) { + this(new ObjectProperty(content, String.class), contentMode); + } + + /** + * Creates a new instance of Label with text-contents read from given + * datasource. + * + * @param contentSource + * @param contentMode + */ + public Label(Property contentSource, int contentMode) { + setPropertyDataSource(contentSource); + if (contentMode != CONTENT_DEFAULT) { + setContentMode(contentMode); + } + setWidth(100, UNITS_PERCENTAGE); + } + + /** + * Get the component UIDL tag. + * + * @return the Component UIDL tag as string. + */ + @Override + public String getTag() { + return "label"; + } + + /** + * Set the component to read-only. Readonly is not used in label. + * + * @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_SET); + } + dataSource.setReadOnly(readOnly); + } + + /** + * Is the component read-only ? Readonly is not used in label - this returns + * allways false. + * + * @return <code>true</code> if the component is in read only mode. + */ + @Override + public boolean isReadOnly() { + if (dataSource == null) { + throw new IllegalStateException(DATASOURCE_MUST_BE_SET); + } + 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 { + if (contentMode != CONTENT_TEXT) { + target.addAttribute("mode", CONTENT_MODE_NAME[contentMode]); + } + if (contentMode == CONTENT_TEXT) { + target.addText(toString()); + } else if (contentMode == CONTENT_UIDL) { + target.addUIDL(toString()); + } else if (contentMode == CONTENT_XHTML) { + target.startTag("data"); + target.addXMLSection("div", toString(), + "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"); + target.endTag("data"); + } else if (contentMode == CONTENT_PREFORMATTED) { + target.startTag("pre"); + target.addText(toString()); + target.endTag("pre"); + } else if (contentMode == CONTENT_XML) { + target.addXMLSection("data", toString(), null); + } else if (contentMode == CONTENT_RAW) { + target.startTag("data"); + target.addAttribute("escape", false); + target.addText(toString()); + target.endTag("data"); + } + + } + + /** + * Gets the value of the label. Value of the label is the XML contents of + * the label. + * + * @return the Value of the label. + */ + public Object getValue() { + if (dataSource == null) { + throw new IllegalStateException(DATASOURCE_MUST_BE_SET); + } + return dataSource.getValue(); + } + + /** + * Set the value of the label. Value of the label is the XML contents of the + * label. + * + * @param newValue + * the New value of the label. + */ + public void setValue(Object newValue) { + if (dataSource == null) { + throw new IllegalStateException(DATASOURCE_MUST_BE_SET); + } + dataSource.setValue(newValue); + } + + /** + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + if (dataSource == null) { + throw new IllegalStateException(DATASOURCE_MUST_BE_SET); + } + return dataSource.toString(); + } + + /** + * Gets the type of the Property. + * + * @see com.vaadin.data.Property#getType() + */ + public Class getType() { + if (dataSource == null) { + throw new IllegalStateException(DATASOURCE_MUST_BE_SET); + } + return dataSource.getType(); + } + + /** + * Gets the viewing data-source property. + * + * @return the data source property. + * @see com.vaadin.data.Property.Viewer#getPropertyDataSource() + */ + 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) + */ + 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 content mode of the Label. + * + * <p> + * Possible content modes include: + * <ul> + * <li><b>CONTENT_TEXT</b> Content mode, where the label contains only plain + * text. The getValue() result is coded to XML when painting.</li> + * <li><b>CONTENT_PREFORMATTED</b> Content mode, where the label contains + * preformatted text.</li> + * <li><b>CONTENT_UIDL</b> Formatted content mode, where the contents is XML + * restricted to the UIDL 1.0 formatting markups.</li> + * <li><b>CONTENT_XHTML</b> Content mode, where the label contains XHTML. + * Contents is then enclosed in DIV elements having namespace of + * "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd".</li> + * <li><b>CONTENT_XML</b> Content mode, where the label contains well-formed + * or well-balanced XML. Each of the root elements must have their default + * namespace specified.</li> + * <li><b>CONTENT_RAW</b> Content mode, where the label contains RAW output. + * Output is not required to comply to with XML. In Web Adapter output is + * inserted inside the resulting HTML document as-is. This is useful for + * some specific purposes where possibly broken HTML content needs to be + * shown, but in most cases XHTML mode should be preferred.</li> + * </ul> + * </p> + * + * @return the Content mode of the label. + */ + public int getContentMode() { + return contentMode; + } + + /** + * Sets the content mode of the Label. + * + * <p> + * Possible content modes include: + * <ul> + * <li><b>CONTENT_TEXT</b> Content mode, where the label contains only plain + * text. The getValue() result is coded to XML when painting.</li> + * <li><b>CONTENT_PREFORMATTED</b> Content mode, where the label contains + * preformatted text.</li> + * <li><b>CONTENT_UIDL</b> Formatted content mode, where the contents is XML + * restricted to the UIDL 1.0 formatting markups.</li> + * <li><b>CONTENT_XHTML</b> Content mode, where the label contains XHTML. + * Contents is then enclosed in DIV elements having namespace of + * "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd".</li> + * <li><b>CONTENT_XML</b> Content mode, where the label contains well-formed + * or well-balanced XML. Each of the root elements must have their default + * namespace specified.</li> + * <li><b>CONTENT_RAW</b> Content mode, where the label contains RAW output. + * Output is not required to comply to with XML. In Web Adapter output is + * inserted inside the resulting HTML document as-is. This is useful for + * some specific purposes where possibly broken HTML content needs to be + * shown, but in most cases XHTML mode should be preferred.</li> + * </ul> + * </p> + * + * @param contentMode + * the New content mode of the label. + */ + public void setContentMode(int contentMode) { + if (contentMode != this.contentMode && contentMode >= CONTENT_TEXT + && contentMode <= CONTENT_RAW) { + this.contentMode = 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 IT Mill Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ + public 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() + */ + 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) + */ + 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) + */ + 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)); + requestRepaint(); + } + + /** + * Listens the value change events from data source. + * + * @see com.vaadin.data.Property.ValueChangeListener#valueChange(Property.ValueChangeEvent) + */ + public void valueChange(Property.ValueChangeEvent event) { + fireValueChange(); + } + + /** + * 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) + */ + public int compareTo(Object other) { + + String thisValue; + String otherValue; + + if (contentMode == CONTENT_XML || contentMode == CONTENT_UIDL + || contentMode == CONTENT_XHTML) { + thisValue = stripTags(toString()); + } else { + thisValue = toString(); + } + + if (other instanceof Label + && (((Label) other).getContentMode() == CONTENT_XML + || ((Label) other).getContentMode() == CONTENT_UIDL || ((Label) other) + .getContentMode() == CONTENT_XHTML)) { + otherValue = stripTags(other.toString()); + } else { + otherValue = other.toString(); + } + + 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(); + } + +} diff --git a/src/com/vaadin/ui/Layout.java b/src/com/vaadin/ui/Layout.java new file mode 100644 index 0000000000..935ae363b5 --- /dev/null +++ b/src/com/vaadin/ui/Layout.java @@ -0,0 +1,238 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.io.Serializable; + +import com.vaadin.terminal.gwt.client.ui.IMarginInfo; +import com.vaadin.terminal.gwt.client.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 IT Mill 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 + * @deprecated Use {@link #isSpacing()} instead. + */ + @Deprecated + public boolean isSpacingEnabled(); + + /** + * + * @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 IMarginInfo 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/src/com/vaadin/ui/Link.java b/src/com/vaadin/ui/Link.java new file mode 100644 index 0000000000..6deb05a99a --- /dev/null +++ b/src/com/vaadin/ui/Link.java @@ -0,0 +1,243 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.PaintTarget; +import com.vaadin.terminal.Resource; + +/** + * Link is used to create external or internal URL links. + * + * @author IT Mill Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +@SuppressWarnings("serial") +public class Link extends AbstractComponent { + + /* Target window border type constant: No window border */ + public static final int TARGET_BORDER_NONE = Window.BORDER_NONE; + + /* Target window border type constant: Minimal window border */ + public static final int TARGET_BORDER_MINIMAL = Window.BORDER_MINIMAL; + + /* Target window border type constant: Default window border */ + public static final int TARGET_BORDER_DEFAULT = Window.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); + } + + /** + * Gets the component UIDL tag. + * + * @return the Component UIDL tag as string. + */ + @Override + public String getTag() { + return "link"; + } + + /** + * 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(); + } +} diff --git a/src/com/vaadin/ui/ListSelect.java b/src/com/vaadin/ui/ListSelect.java new file mode 100644 index 0000000000..7860474433 --- /dev/null +++ b/src/com/vaadin/ui/ListSelect.java @@ -0,0 +1,96 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +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/src/com/vaadin/ui/LoginForm.java b/src/com/vaadin/ui/LoginForm.java new file mode 100644 index 0000000000..3d3aeaf8b7 --- /dev/null +++ b/src/com/vaadin/ui/LoginForm.java @@ -0,0 +1,260 @@ +package com.vaadin.ui; + +import java.io.ByteArrayInputStream; +import java.io.Serializable; +import java.lang.reflect.Method; +import java.net.URL; +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.ParameterHandler; +import com.vaadin.terminal.URIHandler; + +/** + * LoginForm is a Toolkit component to handle common problem among Ajax + * applications: browsers password managers don't fill dynamically created forms + * like all those UI elements created by IT Mill Toolkit. + * <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. + * <p> + * Note, this is a new Ajax terminal specific component and is likely to change. + * + * @since 5.3 + */ +@SuppressWarnings("serial") +public class LoginForm extends CustomComponent { + + private Embedded iframe = new Embedded(); + + private ApplicationResource loginPage = new ApplicationResource() { + + public Application getApplication() { + return LoginForm.this.getApplication(); + } + + public int getBufferSize() { + return getLoginHTML().length; + } + + public long getCacheTime() { + return -1; + } + + public String getFilename() { + return "login"; + } + + public DownloadStream getStream() { + return new DownloadStream(new ByteArrayInputStream(getLoginHTML()), + getMIMEType(), getFilename()); + } + + public String getMIMEType() { + return "text/html"; + } + }; + + private ParameterHandler paramHandler = new ParameterHandler() { + + public void handleParameters(Map parameters) { + if (parameters.containsKey("username")) { + getWindow().addURIHandler(uriHandler); + + HashMap params = new HashMap(); + // expecting single params + for (Iterator it = parameters.keySet().iterator(); it.hasNext();) { + String key = (String) it.next(); + String value = ((String[]) parameters.get(key))[0]; + params.put(key, value); + } + LoginEvent event = new LoginEvent(params); + fireEvent(event); + } + } + }; + + private URIHandler uriHandler = new URIHandler() { + private final String responce = "<html><body>Login form handeled." + + "<script type='text/javascript'>top.itmill.forceSync();" + + "</script></body></html>"; + + public DownloadStream handleURI(URL context, String relativeUri) { + if (relativeUri != null && relativeUri.contains("loginHandler")) { + if (window != null) { + window.removeURIHandler(this); + } + DownloadStream downloadStream = new DownloadStream( + new ByteArrayInputStream(responce.getBytes()), + "text/html", "loginSuccesfull"); + downloadStream.setCacheTime(-1); + return downloadStream; + } else { + return null; + } + } + }; + + private Window window; + + public LoginForm() { + iframe.setType(Embedded.TYPE_BROWSER); + iframe.setSizeFull(); + setSizeFull(); + setCompositionRoot(iframe); + } + + /** + * 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 theme = getApplication().getMainWindow().getTheme(); + String guessedThemeUri = getApplication().getURL() + "ITMILL/themes/" + + (theme == null ? "default" : theme) + "/styles.css"; + String guessedThemeUri2 = getApplication().getURL() + + "../ITMILL/themes/" + (theme == null ? "default" : theme) + + "/styles.css"; + + String appUri = getApplication().getURL().toString(); + + 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();};" + + "</script>" + + "<link rel='stylesheet' href='" + + guessedThemeUri + + "'/>" + + "<link rel='stylesheet' href='" + + guessedThemeUri2 + + "'/>" + + "</head><body onload='setTarget();' style='margin:0;padding:0;'>" + + "<div class='i-app i-app-loginpage'>" + + "<iframe name='logintarget' style='width:0;height:0;" + + "border:0;margin:0;padding:0;'></iframe>" + + "<form id='loginf' target='logintarget'>" + + "<div>Username</div><div >" + + "<input class='i-textfield' style='display:block;' type='text' name='username'></div>" + + "<div>Password</div>" + + "<div><input class='i-textfield' style='display:block;' type='password' name='password'></div>" + + "<div><input class='i-button' type='submit' value='Login'></div></form></div>" + "</body></html>") + .getBytes(); + } + + @Override + public void attach() { + super.attach(); + getApplication().addResource(loginPage); + getWindow().addParameterHandler(paramHandler); + iframe.setSource(loginPage); + } + + @Override + public void detach() { + getApplication().removeResource(loginPage); + getWindow().removeParameterHandler(paramHandler); + // store window temporary to properly remove uri handler once + // response is handled. (May happen if login handler removes login + // form + window = getWindow(); + if (window.getParent() != null) { + window = (Window) window.getParent(); + } + super.detach(); + } + + /** + * This event is sent when login form is submitted. + */ + public class LoginEvent extends Event { + + private Map params; + + private LoginEvent(Map 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 (String) 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; + + 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); + } + +} diff --git a/src/com/vaadin/ui/MenuBar.java b/src/com/vaadin/ui/MenuBar.java new file mode 100644 index 0000000000..0ce6e725d9 --- /dev/null +++ b/src/com/vaadin/ui/MenuBar.java @@ -0,0 +1,617 @@ +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; + +/** + * <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 { + + // Items of the top-level menu + private final List<MenuItem> menuItems; + + // Number of items in this menu + private static int numberOfItems = 0; + + private boolean collapseItems; + private Resource submenuIcon; + private MenuItem moreItem; + + /** Tag is the UIDL element name for client-server communications. */ + @Override + public java.lang.String getTag() { + return "menubar"; + } + + /** Paint (serialise) the component for the client. */ + @Override + public void paintContent(PaintTarget target) throws PaintException { + + // Superclass writes any common attributes in the paint target. + super.paintContent(target); + + // Stack for list iterators + Stack<Iterator<MenuItem>> iteratorStack = new Stack<Iterator<MenuItem>>(); + + target.startTag("options"); + + if (submenuIcon != null) { + target.addAttribute("submenuIcon", submenuIcon); + } + + target.addAttribute("collapseItems", collapseItems); + + if (collapseItems) { + 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"); + + Iterator<MenuItem> itr = menuItems.iterator(); + + // This generates the tree from the contents of the menu + while (itr.hasNext()) { + + MenuItem item = itr.next(); + + target.startTag("item"); + + target.addAttribute("text", item.getText()); + target.addAttribute("id", item.getId()); + + Command command = item.getCommand(); + if (command != null) { + target.addAttribute("command", true); + } else { + target.addAttribute("command", false); + } + + Resource icon = item.getIcon(); + if (icon != null) { + target.addAttribute("icon", icon); + } + + if (item.hasChildren()) { + iteratorStack.push(itr); // For later use + + // Go through the children + itr = item.getChildren().iterator(); + } else { + target.endTag("item"); // Item had no children, end description + } + + // The end submenu. More than one submenu may end at once. + while (!itr.hasNext() && !iteratorStack.empty()) { + itr = iteratorStack.pop(); + target.endTag("item"); + } + + } + + target.endTag("items"); + } + + /** Deserialize changes received from client. */ + @Override + public void changeVariables(Object source, Map 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.getCommand().menuSelected(tmpItem); + } + }// if + }// changeVariables + + /** + * Constructs an empty, horizontal menu + */ + public MenuBar() { + menuItems = new ArrayList<MenuItem>(); + setCollapse(true); + 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 icon to be used if a sub-menu has children. Defaults to null; + * + * @param icon + */ + public void setSubmenuIcon(Resource icon) { + submenuIcon = icon; + requestRepaint(); + } + + /** + * Get the icon used for sub-menus. Returns null if no icon is set. + * + * @return + */ + public Resource getSubmenuIcon() { + return submenuIcon; + } + + /** + * Enable or disable collapsing top-level items. Top-level items will + * collapse to if there is not enough room for them. Items that don't fit + * will be placed under the "More" menu item. + * + * Collapsing is enabled by default. + * + * @param collapse + */ + public void setCollapse(boolean collapse) { + collapseItems = collapse; + requestRepaint(); + } + + /** + * Collapsing is enabled by default. + * + * @return true if the top-level items will be collapsed + */ + public boolean getCollapse() { + return collapseItems; + } + + /** + * 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 the "More" text is be + * used. + * + * @param item + */ + public void setMoreMenuItem(MenuItem item) { + if (item != null) { + moreItem = item; + } else { + moreItem = new MenuItem("More", null, null); + } + requestRepaint(); + } + + /** + * Get the MenuItem used as the collapse menu item. + * + * @return + */ + public MenuItem getMoreMenuItem() { + return moreItem; + } + + /** + * 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; + + /** + * 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 itsChildren != null; + } + + /** + * 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 + */ + public MenuBar.MenuItem addItem(String caption, Resource icon, + MenuBar.Command command) { + 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 + * + */ + public MenuBar.MenuItem addItemBefore(String caption, Resource icon, + MenuBar.Command command, MenuBar.MenuItem itemToAddBefore) { + + 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() { + return itsChildren.size(); + } + + /** + * 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; + } + + }// class MenuItem + +}// class MenuBar diff --git a/src/com/vaadin/ui/NativeSelect.java b/src/com/vaadin/ui/NativeSelect.java new file mode 100644 index 0000000000..e178a39397 --- /dev/null +++ b/src/com/vaadin/ui/NativeSelect.java @@ -0,0 +1,91 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +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/src/com/vaadin/ui/OptionGroup.java b/src/com/vaadin/ui/OptionGroup.java new file mode 100644 index 0000000000..861b57f5c6 --- /dev/null +++ b/src/com/vaadin/ui/OptionGroup.java @@ -0,0 +1,41 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.util.Collection; + +import com.vaadin.data.Container; +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.PaintTarget; + +/** + * Configures select to be used as an option group. + */ +@SuppressWarnings("serial") +public class OptionGroup extends AbstractSelect { + + 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"); + super.paintContent(target); + } + +} diff --git a/src/com/vaadin/ui/OrderedLayout.java b/src/com/vaadin/ui/OrderedLayout.java new file mode 100644 index 0000000000..572d621919 --- /dev/null +++ b/src/com/vaadin/ui/OrderedLayout.java @@ -0,0 +1,122 @@ +package com.vaadin.ui; + +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.PaintTarget; + +/** + * Ordered layout. + * + * <code>OrderedLayout</code> is a component container, which shows the + * subcomponents in the order of their addition in specified orientation. + * + * @author IT Mill Ltd. + * @version + * @VERSION@ + * @since 3.0 + * @deprecated Replaced by VerticalLayout/HorizontalLayout. For type checking + * please not that VerticalLayout/HorizontalLayout do not extend + * OrderedLayout but AbstractOrderedLayout (which also OrderedLayout + * extends). + */ +@SuppressWarnings("serial") +@Deprecated +public class OrderedLayout extends AbstractOrderedLayout { + /* Predefined orientations */ + + /** + * Components are to be laid out vertically. + */ + public static final int ORIENTATION_VERTICAL = 0; + + /** + * Components are to be laid out horizontally. + */ + public static final int ORIENTATION_HORIZONTAL = 1; + + /** + * Orientation of the layout. + */ + private int orientation; + + /** + * Creates a new ordered layout. The order of the layout is + * <code>ORIENTATION_VERTICAL</code>. + * + * @deprecated Use VerticalLayout instead. + */ + @Deprecated + public OrderedLayout() { + this(ORIENTATION_VERTICAL); + } + + /** + * Create a new ordered layout. The orientation of the layout is given as + * parameters. + * + * @param orientation + * the Orientation of the layout. + * + * @deprecated Use VerticalLayout/HorizontalLayout instead. + */ + @Deprecated + public OrderedLayout(int orientation) { + this.orientation = orientation; + if (orientation == ORIENTATION_VERTICAL) { + setWidth(100, UNITS_PERCENTAGE); + } + } + + /** + * Gets the orientation of the container. + * + * @return the Value of property orientation. + */ + public int getOrientation() { + return orientation; + } + + /** + * Sets the orientation of this OrderedLayout. This method should only be + * used before initial paint. + * + * @param orientation + * the New value of property orientation. + * @deprecated Use VerticalLayout/HorizontalLayout or define orientation in + * constructor instead + */ + @Deprecated + public void setOrientation(int orientation) { + setOrientation(orientation, true); + } + + /** + * Internal method to change orientation of layout. This method should only + * be used before initial paint. + * + * @param orientation + */ + protected void setOrientation(int orientation, boolean needsRepaint) { + // Checks the validity of the argument + if (orientation < ORIENTATION_VERTICAL + || orientation > ORIENTATION_HORIZONTAL) { + throw new IllegalArgumentException(); + } + + this.orientation = orientation; + if (needsRepaint) { + requestRepaint(); + } + } + + @Override + public void paintContent(PaintTarget target) throws PaintException { + super.paintContent(target); + + // Adds the orientation attributes (the default is vertical) + if (orientation == ORIENTATION_HORIZONTAL) { + target.addAttribute("orientation", "horizontal"); + } + + } + +} diff --git a/src/com/vaadin/ui/Panel.java b/src/com/vaadin/ui/Panel.java new file mode 100644 index 0000000000..e50713846c --- /dev/null +++ b/src/com/vaadin/ui/Panel.java @@ -0,0 +1,555 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.util.Iterator; +import java.util.LinkedList; +import java.util.Map; + +import com.vaadin.event.Action; +import com.vaadin.event.ShortcutAction; +import com.vaadin.event.Action.Handler; +import com.vaadin.terminal.KeyMapper; +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.PaintTarget; +import com.vaadin.terminal.Scrollable; + +/** + * Panel - a simple single component container. + * + * @author IT Mill Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +@SuppressWarnings("serial") +public class Panel extends AbstractComponentContainer implements Scrollable, + ComponentContainer.ComponentAttachListener, + ComponentContainer.ComponentDetachListener, Action.Container { + + public static final String STYLE_LIGHT = "light"; + + /** + * Content of the panel. + */ + private ComponentContainer content; + + /** + * Scroll X position. + */ + private int scrollOffsetX = 0; + + /** + * Scroll Y position. + */ + private int scrollOffsetY = 0; + + /** + * Scrolling mode. + */ + private boolean scrollable = false; + + /** List of action handlers */ + private LinkedList actionHandlers = null; + + /** Action mapper */ + private KeyMapper actionMapper = null; + + /** + * 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) { + setContent(content); + setWidth(100, UNITS_PERCENTAGE); + } + + /** + * Creates a new empty panel with caption. Default layout is used. + * + * @param caption + * the caption used in the panel. + */ + 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. + * @param content + * the content used in the panel. + */ + public Panel(String caption, ComponentContainer content) { + this(content); + setCaption(caption); + } + + /** + * Gets the current layout of the panel. + * + * @return the Current layout of the panel. + * @deprecated A Panel can now contain a ComponentContainer which is not + * necessarily a Layout. Use {@link #getContent()} instead. + */ + @Deprecated + public Layout getLayout() { + if (content instanceof Layout) { + return (Layout) content; + } else if (content == null) { + return null; + } + + throw new IllegalStateException( + "Panel does not contain a Layout. Use getContent() instead of getLayout()."); + } + + /** + * Sets the layout of the panel. + * + * If given layout is null, a VerticalLayout with margins set is used as a + * default. + * + * Components from old layout are not moved to new layout by default + * (changed in 5.2.2). Use function in Layout interface manually. + * + * @param newLayout + * the New layout of the panel. + * @deprecated A Panel can now contain a ComponentContainer which is not + * necessarily a Layout. Use + * {@link #setContent(ComponentContainer)} instead. + */ + @Deprecated + public void setLayout(Layout newLayout) { + setContent(newLayout); + } + + /** + * 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; + } + + /** + * 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; + } + + /** + * 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 { + content.paint(target); + + if (isScrollable()) { + target.addVariable(this, "scrollLeft", getScrollLeft()); + target.addVariable(this, "scrollTop", getScrollTop()); + } + + if (actionHandlers != null && !actionHandlers.isEmpty()) { + target.addVariable(this, "action", ""); + target.startTag("actions"); + + for (final Iterator ahi = actionHandlers.iterator(); ahi.hasNext();) { + final Action[] aa = ((Action.Handler) ahi.next()).getActions( + null, this); + if (aa != null) { + for (int ai = 0; ai < aa.length; ai++) { + final Action a = aa[ai]; + target.startTag("action"); + final String akey = actionMapper.key(aa[ai]); + target.addAttribute("key", akey); + if (a.getCaption() != null) { + target.addAttribute("caption", a.getCaption()); + } + if (a.getIcon() != null) { + target.addAttribute("icon", a.getIcon()); + } + if (a instanceof ShortcutAction) { + final ShortcutAction sa = (ShortcutAction) a; + target.addAttribute("kc", sa.getKeyCode()); + final int[] modifiers = sa.getModifiers(); + if (modifiers != null) { + final String[] smodifiers = new String[modifiers.length]; + for (int i = 0; i < modifiers.length; i++) { + smodifiers[i] = String + .valueOf(modifiers[i]); + } + target.addAttribute("mk", smodifiers); + } + } + target.endTag("action"); + } + } + } + target.endTag("actions"); + } + } + + @Override + public void requestRepaintAll() { + // Panel has odd structure, delegate to layout + requestRepaint(); + if (getContent() != null) { + getContent().requestRepaintAll(); + } + } + + /** + * Gets the component UIDL tag. + * + * @return the Component UIDL tag as string. + */ + @Override + public String getTag() { + return "panel"; + } + + /** + * 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 added. + * @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 trough all the components + * in the container. + * + * @return the Iterator of the components inside the container. + * @see com.vaadin.ui.ComponentContainer#getComponentIterator() + */ + public Iterator getComponentIterator() { + return content.getComponentIterator(); + } + + /** + * Called when one or more variables handled by the implementing class are + * changed. + * + * @see com.vaadin.terminal.VariableOwner#changeVariables(Object, + * Map) + */ + @Override + public void changeVariables(Object source, Map variables) { + super.changeVariables(source, 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 + scrollOffsetX = newScrollX.intValue(); + } + if (newScrollY != null && newScrollY.intValue() != getScrollTop()) { + // set internally, not to fire request repaint + scrollOffsetY = newScrollY.intValue(); + } + + // Actions + if (variables.containsKey("action")) { + final String key = (String) variables.get("action"); + final Action action = (Action) actionMapper.get(key); + if (action != null && actionHandlers != null) { + Object[] array = actionHandlers.toArray(); + for (int i = 0; i < array.length; i++) { + ((Action.Handler) array[i]) + .handleAction(action, this, this); + } + } + } + + } + + /* Scrolling functionality */ + + /* Documented in interface */ + public int getScrollLeft() { + return scrollOffsetX; + } + + /** + * @deprecated use getScrollLeft() instead + */ + @Deprecated + public int getScrollOffsetX() { + return getScrollLeft(); + } + + /* Documented in interface */ + public int getScrollTop() { + return scrollOffsetY; + } + + /** + * @deprecated use getScrollTop() instead + */ + @Deprecated + public int getScrollOffsetY() { + return getScrollTop(); + } + + /* Documented in interface */ + public boolean isScrollable() { + return scrollable; + } + + /* Documented in interface */ + public void setScrollable(boolean isScrollingEnabled) { + if (scrollable != isScrollingEnabled) { + scrollable = isScrollingEnabled; + requestRepaint(); + } + } + + /* Documented in interface */ + public void setScrollLeft(int pixelsScrolled) { + if (pixelsScrolled < 0) { + throw new IllegalArgumentException( + "Scroll offset must be at least 0"); + } + if (scrollOffsetX != pixelsScrolled) { + scrollOffsetX = pixelsScrolled; + requestRepaint(); + } + } + + /** + * @deprecated use setScrollLeft() method instead + */ + @Deprecated + public void setScrollOffsetX(int pixels) { + setScrollLeft(pixels); + } + + /* Documented in interface */ + public void setScrollTop(int pixelsScrolledDown) { + if (pixelsScrolledDown < 0) { + throw new IllegalArgumentException( + "Scroll offset must be at least 0"); + } + if (scrollOffsetY != pixelsScrolledDown) { + scrollOffsetY = pixelsScrolledDown; + requestRepaint(); + } + } + + /** + * @deprecated use setScrollTop() method instead + */ + @Deprecated + public void setScrollOffsetY(int pixels) { + setScrollTop(pixels); + } + + /* Documented in superclass */ + 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) + */ + 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) + */ + public void componentDetachedFromContainer(ComponentDetachEvent event) { + if (event.getContainer() == content) { + fireComponentDetachEvent(event.getDetachedComponent()); + } + } + + /** + * Notifies the component that it is connected to an application. + * + * @see com.vaadin.ui.Component#attach() + */ + @Override + public void attach() { + // can't call parent here as this is Panels hierarchy is a hack + requestRepaint(); + if (content != null) { + content.attach(); + } + } + + /** + * Notifies the component that it is detached from the application. + * + * @see com.vaadin.ui.Component#detach() + */ + @Override + public void detach() { + // can't call parent here as this is Panels hierarchy is a hack + if (content != null) { + content.detach(); + } + } + + /** + * Removes all components from this container. + * + * @see com.vaadin.ui.ComponentContainer#removeAllComponents() + */ + @Override + public void removeAllComponents() { + content.removeAllComponents(); + } + + public void addActionHandler(Handler actionHandler) { + if (actionHandler != null) { + + if (actionHandlers == null) { + actionHandlers = new LinkedList(); + actionMapper = new KeyMapper(); + } + + if (!actionHandlers.contains(actionHandler)) { + actionHandlers.add(actionHandler); + requestRepaint(); + } + } + + } + + /** + * Removes an action handler. + * + * @see com.vaadin.event.Action.Container#removeActionHandler(Action.Handler) + */ + public void removeActionHandler(Action.Handler actionHandler) { + + if (actionHandlers != null && actionHandlers.contains(actionHandler)) { + + actionHandlers.remove(actionHandler); + + if (actionHandlers.isEmpty()) { + actionHandlers = null; + actionMapper = null; + } + + requestRepaint(); + } + } +} diff --git a/src/com/vaadin/ui/PopupDateField.java b/src/com/vaadin/ui/PopupDateField.java new file mode 100644 index 0000000000..5164601f0d --- /dev/null +++ b/src/com/vaadin/ui/PopupDateField.java @@ -0,0 +1,52 @@ +/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.ui;
+
+import java.util.Date;
+
+import com.vaadin.data.Property;
+
+/**
+ * <p>
+ * A date entry component, which displays the actual date selector as a popup.
+ *
+ * </p>
+ *
+ * @see DateField
+ * @see InlineDateField
+ * @author IT Mill Ltd.
+ * @version
+ * @VERSION@
+ * @since 5.0
+ */
+@SuppressWarnings("serial")
+public class PopupDateField extends DateField {
+
+ public PopupDateField() {
+ super();
+ type = TYPE_POPUP;
+ }
+
+ public PopupDateField(Property dataSource) throws IllegalArgumentException {
+ super(dataSource);
+ type = TYPE_POPUP;
+ }
+
+ public PopupDateField(String caption, Date value) {
+ super(caption, value);
+ type = TYPE_POPUP;
+ }
+
+ public PopupDateField(String caption, Property dataSource) {
+ super(caption, dataSource);
+ type = TYPE_POPUP;
+ }
+
+ public PopupDateField(String caption) {
+ super(caption);
+ type = TYPE_POPUP;
+ }
+
+}
diff --git a/src/com/vaadin/ui/PopupView.java b/src/com/vaadin/ui/PopupView.java new file mode 100644 index 0000000000..4fa89a7eb5 --- /dev/null +++ b/src/com/vaadin/ui/PopupView.java @@ -0,0 +1,432 @@ +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.PaintException; +import com.vaadin.terminal.PaintTarget; + +/** + * + * 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 IT Mill Ltd. + */ +@SuppressWarnings("serial") +public class PopupView extends AbstractComponentContainer { + + 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"); + } + } + + /* 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() { + public java.lang.String getMinimizedValueAsHTML() { + return small; + } + + 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 || newContent.getMinimizedValueAsHTML() == null + || newContent.getPopupComponent() == null) { + throw new IllegalArgumentException( + "Content object is or contains 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 automaticly 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() + */ + public Iterator<Component> getComponentIterator() { + return new Iterator<Component>() { + + private boolean first = visibleComponent == null; + + public boolean hasNext() { + return !first; + } + + public Component next() { + if (!first) { + first = true; + return visibleComponent; + } else { + return null; + } + } + + public void remove() { + throw new UnsupportedOperationException(); + } + }; + + } + + /** + * 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 + */ + 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. + */ + + /** + * @see com.vaadin.ui.AbstractComponent#getTag() + */ + @Override + public java.lang.String getTag() { + return "popupview"; + } + + /** + * 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 { + // Superclass writes any common attributes in the paint target. + super.paintContent(target); + + String html = content.getMinimizedValueAsHTML(); + if (html == null) { + throw new PaintException( + "Recieved null when trying to paint minimized value."); + } + target.addAttribute("html", content.getMinimizedValueAsHTML()); + target.addAttribute("hideOnMouseOut", hideOnMouseOut); + + // Only paint component to client if we know that the popup is showing + if (isPopupVisible()) { + target.startTag("popupComponent"); + visibleComponent.paint(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 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/src/com/vaadin/ui/ProgressIndicator.java b/src/com/vaadin/ui/ProgressIndicator.java new file mode 100644 index 0000000000..1ad5848dfb --- /dev/null +++ b/src/com/vaadin/ui/ProgressIndicator.java @@ -0,0 +1,267 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import com.vaadin.data.Property; +import com.vaadin.data.util.ObjectProperty; +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.PaintTarget; + +/** + * <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 IT Mill Ltd. + * @version + * @VERSION@ + * @since 4 + */ +@SuppressWarnings("serial") +public class ProgressIndicator extends AbstractField implements Property, + Property.Viewer, Property.ValueChangeListener { + + /** + * 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(new Float(0), Float.class)); + } + + /** + * Creates a new instance of ProgressIndicator with given state. + * + * @param value + */ + public ProgressIndicator(Float value) { + setPropertyDataSource(new ObjectProperty(value, Float.class)); + } + + /** + * Creates a new instance of ProgressIndicator with stae read from given + * datasource. + * + * @param contentSource + */ + public ProgressIndicator(Property contentSource) { + setPropertyDataSource(contentSource); + } + + /** + * Gets the component UIDL tag. + * + * @return the Component UIDL tag as string. + */ + @Override + public String getTag() { + return "progressindicator"; + } + + /** + * 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 Object getValue() { + if (dataSource == null) { + throw new IllegalStateException("Datasource must be set"); + } + return 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(java.lang.Object) + */ + @Override + public void setValue(Object newValue) { + if (dataSource == null) { + throw new IllegalStateException("Datasource must be set"); + } + dataSource.setValue(newValue); + } + + /** + * @see com.vaadin.ui.AbstractField#toString() + */ + @Override + public String toString() { + if (dataSource == null) { + throw new IllegalStateException("Datasource must be set"); + } + return dataSource.toString(); + } + + /** + * @see com.vaadin.ui.AbstractField#getType() + */ + @Override + public Class 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; + } + +} diff --git a/src/com/vaadin/ui/RichTextArea.java b/src/com/vaadin/ui/RichTextArea.java new file mode 100644 index 0000000000..4c1c281811 --- /dev/null +++ b/src/com/vaadin/ui/RichTextArea.java @@ -0,0 +1,35 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.PaintTarget; + +/** + * 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. + */ +@SuppressWarnings("serial") +public class RichTextArea extends TextField { + + @Override + public void paintContent(PaintTarget target) throws PaintException { + target.addAttribute("richtext", true); + super.paintContent(target); + } + + /** + * RichTextArea does not support input prompt. + */ + @Override + public void setInputPrompt(String inputPrompt) { + throw new UnsupportedOperationException( + "RichTextArea does not support inputPrompt"); + } + +} diff --git a/src/com/vaadin/ui/Select.java b/src/com/vaadin/ui/Select.java new file mode 100644 index 0000000000..ce3fb7e687 --- /dev/null +++ b/src/com/vaadin/ui/Select.java @@ -0,0 +1,476 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +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.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 IT Mill Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +@SuppressWarnings("serial") +public class Select extends AbstractSelect implements AbstractSelect.Filtering { + + /** + * 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; + private List filteredOptions; + + /** + * Flag to indicate that request repaint is called by filter request only + */ + private boolean optionRequest; + + /* 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); + 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); + } + + // Adds the required attribute + if (isRequired()) { + target.addAttribute("required", 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("filteringmode", getFilteringMode()); + + // Paints the options and create array of selected id keys + // TODO Also use conventional rendering if lazy loading is not supported + // by terminal + int keyIndex = 0; + + target.startTag("options"); + + if (currentPage < 0) { + optionRequest = false; + currentPage = 0; + filterstring = ""; + } + + List options = getFilteredOptions(); + options = sanitetizeList(options, needNullSelectOption); + + final boolean paintNullSelection = needNullSelectOption + && (currentPage == 0 && (filterstring == null || filterstring + .equals(""))); + + 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 (filteredOptions != null) { + target.addAttribute("totalMatches", filteredOptions.size() + + (needNullSelectOption ? 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; + } + + /** + * 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. + * + * @param options + * @param needNullSelectOption + * flag to indicate if nullselect option needs to be taken into + * consideration + */ + private List sanitetizeList(List options, boolean needNullSelectOption) { + + if (options.size() > pageLength) { + int first = currentPage * pageLength; + int last = first + pageLength; + if (needNullSelectOption) { + if (currentPage > 0) { + first--; + } + last--; + } + if (options.size() < last) { + last = options.size(); + } + if (!optionRequest) { + // TODO ensure proper page + if (!isMultiSelect()) { + Object selection = getValue(); + if (selection != null) { + int index = options.indexOf(selection); + if (index != -1 && (index < first || index >= last)) { + int newPage = (index + (needNullSelectOption ? 1 + : 0)) + / pageLength; + currentPage = newPage; + return sanitetizeList(options, needNullSelectOption); + } + } + } + } + + return options.subList(first, last); + } else { + return options; + } + } + + protected List getFilteredOptions() { + if (filterstring == null || filterstring.equals("") + || filteringMode == FILTERINGMODE_OFF) { + prevfilterstring = null; + filteredOptions = new LinkedList(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(); + 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 variables) { + + // 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 s = new LinkedList(); + 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) { + Set newsel = (Set) getValue(); + if (newsel == null) { + newsel = new HashSet(); + } else { + newsel = new HashSet(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(); + return; + } + + // Try to set the property value + + // 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; + } + + } + + @Override + public void requestRepaint() { + super.requestRepaint(); + optionRequest = false; + prevfilterstring = filterstring; + filterstring = null; + } + + private void optionRepaint() { + super.requestRepaint(); + } + + /** + * Gets the component UIDL tag. + * + * @return the Component UIDL tag as string. + */ + @Override + public String getTag() { + return "select"; + } + + public void setFilteringMode(int filteringMode) { + this.filteringMode = filteringMode; + } + + 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; + } + +} diff --git a/src/com/vaadin/ui/Slider.java b/src/com/vaadin/ui/Slider.java new file mode 100644 index 0000000000..ec9fc2c90b --- /dev/null +++ b/src/com/vaadin/ui/Slider.java @@ -0,0 +1,503 @@ +/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.ui;
+
+import java.util.Map;
+
+import com.vaadin.terminal.PaintException;
+import com.vaadin.terminal.PaintTarget;
+
+/**
+ * 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 IT Mill Ltd.
+ */
+@SuppressWarnings("serial")
+public class Slider extends AbstractField {
+
+ public static final int ORIENTATION_HORIZONTAL = 0;
+
+ public static final int ORIENTATION_VERTICAL = 1;
+
+ /**
+ * Style constant representing a scrollbar styled slider. Use this with
+ * {@link #addStyleName(String)}. Default styling usually represents a
+ * common slider found e.g. in Adobe Photoshop. The client side
+ * implementation dictates how different styles will look.
+ */
+ @Deprecated
+ public static final String STYLE_SCROLLBAR = "scrollbar";
+
+ /** Minimum value of slider */
+ private double min = 0;
+
+ /** Maximum value of slider */
+ private double max = 100;
+
+ /**
+ * Resolution, how many digits are considered relevant after desimal point.
+ * Must be a non-negative value
+ */
+ private int resolution = 0;
+
+ /**
+ * Slider orientation (horizontal/vertical), defaults .
+ */
+ private int orientation = ORIENTATION_HORIZONTAL;
+
+ /**
+ * Slider size in pixels. In horizontal mode, if set to -1, allow 100% width
+ * of container. In vertical mode, if set to -1, default height is
+ * determined by the client-side implementation.
+ *
+ * @deprecated
+ */
+ @Deprecated
+ private int size = -1;
+
+ /**
+ * Handle (draggable control element) size in percents relative to base
+ * size. Must be a value between 1-99. Other values are converted to nearest
+ * bound. A negative value sets the width to auto (client-side
+ * implementation calculates).
+ *
+ * @deprecated The size is dictated by the current theme.
+ */
+ @Deprecated
+ private int handleSize = -1;
+
+ /**
+ * Show arrows that can be pressed to slide the handle in some increments
+ * (client-side implementation decides the increment, usually somewhere
+ * between 5-10% of slide range).
+ */
+ @Deprecated
+ private final boolean arrows = false;
+
+ /**
+ * 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. All slider
+ * values set to defaults.
+ *
+ * @param caption
+ * The caption for this Slider (e.g. "Volume").
+ */
+ public Slider(String caption) {
+ this();
+ setCaption(caption);
+ }
+
+ /**
+ * Create a new slider with given range and resolution
+ *
+ * @param min
+ * @param max
+ * @param resolution
+ */
+ public Slider(double min, double max, int resolution) {
+ this();
+ setMin(min);
+ setMax(max);
+ setResolution(resolution);
+ }
+
+ /**
+ * Create a new slider with given range
+ *
+ * @param min
+ * @param max
+ */
+ public Slider(int min, int max) {
+ this();
+ setMin(min);
+ setMax(max);
+ setResolution(0);
+ }
+
+ /**
+ * Create a new slider with given caption and range
+ *
+ * @param caption
+ * @param min
+ * @param max
+ */
+ public Slider(String caption, int min, int max) {
+ this(min, max);
+ setCaption(caption);
+ }
+
+ /**
+ * Gets the biggest possible value in Sliders range.
+ *
+ * @return the biggest value slider can have
+ */
+ public double getMax() {
+ return max;
+ }
+
+ /**
+ * Set the maximum value of the Slider. If the current value of the Slider
+ * is out of new bounds, the value is set to new minimum.
+ *
+ * @param max
+ * New maximum value of the Slider.
+ */
+ public void setMax(double max) {
+ this.max = max;
+ try {
+ if ((new Double(getValue().toString())).doubleValue() > max) {
+ super.setValue(new Double(max));
+ }
+ } catch (final ClassCastException e) {
+ // FIXME: Handle exception
+ /*
+ * Where does ClassCastException come from? Can't see any casts
+ * above
+ */
+ super.setValue(new Double(max));
+ }
+ requestRepaint();
+ }
+
+ /**
+ * Gets the minimum value in Sliders range.
+ *
+ * @return the smalles value slider can have
+ */
+ public double getMin() {
+ return min;
+ }
+
+ /**
+ * Set the minimum value of the Slider. If the current value of the Slider
+ * is out of new bounds, the value is set to new minimum.
+ *
+ * @param min
+ * New minimum value of the Slider.
+ */
+ public void setMin(double min) {
+ this.min = min;
+ try {
+ if ((new Double(getValue().toString())).doubleValue() < min) {
+ super.setValue(new Double(min));
+ }
+ } catch (final ClassCastException e) {
+ // FIXME: Handle exception
+ /*
+ * Where does ClassCastException come from? Can't see any casts
+ * above
+ */
+ super.setValue(new Double(min));
+ }
+ requestRepaint();
+ }
+
+ /**
+ * Get the current orientation of the Slider (horizontal or vertical).
+ *
+ * @return orientation
+ */
+ public int getOrientation() {
+ return orientation;
+ }
+
+ /**
+ * Set the orientation of the Slider.
+ *
+ * @param int new orientation
+ */
+ public void setOrientation(int orientation) {
+ this.orientation = orientation;
+ requestRepaint();
+ }
+
+ /**
+ * Get the current resolution of the Slider.
+ *
+ * @return resolution
+ */
+ public int getResolution() {
+ return resolution;
+ }
+
+ /**
+ * Set a new resolution for the Slider.
+ *
+ * @param resolution
+ */
+ public void setResolution(int resolution) {
+ if (resolution < 0) {
+ return;
+ }
+ this.resolution = resolution;
+ requestRepaint();
+ }
+
+ /**
+ * Set the value of this Slider.
+ *
+ * @param value
+ * New value of Slider. Must be within Sliders range (min - max),
+ * otherwise throws an exception.
+ * @param repaintIsNotNeeded
+ * If true, client-side is not requested to repaint itself.
+ * @throws ValueOutOfBoundsException
+ */
+ public void setValue(Double value, boolean repaintIsNotNeeded)
+ throws ValueOutOfBoundsException {
+ 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(new Double(newValue), repaintIsNotNeeded);
+ }
+
+ /**
+ * Set the value of this Slider.
+ *
+ * @param value
+ * New value of Slider. Must be within Sliders range (min - max),
+ * otherwise throws an exception.
+ * @throws ValueOutOfBoundsException
+ */
+ public void setValue(Double value) throws ValueOutOfBoundsException {
+ setValue(value, false);
+ }
+
+ /**
+ * Set the value of this Slider.
+ *
+ * @param value
+ * New value of Slider. Must be within Sliders range (min - max),
+ * otherwise throws an exception.
+ * @throws ValueOutOfBoundsException
+ */
+ public void setValue(double value) throws ValueOutOfBoundsException {
+ setValue(new Double(value), false);
+ }
+
+ /**
+ * Get the current Slider size.
+ *
+ * @return size in pixels or -1 for auto sizing.
+ * @deprecated use standard getWidth/getHeight instead
+ */
+ @Deprecated
+ public int getSize() {
+ return size;
+ }
+
+ /**
+ * Set the size for this Slider.
+ *
+ * @param size
+ * in pixels, or -1 auto sizing.
+ * @deprecated use standard setWidth/setHeight instead
+ */
+ @Deprecated
+ public void setSize(int size) {
+ this.size = size;
+ switch (orientation) {
+ case ORIENTATION_HORIZONTAL:
+ setWidth(size, UNITS_PIXELS);
+ break;
+ default:
+ setHeight(size, UNITS_PIXELS);
+ break;
+ }
+ requestRepaint();
+ }
+
+ /**
+ * Get the handle size of this Slider.
+ *
+ * @return handle size in percentages.
+ * @deprecated The size is dictated by the current theme.
+ */
+ @Deprecated
+ public int getHandleSize() {
+ return handleSize;
+ }
+
+ /**
+ * Set the handle size of this Slider.
+ *
+ * @param handleSize
+ * in percentages relative to slider base size.
+ * @deprecated The size is dictated by the current theme.
+ */
+ @Deprecated
+ public void setHandleSize(int handleSize) {
+ if (handleSize < 0) {
+ this.handleSize = -1;
+ } else if (handleSize > 99) {
+ this.handleSize = 99;
+ } else if (handleSize < 1) {
+ this.handleSize = 1;
+ } else {
+ this.handleSize = handleSize;
+ }
+ requestRepaint();
+ }
+
+ @Override
+ public String getTag() {
+ return "slider";
+ }
+
+ @Override
+ public void paintContent(PaintTarget target) throws PaintException {
+ super.paintContent(target);
+
+ 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", ((Double) getValue())
+ .doubleValue());
+ } else {
+ target.addVariable(this, "value", ((Double) getValue()).intValue());
+ }
+
+ if (orientation == ORIENTATION_VERTICAL) {
+ target.addAttribute("vertical", true);
+ }
+
+ if (arrows) {
+ target.addAttribute("arrows", true);
+ }
+
+ if (size > -1) {
+ target.addAttribute("size", size);
+ }
+
+ if (min != max && min < max) {
+ target.addAttribute("hsize", handleSize);
+ } else {
+ target.addAttribute("hsize", 100);
+ }
+
+ }
+
+ /**
+ * 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 variables) {
+ super.changeVariables(source, 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);
+ }
+ }
+ }
+ }
+
+ /**
+ * ValueOutOfBoundsException
+ *
+ * @author IT Mill Ltd.
+ *
+ */
+ public class ValueOutOfBoundsException extends Exception {
+
+ private final Double value;
+
+ /**
+ * Constructs an <code>ValueOutOfBoundsException</code> with the
+ * specified detail message.
+ *
+ * @param valueOutOfBounds
+ */
+ public ValueOutOfBoundsException(Double valueOutOfBounds) {
+ value = valueOutOfBounds;
+ }
+
+ public Double getValue() {
+ return value;
+ }
+
+ }
+
+ @Override
+ public Class getType() {
+ return Double.class;
+ }
+
+}
diff --git a/src/com/vaadin/ui/SplitPanel.java b/src/com/vaadin/ui/SplitPanel.java new file mode 100644 index 0000000000..bad238d888 --- /dev/null +++ b/src/com/vaadin/ui/SplitPanel.java @@ -0,0 +1,330 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.util.Iterator; +import java.util.Map; + +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.PaintTarget; +import com.vaadin.terminal.gwt.client.RenderInformation.Size; + +/** + * SplitPanel. + * + * <code>SplitPanel</code> is a component container, that can contain two + * components (possibly containers) which are split by divider element. + * + * @author IT Mill Ltd. + * @version + * @VERSION@ + * @since 5.0 + */ +@SuppressWarnings("serial") +public class SplitPanel extends AbstractLayout { + + /* Predefined orientations */ + + /** + * Components are to be layed out vertically. + */ + public static final int ORIENTATION_VERTICAL = 0; + + /** + * Components are to be layed out horizontally. + */ + public static final int ORIENTATION_HORIZONTAL = 1; + + private Component firstComponent; + + private Component secondComponent; + + /** + * Orientation of the layout. + */ + private int orientation; + + private int pos = 50; + + private int posUnit = UNITS_PERCENTAGE; + + private boolean locked = false; + + /** + * Creates a new split panel. The orientation of the panels is + * <code>ORIENTATION_VERTICAL</code>. + */ + public SplitPanel() { + orientation = ORIENTATION_VERTICAL; + setSizeFull(); + } + + /** + * Create a new split panels. The orientation of the panel is given as + * parameters. + * + * @param orientation + * the Orientation of the layout. + */ + public SplitPanel(int orientation) { + this(); + setOrientation(orientation); + } + + /** + * Gets the component UIDL tag. + * + * @return the Component UIDL tag as string. + */ + @Override + public String getTag() { + if (orientation == ORIENTATION_HORIZONTAL) { + return "hsplitpanel"; + } else { + return "vsplitpanel"; + } + } + + /** + * 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 (firstComponent == null) { + firstComponent = c; + } else if (secondComponent == null) { + secondComponent = c; + } else { + throw new UnsupportedOperationException( + "Split panel can contain only two components"); + } + super.addComponent(c); + requestRepaint(); + } + + public void setFirstComponent(Component c) { + if (firstComponent != null) { + // detach old + removeComponent(firstComponent); + } + firstComponent = c; + super.addComponent(c); + } + + public void setSecondComponent(Component c) { + if (secondComponent != null) { + // detach old + removeComponent(secondComponent); + } + secondComponent = c; + super.addComponent(c); + } + + /** + * Removes the component from this container. + * + * @param c + * the component to be removed. + */ + @Override + public void removeComponent(Component c) { + super.removeComponent(c); + if (c == firstComponent) { + firstComponent = null; + } else { + secondComponent = null; + } + requestRepaint(); + } + + /** + * Gets the component container iterator for going trough all the components + * in the container. + * + * @return the Iterator of the components inside the container. + */ + public Iterator getComponentIterator() { + return new Iterator() { + int i = 0; + + public boolean hasNext() { + if (i < (firstComponent == null ? 0 : 1) + + (secondComponent == null ? 0 : 1)) { + return true; + } + return false; + } + + public Object next() { + if (!hasNext()) { + return null; + } + i++; + if (i == 1) { + return firstComponent == null ? secondComponent + : firstComponent; + } else if (i == 2) { + return secondComponent; + } + return null; + } + + public void remove() { + if (i == 1) { + if (firstComponent != null) { + setFirstComponent(null); + i = 0; + } else { + setSecondComponent(null); + } + } else if (i == 2) { + setSecondComponent(null); + } + } + }; + } + + /** + * 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 { + super.paintContent(target); + + final String position = pos + UNIT_SYMBOLS[posUnit]; + + target.addAttribute("position", position); + + if (isLocked()) { + target.addAttribute("locked", true); + } + + if (firstComponent != null) { + firstComponent.paint(target); + } else { + VerticalLayout temporaryComponent = new VerticalLayout(); + temporaryComponent.setParent(this); + temporaryComponent.paint(target); + } + if (secondComponent != null) { + secondComponent.paint(target); + } else { + VerticalLayout temporaryComponent = new VerticalLayout(); + temporaryComponent.setParent(this); + temporaryComponent.paint(target); + } + } + + /** + * Gets the orientation of the container. + * + * @return the Value of property orientation. + */ + public int getOrientation() { + return orientation; + } + + /** + * Set the orientation of the container. + * + * @param orientation + * the New value of property orientation. + */ + public void setOrientation(int orientation) { + + // Checks the validity of the argument + if (orientation < ORIENTATION_VERTICAL + || orientation > ORIENTATION_HORIZONTAL) { + throw new IllegalArgumentException(); + } + + this.orientation = orientation; + requestRepaint(); + } + + /* Documented in superclass */ + public void replaceComponent(Component oldComponent, Component newComponent) { + if (oldComponent == firstComponent) { + setFirstComponent(newComponent); + } else if (oldComponent == secondComponent) { + setSecondComponent(newComponent); + } + requestRepaint(); + } + + /** + * Moves the position of the splitter. + * + * @param pos + * the new size of the first region in persentage + */ + public void setSplitPosition(int pos) { + setSplitPosition(pos, UNITS_PERCENTAGE); + } + + /** + * Moves the position of the splitter with given position and unit. + * + * @param pos + * size of the first region + * @param unit + * the unit (from {@link Size}) in which the size is given. + */ + public void setSplitPosition(int pos, int unit) { + this.pos = pos; + posUnit = unit; + 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) { + this.locked = 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 locked; + } + + /* + * 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 variables) { + + super.changeVariables(source, variables); + + if (variables.containsKey("position") && !isLocked()) { + Integer newPos = (Integer) variables.get("position"); + // Client always sends pixel values + setSplitPosition(newPos, UNITS_PIXELS); + } + + } + +} diff --git a/src/com/vaadin/ui/TabSheet.java b/src/com/vaadin/ui/TabSheet.java new file mode 100644 index 0000000000..b0dac11767 --- /dev/null +++ b/src/com/vaadin/ui/TabSheet.java @@ -0,0 +1,790 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.io.Serializable; +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.Map; + +import com.vaadin.terminal.ErrorMessage; +import com.vaadin.terminal.KeyMapper; +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.PaintTarget; +import com.vaadin.terminal.Resource; +import com.vaadin.terminal.Paintable.RepaintRequestListener; + +/** + * Tabsheet component. + * + * @author IT Mill Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +@SuppressWarnings("serial") +public class TabSheet extends AbstractComponentContainer implements + RepaintRequestListener { + + /** + * Linked list of component tabs. + */ + private final LinkedList components = new LinkedList(); + + /** + * Map containing information related to the tabs (caption, icon etc). + */ + private final HashMap<Component, Tab> tabs = new HashMap<Component, Tab>(); + + /** + * Selected tab. + */ + private Component selected = null; + + private final KeyMapper keyMapper = new KeyMapper(); + + /** + * Holds the value of property tabsHIdden. + */ + private boolean tabsHidden; + + private LinkedList paintedTabs = new LinkedList(); + + /** + * Constructs a new Tabsheet. Tabsheet is immediate by default. + */ + public TabSheet() { + super(); + // expand horizontally by default + setWidth(100, UNITS_PERCENTAGE); + setImmediate(true); + } + + /** + * Gets the component container iterator for going trough all the components + * in the container. + * + * @return the Iterator of the components inside the container. + */ + public Iterator getComponentIterator() { + return java.util.Collections.unmodifiableList(components).iterator(); + } + + /** + * Removes the component from this container. + * + * @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()) { + selected = null; + } else { + selected = (Component) components.getFirst(); + fireSelectedTabChange(); + } + } + requestRepaint(); + } + } + + /** + * Adds a new tab into TabSheet. Components caption and icon are rendered + * into tab. + * + * @param c + * the component to be added. + */ + @Override + public void addComponent(Component c) { + addTab(c); + } + + /** + * Adds a new tab into TabSheet. + * + * @param c + * the component to be added onto tab. + * @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 tab + */ + public Tab addTab(Component c, String caption, Resource icon) { + if (c != null) { + components.addLast(c); + Tab tab = new TabSheetTabImpl(caption, icon); + + tabs.put(c, tab); + if (selected == null) { + selected = c; + fireSelectedTabChange(); + } + super.addComponent(c); + requestRepaint(); + return tab; + } else { + return null; + } + } + + /** + * Adds a new tab into TabSheet. Components caption and icon are rendered + * into tab. + * + * @param c + * the component to be added onto tab. + * @return the created tab + */ + public Tab addTab(Component c) { + if (c != null) { + return addTab(c, c.getCaption(), c.getIcon()); + } + return null; + } + + /** + * Gets the component UIDL tag. + * + * @return the Component UIDL tag as string. + */ + @Override + public String getTag() { + return "tabsheet"; + } + + /** + * Moves all components from another container to this container. The + * components are removed from the other container. + * + * @param source + * the container components are removed from. + */ + @Override + public void moveComponentsFrom(ComponentContainer source) { + for (final Iterator i = source.getComponentIterator(); i.hasNext();) { + final Component c = (Component) 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 event + * the Paint Event. + * @throws PaintException + * if the paint operation failed. + */ + @Override + public void paintContent(PaintTarget target) throws PaintException { + + if (areTabsHidden()) { + target.addAttribute("hidetabs", true); + } + + target.startTag("tabs"); + + for (final Iterator i = getComponentIterator(); i.hasNext();) { + final Component 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()) { + selected = component; + } else { + /* + * The current selection is not valid but this tab cannot be + * selected either. + */ + selected = null; + } + } + target.startTag("tab"); + if (!tab.isEnabled() && tab.isVisible()) { + target.addAttribute("disabled", true); + } + + if (!tab.isVisible()) { + target.addAttribute("hidden", true); + } + + final Resource icon = tab.getIcon(); + if (icon != null) { + target.addAttribute("icon", icon); + } + final String caption = tab.getCaption(); + if (caption != null && caption.length() > 0) { + target.addAttribute("caption", caption); + } + + final String description = tab.getDescription(); + if (description != null) { + target.addAttribute("description", description); + } + + final ErrorMessage componentError = tab.getComponentError(); + if (componentError != null) { + componentError.paint(target); + } + + target.addAttribute("key", keyMapper.key(component)); + if (component.equals(selected)) { + target.addAttribute("selected", true); + component.paint(target); + paintedTabs.add(component); + } else if (paintedTabs.contains(component)) { + component.paint(target); + } else { + component.requestRepaintRequests(); + } + target.endTag("tab"); + } + + target.endTag("tabs"); + + if (selected != null) { + target.addVariable(this, "selected", keyMapper.key(selected)); + } + } + + /** + * Are tabs hidden. + * + * @return the Property visibility. + */ + public boolean areTabsHidden() { + return tabsHidden; + } + + /** + * Setter for property tabsHidden. + * + * @param tabsHidden + * True if the tabs should be hidden. + */ + public void hideTabs(boolean tabsHidden) { + this.tabsHidden = tabsHidden; + requestRepaint(); + } + + /** + * Gets the caption for a component. + * + * @param c + * the component. + * @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 tabs captions. + * + * @param c + * the component. + * @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 component. + * + * @param c + * the component. + * @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. + * + * Normally TabSheet uses icon from component + * + * @param c + * the component + * @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 Tab for the component. The Tab object can be used for setting + * caption,icon, etc for the tab. + * + * @param c + * the component + * @return + */ + public Tab getTab(Component c) { + return tabs.get(c); + } + + /** + * Sets the selected tab. + * + * @param c + */ + public void setSelectedTab(Component c) { + if (c != null && components.contains(c) && !selected.equals(c)) { + selected = c; + fireSelectedTabChange(); + requestRepaint(); + } + } + + /** + * Gets the selected tab. + * + * @return the selected tab. + */ + public Component getSelectedTab() { + return selected; + } + + /** + * 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 variables) { + if (variables.containsKey("selected")) { + setSelectedTab((Component) keyMapper.get((String) variables + .get("selected"))); + } + } + + /* Documented in superclass */ + public void replaceComponent(Component oldComponent, Component newComponent) { + + if (selected == oldComponent) { + // keep selection w/o selectedTabChange event + selected = newComponent; + } + + Tab newTab = tabs.get(newComponent); + Tab oldTab = tabs.get(oldComponent); + + // Gets the captions + String oldCaption = null; + Resource oldIcon = null; + String newCaption = null; + Resource newIcon = null; + + if (oldTab != null) { + oldCaption = oldTab.getCaption(); + oldIcon = oldTab.getIcon(); + } + + if (newTab != null) { + newCaption = newTab.getCaption(); + newIcon = newTab.getIcon(); + } else { + newCaption = newComponent.getCaption(); + newIcon = newComponent.getIcon(); + } + + // Gets the locations + int oldLocation = -1; + int newLocation = -1; + int location = 0; + for (final Iterator i = components.iterator(); i.hasNext();) { + final Component 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); + keyMapper.remove(oldComponent); + newTab = addTab(newComponent); + components.remove(newComponent); + components.add(oldLocation, newComponent); + newTab.setCaption(oldCaption); + newTab.setIcon(oldIcon); + } 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); + } + + if (newTab != null) { + // This should always be true + newTab.setCaption(oldCaption); + newTab.setIcon(oldIcon); + } + if (oldTab != null) { + // This should always be true + oldTab.setCaption(newCaption); + oldTab.setIcon(newIcon); + } + + 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 thrown, when the selected tab in + * the tab sheet is changed. + * + * @author IT Mill 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 + * + * @author IT Mill Ltd. + * + * @version + * @VERSION@ + * @since 3.0 + */ + public interface SelectedTabChangeListener extends Serializable { + + /** + * Visible tab in tab sheet has has been changed. + * + * @param event + * the Selected tab change event. + */ + public void selectedTabChange(SelectedTabChangeEvent event); + } + + /** + * Adds the selected tab change listener + * + * @param listener + * the Listener to be added. + */ + public void addListener(SelectedTabChangeListener listener) { + addListener(SelectedTabChangeEvent.class, listener, + SELECTED_TAB_CHANGE_METHOD); + } + + /** + * Removes the selected tab change listener + * + * @param listener + * the Listener to be removed. + */ + public void removeListener(SelectedTabChangeListener listener) { + removeListener(SelectedTabChangeEvent.class, listener, + SELECTED_TAB_CHANGE_METHOD); + } + + /** + * Emits the options change event. + */ + protected void fireSelectedTabChange() { + fireEvent(new SelectedTabChangeEvent(this)); + } + + /* + * If child is not rendered on the client we need to repaint on child + * repaint due the way captions and icons are handled. + */ + public void repaintRequested(RepaintRequestEvent event) { + if (!paintedTabs.contains(event.getPaintable())) { + requestRepaint(); + } + } + + @Override + public void detach() { + super.detach(); + paintedTabs.clear(); + } + + /** + * + */ + public interface Tab extends Serializable { + /** + * Returns the visible status for the tab. + * + * @return true for visible, false for hidden + */ + public boolean isVisible(); + + /** + * Sets the visible status for the tab. + * + * @param visible + * true for visible, false for hidden + */ + public void setVisible(boolean visible); + + /** + * Returns the enabled status for the tab. + * + * @return true for enabled, false for disabled + */ + public boolean isEnabled(); + + /** + * Sets the enabled status for the tab. + * + * @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. + * + * @return the description for the tab + */ + public String getDescription(); + + /** + * Sets the description for the tab. + * + * @param description + * the new description string for the tab. + */ + public void setDescription(String description); + + public void setComponentError(ErrorMessage componentError); + + public ErrorMessage getComponentError(); + + } + + /** + * TabSheet's implementation of Tab + * + */ + public class TabSheetTabImpl implements Tab { + + private String caption = ""; + private Resource icon = null; + private boolean enabled = true; + private boolean visible = true; + private String description = null; + private ErrorMessage componentError = null; + + public TabSheetTabImpl(String caption, Resource icon) { + if (caption == null) { + caption = ""; + } + this.caption = caption; + this.icon = icon; + } + + /** + * Returns the tab caption. Can never be null. + */ + public String getCaption() { + return caption; + } + + public void setCaption(String caption) { + this.caption = caption; + requestRepaint(); + } + + public Resource getIcon() { + return icon; + } + + public void setIcon(Resource icon) { + this.icon = icon; + requestRepaint(); + } + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + requestRepaint(); + } + + public boolean isVisible() { + return visible; + } + + public void setVisible(boolean visible) { + this.visible = visible; + requestRepaint(); + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + requestRepaint(); + } + + public ErrorMessage getComponentError() { + return componentError; + } + + public void setComponentError(ErrorMessage componentError) { + this.componentError = componentError; + requestRepaint(); + } + + } +} diff --git a/src/com/vaadin/ui/Table.java b/src/com/vaadin/ui/Table.java new file mode 100644 index 0000000000..8df31ace01 --- /dev/null +++ b/src/com/vaadin/ui/Table.java @@ -0,0 +1,3222 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; +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 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.event.Action; +import com.vaadin.event.ItemClickEvent; +import com.vaadin.event.Action.Handler; +import com.vaadin.event.ItemClickEvent.ItemClickListener; +import com.vaadin.event.ItemClickEvent.ItemClickSource; +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.MouseEventDetails; + +/** + * <p> + * <code>TableComponent</code> is used for representing data or components in + * pageable and selectable table. + * </p> + * + * <p> + * Note! Since version 5, components in Table will not have their caption nor + * icon rendered. In order to workaround this limitation, wrap your component in + * a Layout. + * </p> + * + * @author IT Mill Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +@SuppressWarnings("serial") +public class Table extends AbstractSelect implements Action.Container, + Container.Ordered, Container.Sortable, ItemClickSource { + + private static final int CELL_KEY = 0; + + private static final int CELL_HEADER = 1; + + private static final int CELL_ICON = 2; + + private static final int CELL_ITEMID = 3; + + private static final int CELL_FIRSTCOL = 4; + + /** + * Left column alignment. <b>This is the default behaviour. </b> + */ + public static final String ALIGN_LEFT = "b"; + + /** + * Center column alignment. + */ + public static final String ALIGN_CENTER = "c"; + + /** + * Right column alignment. + */ + public static final String ALIGN_RIGHT = "e"; + + /** + * Column header mode: Column headers are hidden. <b>This is the default + * behavior. </b> + */ + public static final int COLUMN_HEADER_MODE_HIDDEN = -1; + + /** + * Column header mode: Property ID:s are used as column headers. + */ + public static final int COLUMN_HEADER_MODE_ID = 0; + + /** + * Column header mode: Column headers are explicitly specified with + * <code>setColumnHeaders</code>. + */ + public static final int COLUMN_HEADER_MODE_EXPLICIT = 1; + + /** + * Column header mode: Column headers are explicitly specified with + * <code>setColumnHeaders</code> + */ + public static final int COLUMN_HEADER_MODE_EXPLICIT_DEFAULTS_ID = 2; + + /** + * Row caption mode: The row headers are hidden. <b>This is the default + * mode. </b> + */ + public static final int ROW_HEADER_MODE_HIDDEN = -1; + + /** + * Row caption mode: Items Id-objects toString is used as row caption. + */ + public static final int ROW_HEADER_MODE_ID = AbstractSelect.ITEM_CAPTION_MODE_ID; + + /** + * Row caption mode: Item-objects toString is used as row caption. + */ + public static final int ROW_HEADER_MODE_ITEM = AbstractSelect.ITEM_CAPTION_MODE_ITEM; + + /** + * Row caption mode: Index of the item is used as item caption. The index + * mode can only be used with the containers implementing Container.Indexed + * interface. + */ + public static final int ROW_HEADER_MODE_INDEX = AbstractSelect.ITEM_CAPTION_MODE_INDEX; + + /** + * Row caption mode: Item captions are explicitly specified. + */ + public static final int ROW_HEADER_MODE_EXPLICIT = AbstractSelect.ITEM_CAPTION_MODE_EXPLICIT; + + /** + * Row caption mode: Item captions are read from property specified with + * <code>setItemCaptionPropertyId</code>. + */ + public static final int ROW_HEADER_MODE_PROPERTY = AbstractSelect.ITEM_CAPTION_MODE_PROPERTY; + + /** + * Row caption mode: Only icons are shown, the captions are hidden. + */ + public static final int ROW_HEADER_MODE_ICON_ONLY = AbstractSelect.ITEM_CAPTION_MODE_ICON_ONLY; + + /** + * Row caption mode: Item captions are explicitly specified, but if the + * caption is missing, the item id objects <code>toString()</code> is used + * instead. + */ + public static final int ROW_HEADER_MODE_EXPLICIT_DEFAULTS_ID = AbstractSelect.ITEM_CAPTION_MODE_EXPLICIT_DEFAULTS_ID; + + /* 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 columnIdMap = new KeyMapper(); + + /** + * Holds visible column propertyIds - in order. + */ + private LinkedList<Object> visibleColumns = new LinkedList<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 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, String> columnAlignments = new HashMap<Object, String>(); + + /** + * 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 int columnHeaderMode = COLUMN_HEADER_MODE_EXPLICIT_DEFAULTS_ID; + + /** + * True iff the row captions are hidden. + */ + private boolean rowCaptionsAreHidden = true; + + /** + * Page contents buffer used in buffered mode. + */ + private Object[][] pageBuffer = null; + + /** + * Set of properties listened - the list is kept to release the listeners + * later. + * + * Note: This should be set or list. IdentityHashMap used due very heavy + * hashCode in indexed container + */ + 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 actionMapper = null; + + /** + * Table cell editor factory. + */ + private FieldFactory fieldFactory = new BaseFieldFactory(); + + /** + * 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 disabled alltogether; even if some of the properties + * would be sortable. + */ + private boolean sortDisabled = false; + + /** + * 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; + + private int clickListenerCount; + + /* + * 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; + + /* 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"); + } + + // 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, null); + setColumnIcon(col, null); + } + } + } finally { + if (disabledHere) { + enableContentRefreshing(false); + } + } + } + + this.visibleColumns = newVC; + + // Assures visual refresh + resetPageBuffer(); + refreshRenderedCells(); + } + + /** + * 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 + * <code>COLUMN_HEADER_MODE_EXPLICIT</code> or + * <code>COLUMN_HEADER_MODE_EXPLICIT_DEFAULTS_ID</code> mode to show the + * headers. In the defaults mode any nulls in the headers array are replaced + * with id.toString() outputs when rendering. + * </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] = columnHeaders.get(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 + * <code>COLUMN_HEADER_MODE_EXPLICIT</code> or + * <code>COLUMN_HEADER_MODE_EXPLICIT_DEFAULTS_ID</code> 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 + * <code>getVisibleColumns</code> 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]); + } + + // Assures the visual refresh + resetPageBuffer(); + refreshRenderedCells(); + } + + /** + * 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 + * <code>COLUMN_HEADER_MODE_EXPLICIT</code> or + * <code>COLUMN_HEADER_MODE_EXPLICIT_DEFAULTS_ID</code> mode to show the + * headers with icons. + * </p> + * + * @return the Array of icons that match the <code>getVisibleColumns</code>. + */ + 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 + * <code>COLUMN_HEADER_MODE_EXPLICIT</code> or + * <code>COLUMN_HEADER_MODE_EXPLICIT_DEFAULTS_ID</code> mode to show the + * headers with icons. + * </p> + * + * @param columnIcons + * the Array of icons that match the + * <code>getVisibleColumns</code>. + */ + 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]); + } + + // Assure visual refresh + resetPageBuffer(); + refreshRenderedCells(); + } + + /** + * Gets the array of column alignments. + * + * <p> + * The items in the array must match the properties identified by + * <code>getVisibleColumns()</code>. The possible values for the alignments + * include: + * <ul> + * <li><code>ALIGN_LEFT</code>: Left alignment</li> + * <li><code>ALIGN_CENTER</code>: Centered</li> + * <li><code>ALIGN_RIGHT</code>: Right alignment</li> + * </ul> + * The alignments default to <code>ALIGN_LEFT</code>: any null values are + * rendered as align lefts. + * </p> + * + * @return the Column alignments array. + */ + public String[] getColumnAlignments() { + if (columnAlignments == null) { + return null; + } + final String[] alignments = new String[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 items in the array must match the properties identified by + * <code>getVisibleColumns()</code>. The possible values for the alignments + * include: + * <ul> + * <li><code>ALIGN_LEFT</code>: Left alignment</li> + * <li><code>ALIGN_CENTER</code>: Centered</li> + * <li><code>ALIGN_RIGHT</code>: Right alignment</li> + * </ul> + * The alignments default to <code>ALIGN_LEFT</code> + * </p> + * + * @param columnAlignments + * the Column alignments array. + */ + public void setColumnAlignments(String[] columnAlignments) { + + if (columnAlignments.length != visibleColumns.size()) { + throw new IllegalArgumentException( + "The length of the alignments array must match the number of visible columns"); + } + + // Checks all alignments + for (int i = 0; i < columnAlignments.length; i++) { + final String a = columnAlignments[i]; + if (a != null && !a.equals(ALIGN_LEFT) && !a.equals(ALIGN_CENTER) + && !a.equals(ALIGN_RIGHT)) { + throw new IllegalArgumentException("Column " + i + + " aligment '" + a + "' is invalid"); + } + } + + // Resets the alignments + final HashMap<Object, String> newCA = new HashMap<Object, String>(); + 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 + resetPageBuffer(); + 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 columnId + * colunmns property id + * @param width + * width to be reserved for colunmns content + * @since 4.0.3 + */ + public void setColumnWidth(Object columnId, int width) { + if (width < 0) { + columnWidths.remove(columnId); + } else { + columnWidths.put(columnId, new Integer(width)); + } + } + + /** + * 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-sizeable columns the column + * becomes fixed width column if users resizes the column. + * + * @param columnId + * colunmns property id + * @param expandRatio + * the expandRatio used to divide excess space for this column + */ + public void setColumnExpandRatio(Object columnId, float expandRatio) { + if (expandRatio < 0) { + columnWidths.remove(columnId); + } else { + columnWidths.put(columnId, 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 colun or -1 when value not set + */ + public int getColumnWidth(Object propertyId) { + 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> + * + * @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 + resetPageBuffer(); + refreshRenderedCells(); + } + } + + /** + * 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 = ((Container.Indexed) items).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 = ((Container.Ordered) items).firstItemId(); + } + + return currentPageFirstItemId; + } + + /** + * 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 = ((Container.Indexed) items) + .indexOfId(currentPageFirstItemId); + } else { + // If the table item container does not have index, we have to + // calculates the index by hand + Object id = ((Container.Ordered) items).firstItemId(); + while (id != null && !id.equals(currentPageFirstItemId)) { + index++; + id = ((Container.Ordered) items).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) { + setCurrentPageFirstItemIndex(maxIndex); + return; + } + + this.currentPageFirstItemId = currentPageFirstItemId; + currentPageFirstItemIndex = index; + } + + // Assures the visual refresh + resetPageBuffer(); + refreshRenderedCells(); + + } + + /** + * 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); + } + + // Assures the visual refresh + resetPageBuffer(); + refreshRenderedCells(); + } + + /** + * Gets the header for the specified column. + * + * @param propertyId + * the propertyId indentifying the column. + * @return the header for the specifed column if it has one. + */ + public String getColumnHeader(Object propertyId) { + if (getColumnHeaderMode() == COLUMN_HEADER_MODE_HIDDEN) { + return null; + } + + String header = columnHeaders.get(propertyId); + if ((header == null && getColumnHeaderMode() == COLUMN_HEADER_MODE_EXPLICIT_DEFAULTS_ID) + || getColumnHeaderMode() == COLUMN_HEADER_MODE_ID) { + header = propertyId.toString(); + } + + return header; + } + + /** + * Sets the column header for the specified column; + * + * @param propertyId + * the propertyId indentifying the column. + * @param header + * the header to set. + */ + public void setColumnHeader(Object propertyId, String header) { + + if (header == null) { + columnHeaders.remove(propertyId); + return; + } + columnHeaders.put(propertyId, header); + + // Assures the visual refresh + refreshRenderedCells(); + } + + /** + * 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 String getColumnAlignment(Object propertyId) { + final String 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: ALIGN_LEFT, ALIGN_CENTER or ALIGN_RIGHT + * </p> + * + * @param propertyId + * the propertyID identifying the column. + * @param alignment + * the desired alignment. + */ + public void setColumnAlignment(Object propertyId, String alignment) { + + // Checks for valid alignments + if (alignment != null && !alignment.equals(ALIGN_LEFT) + && !alignment.equals(ALIGN_CENTER) + && !alignment.equals(ALIGN_RIGHT)) { + throw new IllegalArgumentException("Column alignment '" + alignment + + "' is not supported."); + } + + if (alignment == null || alignment.equals(ALIGN_LEFT)) { + columnAlignments.remove(propertyId); + return; + } + + columnAlignments.put(propertyId, alignment); + + // Assures the visual refresh + 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 IllegalAccessException + */ + public void setColumnCollapsed(Object propertyId, boolean collapsed) + throws IllegalAccessException { + if (!isColumnCollapsingAllowed()) { + throw new IllegalAccessException("Column collapsing not allowed!"); + } + + if (collapsed) { + collapsedColumns.add(propertyId); + } else { + collapsedColumns.remove(propertyId); + } + + // Assures the visual refresh + resetPageBuffer(); + refreshRenderedCells(); + } + + /** + * 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 + refreshRenderedCells(); + } + + /** + * 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 reorderingAllowed + * specifies whether column reordering is allowed. + */ + public void setColumnReorderingAllowed(boolean reorderingAllowed) { + columnReorderingAllowed = reorderingAllowed; + + // Assures the visual refresh + refreshRenderedCells(); + } + + /* + * 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 + resetPageBuffer(); + refreshRenderedCells(); + } + + /** + * Getter for property currentPageFirstItem. + * + * @return the Value of property currentPageFirstItem. + */ + public int getCurrentPageFirstItemIndex() { + return currentPageFirstItemIndex; + } + + private 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; + } + + // Ensures that the new value is valid + if (newIndex > maxIndex) { + newIndex = maxIndex; + } + + // Refresh first item id + if (items instanceof Container.Indexed) { + try { + currentPageFirstItemId = ((Container.Indexed) items) + .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 = ((Container.Ordered) items).firstItemId(); + + // Go forwards in the middle of the list (respect borders) + while (currentPageFirstItemIndex < newIndex + && !((Container.Ordered) items) + .isLastId(currentPageFirstItemId)) { + currentPageFirstItemIndex++; + currentPageFirstItemId = ((Container.Ordered) items) + .nextItemId(currentPageFirstItemId); + } + + // If we did hit the border + if (((Container.Ordered) items).isLastId(currentPageFirstItemId)) { + currentPageFirstItemIndex = size - 1; + } + + // Go backwards in the middle of the list (respect borders) + while (currentPageFirstItemIndex > newIndex + && !((Container.Ordered) items) + .isFirstId(currentPageFirstItemId)) { + currentPageFirstItemIndex--; + currentPageFirstItemId = ((Container.Ordered) items) + .prevItemId(currentPageFirstItemId); + } + + // If we did hit the border + if (((Container.Ordered) items).isFirstId(currentPageFirstItemId)) { + currentPageFirstItemIndex = 0; + } + + // Go forwards once more + while (currentPageFirstItemIndex < newIndex + && !((Container.Ordered) items) + .isLastId(currentPageFirstItemId)) { + currentPageFirstItemIndex++; + currentPageFirstItemId = ((Container.Ordered) items) + .nextItemId(currentPageFirstItemId); + } + + // If for some reason we do hit border again, override + // the user index request + if (((Container.Ordered) items).isLastId(currentPageFirstItemId)) { + newIndex = currentPageFirstItemIndex = size - 1; + } + } + if (needsPageBufferReset) { + // Assures the visual refresh + resetPageBuffer(); + refreshRenderedCells(); + } + } + + /** + * 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 int getColumnHeaderMode() { + return columnHeaderMode; + } + + /** + * Setter for property columnHeaderMode. + * + * @param columnHeaderMode + * the New value of property columnHeaderMode. + */ + public void setColumnHeaderMode(int columnHeaderMode) { + if (columnHeaderMode >= COLUMN_HEADER_MODE_HIDDEN + && columnHeaderMode <= COLUMN_HEADER_MODE_EXPLICIT_DEFAULTS_ID) { + this.columnHeaderMode = columnHeaderMode; + } + + // Assures the visual refresh + refreshRenderedCells(); + } + + /** + * Refreshes rendered rows + */ + private void refreshRenderedCells() { + if (getParent() == null) { + return; + } + + if (isContentRefreshesEnabled) { + + HashSet<Property> oldListenedProperties = listenedProperties; + HashSet<Component> oldVisibleComponents = visibleComponents; + + // initialize the listener collections + listenedProperties = new HashSet<Property>(); + visibleComponents = new HashSet<Component>(); + + // Collects the basic facts about the table page + final Object[] colids = getVisibleColumns(); + final int cols = colids.length; + final int pagelen = getPageLength(); + int firstIndex = getCurrentPageFirstItemIndex(); + int rows, totalRows; + rows = totalRows = size(); + 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; + } + Object id; + if (firstToBeRenderedInClient >= 0) { + if (firstToBeRenderedInClient < totalRows) { + firstIndex = firstToBeRenderedInClient; + } else { + firstIndex = totalRows - 1; + } + } else { + // initial load + firstToBeRenderedInClient = firstIndex; + } + if (totalRows > 0) { + if (rows + firstIndex > totalRows) { + rows = totalRows - firstIndex; + } + } else { + rows = 0; + } + + Object[][] cells = new Object[cols + CELL_FIRSTCOL][rows]; + if (rows == 0) { + pageBuffer = cells; + unregisterPropertiesAndComponents(oldListenedProperties, + oldVisibleComponents); + return; + } + + // Gets the first item id + if (items instanceof Container.Indexed) { + id = ((Container.Indexed) items).getIdByIndex(firstIndex); + } else { + id = ((Container.Ordered) items).firstItemId(); + for (int i = 0; i < firstIndex; i++) { + id = ((Container.Ordered) items).nextItemId(id); + } + } + + final int 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 ROW_HEADER_MODE_INDEX: + cells[CELL_HEADER][i] = String.valueOf(i + firstIndex + + 1); + break; + default: + cells[CELL_HEADER][i] = getItemCaption(id); + } + cells[CELL_ICON][i] = getItemIcon(id); + } + + if (cols > 0) { + for (int j = 0; j < cols; j++) { + if (isColumnCollapsed(colids[j])) { + continue; + } + Property p = null; + Object value = ""; + boolean isGenerated = columnGenerators + .containsKey(colids[j]); + + if (!isGenerated) { + p = getContainerProperty(id, colids[j]); + } + + // check in current pageBuffer already has row + int index = firstIndex + i; + if (p != null || isGenerated) { + if (index < firstIndexNotInCache + && index >= pageBufferFirstIndex) { + // we have data already in our cache, + // recycle it instead of fetching it via + // getValue/getPropertyValue + int indexInOldBuffer = index + - pageBufferFirstIndex; + value = pageBuffer[CELL_FIRSTCOL + j][indexInOldBuffer]; + } else { + if (isGenerated) { + ColumnGenerator cg = columnGenerators + .get(colids[j]); + value = cg + .generateCell(this, id, colids[j]); + + } else if (iscomponent[j]) { + value = p.getValue(); + } 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) + && p instanceof Property.ValueChangeNotifier) { + // only add listener to property once + 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); + } + } else { + value = getPropertyValue(id, colids[j], + null); + } + } + } + + if (value instanceof Component) { + if (oldVisibleComponents == null + || !oldVisibleComponents.contains(value)) { + ((Component) value).setParent(this); + } + visibleComponents.add((Component) value); + } + cells[CELL_FIRSTCOL + j][i] = value; + } + } + + id = ((Container.Ordered) items).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; + } + + pageBufferFirstIndex = firstIndex; + + // Saves the results to internal buffer + pageBuffer = cells; + + unregisterPropertiesAndComponents(oldListenedProperties, + oldVisibleComponents); + + requestRepaint(); + } + + } + + /** + * 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)) { + c.setParent(null); + } + } + } + + 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); + } + } + } + } + + /** + * 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><code>ROW_HEADER_MODE_HIDDEN</code>: The row captions are hidden.</li> + * <li><code>ROW_HEADER_MODE_ID</code>: Items Id-objects + * <code>toString()</code> is used as row caption. + * <li><code>ROW_HEADER_MODE_ITEM</code>: Item-objects + * <code>toString()</code> is used as row caption. + * <li><code>ROW_HEADER_MODE_PROPERTY</code>: Property set with + * <code>setItemCaptionPropertyId()</code> is used as row header. + * <li><code>ROW_HEADER_MODE_EXPLICIT_DEFAULTS_ID</code>: Items Id-objects + * <code>toString()</code> is used as row header. If caption is explicitly + * specified, it overrides the id-caption. + * <li><code>ROW_HEADER_MODE_EXPLICIT</code>: The row headers must be + * explicitly specified.</li> + * <li><code>ROW_HEADER_MODE_INDEX</code>: 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 <code>ROW_HEADER_MODE_HIDDEN</code> + * </p> + * + * @param mode + * the One of the modes listed above. + */ + public void setRowHeaderMode(int mode) { + if (ROW_HEADER_MODE_HIDDEN == mode) { + rowCaptionsAreHidden = true; + } else { + rowCaptionsAreHidden = false; + setItemCaptionMode(mode); + } + + // Assure visual refresh + refreshRenderedCells(); + } + + /** + * Gets the row header mode. + * + * @return the Row header mode. + * @see #setRowHeaderMode(int) + */ + public int getRowHeaderMode() { + return rowCaptionsAreHidden ? ROW_HEADER_MODE_HIDDEN + : getItemCaptionMode(); + } + + /** + * 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)) { + resetPageBuffer(); + refreshRenderedCells(); + } + + return itemId; + } + + /* Overriding select behavior */ + + @Override + public void setValue(Object newValue) throws ReadOnlyException, + ConversionException { + // external selection change, need to truncate pageBuffer + resetPageBuffer(); + refreshRenderedCells(); + super.setValue(newValue); + } + + /** + * Sets the Container that serves as the data source of the viewer. + * + * As a side-effect Table's value (selection) is set to null due old + * selection not necessary exists in new Container. + * + * @see com.vaadin.data.Container.Viewer#setContainerDataSource(Container) + */ + @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()); + + // null value as we may not be sure that currently selected identifier + // exits in new ds + setValue(null); + + // Assure visual refresh + resetPageBuffer(); + + enableContentRefreshing(true); + + } + + /* 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 variables) { + + boolean clientNeedsContentRefresh = false; + + handleClickEvent(variables); + + disableContentRefreshing(); + + if (!isSelectable() && variables.containsKey("selected")) { + // Not-selectable is a special case, AbstractSelect does not support + // TODO could be optimized. + variables = new HashMap(variables); + variables.remove("selected"); + } + + super.changeVariables(source, variables); + + // 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 + e.printStackTrace(); + } + + // 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; + } + } + } + clientNeedsContentRefresh = true; + } + + // 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 = (Action) actionMapper.get(st.nextToken()); + if (action != null && containsId(itemId) + && actionHandlers != null) { + for (final Iterator<Handler> i = actionHandlers.iterator(); i + .hasNext();) { + (i.next()).handleAction(action, this, itemId); + } + } + } + } + + if (!sortDisabled) { + // 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 + e.printStackTrace(); + } + clientNeedsContentRefresh = true; + } + } + if (isColumnReorderingAllowed()) { + if (variables.containsKey("columnorder")) { + try { + final Object[] ids = (Object[]) variables + .get("columnorder"); + for (int i = 0; i < ids.length; i++) { + ids[i] = columnIdMap.get(ids[i].toString()); + } + setColumnOrder(ids); + } catch (final Exception e) { + // FIXME: Handle exception + e.printStackTrace(); + + } + clientNeedsContentRefresh = true; + } + } + + enableContentRefreshing(clientNeedsContentRefresh); + } + + /** + * Handles click event + * + * @param variables + */ + private void handleClickEvent(Map variables) { + if (clickListenerCount > 0) { + 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)); + } + } + } + } + + /** + * 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 { + + // The tab ordering number + if (getTabIndex() > 0) { + target.addAttribute("tabindex", getTabIndex()); + } + + // Initialize temps + final Object[] colids = getVisibleColumns(); + final int cols = colids.length; + final int first = getCurrentPageFirstItemIndex(); + int total = size(); + final int pagelen = getPageLength(); + final int colHeadMode = getColumnHeaderMode(); + final boolean colheads = colHeadMode != COLUMN_HEADER_MODE_HIDDEN; + final boolean rowheads = getRowHeaderMode() != ROW_HEADER_MODE_HIDDEN; + final Object[][] cells = getVisibleCells(); + final boolean iseditable = isEditable(); + 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); + } + } + + if (!isNullSelectionAllowed() && getNullSelectionItemId() != null + && containsId(getNullSelectionItemId())) { + total--; + rows--; + } + + // selection support + LinkedList<String> selectedKeys = new LinkedList<String>(); + if (isMultiSelect()) { + // only paint selections that are currently visible in the client + HashSet sel = new HashSet((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)); + } + } + + // Table attributes + if (isSelectable()) { + target.addAttribute("selectmode", (isMultiSelect() ? "multi" + : "single")); + } else { + target.addAttribute("selectmode", "none"); + } + + if (clickListenerCount > 0) { + target.addAttribute("listenClicks", true); + } + + target.addAttribute("cols", cols); + target.addAttribute("rows", rows); + + target.addAttribute("firstrow", + (reqFirstRowToPaint >= 0 ? reqFirstRowToPaint + : firstToBeRenderedInClient)); + target.addAttribute("totalrows", total); + if (pagelen != 0) { + target.addAttribute("pagelength", pagelen); + } + if (colheads) { + target.addAttribute("colheaders", true); + } + if (rowheads) { + target.addAttribute("rowheaders", true); + } + + // Visible column order + final Collection sortables = getSortableContainerPropertyIds(); + final ArrayList<String> visibleColOrder = new ArrayList<String>(); + for (final Iterator<Object> it = visibleColumns.iterator(); it + .hasNext();) { + final Object columnId = it.next(); + if (!isColumnCollapsed(columnId)) { + visibleColOrder.add(columnIdMap.key(columnId)); + } + } + target.addAttribute("vcolorder", visibleColOrder.toArray()); + + // Rows + final Set<Action> actionSet = new LinkedHashSet<Action>(); + final boolean selectable = isSelectable(); + final boolean[] iscomponent = new boolean[visibleColumns.size()]; + int iscomponentIndex = 0; + for (final Iterator<Object> it = visibleColumns.iterator(); it + .hasNext() + && iscomponentIndex < iscomponent.length;) { + final Object columnId = it.next(); + if (columnGenerators.containsKey(columnId)) { + iscomponent[iscomponentIndex++] = true; + } else { + final Class colType = getType(columnId); + iscomponent[iscomponentIndex++] = colType != null + && Component.class.isAssignableFrom(colType); + } + } + 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; + } + + for (int i = start; i < end; i++) { + final Object itemId = cells[CELL_ITEMID][i]; + + if (!isNullSelectionAllowed() && getNullSelectionItemId() != null + && itemId == getNullSelectionItemId()) { + // Remove null selection item if null selection is not allowed + continue; + } + + target.startTag("tr"); + + // tr attributes + if (rowheads) { + if (cells[CELL_ICON][i] != null) { + target.addAttribute("icon", (Resource) cells[CELL_ICON][i]); + } + if (cells[CELL_HEADER][i] != null) { + target.addAttribute("caption", + (String) cells[CELL_HEADER][i]); + } + } + target.addAttribute("key", Integer.parseInt(cells[CELL_KEY][i] + .toString())); + if (actionHandlers != null || isSelectable()) { + if (isSelected(itemId)) { + target.addAttribute("selected", true); + } + } + + // Actions + if (actionHandlers != null) { + final ArrayList<String> keys = new ArrayList<String>(); + for (final Iterator<Handler> ahi = actionHandlers.iterator(); ahi + .hasNext();) { + final Action[] aa = (ahi.next()).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); + } + } + + // 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) + && Component.class.isInstance(cells[CELL_FIRSTCOL + + currentColumn][i])) { + final Component c = (Component) cells[CELL_FIRSTCOL + + currentColumn][i]; + if (c == null) { + target.addText(""); + } else { + c.paint(target); + } + } else { + target + .addText((String) cells[CELL_FIRSTCOL + + currentColumn][i]); + } + } + + target.endTag("tr"); + } + target.endTag("rows"); + + // The select variable is only enabled if selectable + if (selectable && selectedKeys.size() > 0) { + target.addVariable(this, "selected", selectedKeys + .toArray(new String[selectedKeys.size()])); + } + + // The cursors are only shown on pageable table + if (first != 0 || getPageLength() > 0) { + target.addVariable(this, "firstvisible", first); + } + + // Sorting + if (getContainerDataSource() instanceof Container.Sortable) { + target.addVariable(this, "sortcolumn", columnIdMap + .key(sortContainerPropertyId)); + target.addVariable(this, "sortascending", sortAscending); + } + + // Resets and paints "to be painted next" variables. Also reset + // pageBuffer + reqFirstRowToPaint = -1; + reqRowsToPaint = -1; + containerChangeToBeRendered = false; + target.addVariable(this, "reqrows", reqRowsToPaint); + target.addVariable(this, "reqfirstrow", reqFirstRowToPaint); + + // Actions + if (!actionSet.isEmpty()) { + target.addVariable(this, "action", ""); + target.startTag("actions"); + for (final Iterator<Action> it = actionSet.iterator(); it.hasNext();) { + final Action a = it.next(); + 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"); + } + if (columnReorderingAllowed) { + final String[] colorder = new String[visibleColumns.size()]; + int i = 0; + for (final Iterator<Object> it = visibleColumns.iterator(); it + .hasNext() + && i < colorder.length;) { + colorder[i++] = columnIdMap.key(it.next()); + } + target.addVariable(this, "columnorder", colorder); + } + // Available columns + if (columnCollapsingAllowed) { + final HashSet<Object> ccs = new HashSet<Object>(); + for (final Iterator<Object> i = visibleColumns.iterator(); i + .hasNext();) { + final Object o = i.next(); + if (isColumnCollapsed(o)) { + ccs.add(o); + } + } + final String[] collapsedkeys = new String[ccs.size()]; + int nextColumn = 0; + for (final Iterator<Object> it = visibleColumns.iterator(); it + .hasNext() + && nextColumn < collapsedkeys.length;) { + final Object columnId = it.next(); + if (isColumnCollapsed(columnId)) { + collapsedkeys[nextColumn++] = columnIdMap.key(columnId); + } + } + target.addVariable(this, "collapsedcolumns", collapsedkeys); + } + target.startTag("visiblecolumns"); + int i = 0; + for (final Iterator<Object> it = visibleColumns.iterator(); it + .hasNext(); i++) { + final Object columnId = it.next(); + if (columnId != null) { + target.startTag("column"); + target.addAttribute("cid", columnIdMap.key(columnId)); + final String head = getColumnHeader(columnId); + target.addAttribute("caption", (head != null ? head : "")); + if (isColumnCollapsed(columnId)) { + target.addAttribute("collapsed", true); + } + if (colheads) { + if (getColumnIcon(columnId) != null) { + target.addAttribute("icon", getColumnIcon(columnId)); + } + if (sortables.contains(columnId)) { + target.addAttribute("sortable", true); + } + } + if (!ALIGN_LEFT.equals(getColumnAlignment(columnId))) { + target.addAttribute("align", getColumnAlignment(columnId)); + } + if (columnWidths.containsKey(columnId)) { + if (getColumnWidth(columnId) > -1) { + target.addAttribute("width", String + .valueOf(getColumnWidth(columnId))); + } else { + target.addAttribute("er", + getColumnExpandRatio(columnId)); + } + } + target.endTag("column"); + } + } + target.endTag("visiblecolumns"); + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.AbstractSelect#getTag() + */ + @Override + public String getTag() { + return "table"; + } + + /** + * 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 #setFieldFactory(FieldFactory) + */ + 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) { + f.setPropertyDataSource(property); + return f; + } + } + + return formatPropertyValue(rowId, colId, 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 ""; + } + return property.toString(); + } + + /* Action container */ + + /** + * Registers a new action handler for this container + * + * @see com.vaadin.event.Action.Container#addActionHandler(Action.Handler) + */ + public void addActionHandler(Action.Handler actionHandler) { + + if (actionHandler != null) { + + if (actionHandlers == null) { + actionHandlers = new LinkedList<Handler>(); + actionMapper = new KeyMapper(); + } + + if (!actionHandlers.contains(actionHandler)) { + actionHandlers.add(actionHandler); + requestRepaint(); + } + + } + } + + /** + * Removes a previously registered action handler for the contents of this + * container. + * + * @see com.vaadin.event.Action.Container#removeActionHandler(Action.Handler) + */ + public void removeActionHandler(Action.Handler actionHandler) { + + if (actionHandlers != null && actionHandlers.contains(actionHandler)) { + + actionHandlers.remove(actionHandler); + + if (actionHandlers.isEmpty()) { + actionHandlers = null; + actionMapper = null; + } + + requestRepaint(); + } + } + + /* 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) { + super.valueChange(event); + } else { + resetPageBuffer(); + refreshRenderedCells(); + containerChangeToBeRendered = true; + } + requestRepaint(); + } + + private 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(); + + if (visibleComponents != null) { + for (final Iterator<Component> i = visibleComponents.iterator(); i + .hasNext();) { + i.next().attach(); + } + } + } + + /** + * Notifies the component that it is detached from the application + * + * @see com.vaadin.ui.Component#detach() + */ + @Override + public void detach() { + super.detach(); + + if (visibleComponents != null) { + for (final Iterator<Component> i = visibleComponents.iterator(); i + .hasNext();) { + i.next().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 = ((Container.Ordered) items) + .nextItemId(itemId); + final boolean ret = super.removeItem(itemId); + if (ret && (itemId != null) && (itemId.equals(currentPageFirstItemId))) { + currentPageFirstItemId = nextItemId; + } + if (!(items instanceof Container.ItemSetChangeNotifier)) { + resetPageBuffer(); + refreshRenderedCells(); + } + 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); + + 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)) { + resetPageBuffer(); + refreshRenderedCells(); + } + 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, + String 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> + * 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); + } + resetPageBuffer(); + refreshRenderedCells(); + } + } + + /** + * 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); + } + resetPageBuffer(); + refreshRenderedCells(); + return true; + } else { + return false; + } + } + + /** + * Returns the list of items on the current page + * + * @see com.vaadin.ui.Select#getVisibleItemIds() + */ + @Override + public Collection getVisibleItemIds() { + + final LinkedList<Object> visible = new LinkedList<Object>(); + + final Object[][] cells = getVisibleCells(); + 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); + if (event instanceof IndexedContainer.ItemSetChangeEvent) { + IndexedContainer.ItemSetChangeEvent evt = (IndexedContainer.ItemSetChangeEvent) event; + // if the event is not a global one and the added item is outside + // the visible/buffered area, no need to do anything + if (evt.getAddedItemIndex() != -1 + && (firstToBeRenderedInClient >= 0) + && (lastToBeRenderedInClient >= 0) + && (firstToBeRenderedInClient > evt.getAddedItemIndex() || lastToBeRenderedInClient < evt + .getAddedItemIndex())) { + return; + } + } + // ensure that page still has first item in page + setCurrentPageFirstItemIndex(getCurrentPageFirstItemIndex()); + + resetPageBuffer(); + refreshRenderedCells(); + } + + /** + * 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) { + super.containerPropertySetChange(event); + + resetPageBuffer(); + refreshRenderedCells(); + } + + /** + * 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(); + } + } + + /** + * Focusing to this component is not supported. + * + * @throws UnsupportedOperationException + * if invoked. + * @see com.vaadin.ui.AbstractField#focus() + */ + @Override + public void focus() throws UnsupportedOperationException { + 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) + */ + 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) + */ + 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() + */ + 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() + */ + 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) + */ + 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) + */ + 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) + */ + public Object addItemAfter(Object previousItemId) + throws UnsupportedOperationException { + Object itemId = ((Container.Ordered) items) + .addItemAfter(previousItemId); + if (!(items instanceof Container.ItemSetChangeNotifier)) { + resetPageBuffer(); + refreshRenderedCells(); + } + return itemId; + } + + /** + * Adds new item after the given item. + * + * @see com.vaadin.data.Container.Ordered#addItemAfter(java.lang.Object, + * java.lang.Object) + */ + public Item addItemAfter(Object previousItemId, Object newItemId) + throws UnsupportedOperationException { + Item item = ((Container.Ordered) items).addItemAfter(previousItemId, + newItemId); + if (!(items instanceof Container.ItemSetChangeNotifier)) { + resetPageBuffer(); + refreshRenderedCells(); + } + return item; + } + + /** + * Gets the FieldFactory that is used to create editor for table cells. + * + * The FieldFactory is only used if the Table is editable. + * + * @return FieldFactory used to create the Field instances. + * @see #isEditable + */ + public FieldFactory getFieldFactory() { + return fieldFactory; + } + + /** + * Sets the FieldFactory that is used to create editor for table cells. + * + * The FieldFactory is only used if the Table is editable. By default the + * BaseFieldFactory is used. + * + * @param fieldFactory + * the field factory to set. + * @see #isEditable + * @see BaseFieldFactory + * + */ + public void setFieldFactory(FieldFactory fieldFactory) { + this.fieldFactory = fieldFactory; + + // Assure visual refresh + resetPageBuffer(); + refreshRenderedCells(); + } + + /** + * 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 + resetPageBuffer(); + refreshRenderedCells(); + } + + /** + * 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[]) + * + */ + 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); + resetPageBuffer(); + refreshRenderedCells(); + + } 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. + * + * @see com.vaadin.data.Container.Sortable#getSortableContainerPropertyIds() + */ + public Collection getSortableContainerPropertyIds() { + final Container c = getContainerDataSource(); + if (c instanceof Container.Sortable && !isSortDisabled()) { + return ((Container.Sortable) c).getSortableContainerPropertyIds(); + } else { + return new LinkedList(); + } + } + + /** + * 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 + 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 + 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. + */ + public boolean isSortDisabled() { + return sortDisabled; + } + + /** + * Disables the sorting altogether. + * + * To disable sorting altogether, set to true. In this case no sortable + * columns are given even in the case where datasource would support this. + * + * @param sortDisabled + * True iff sorting is disabled. + */ + public void setSortDisabled(boolean sortDisabled) { + if (this.sortDisabled != sortDisabled) { + this.sortDisabled = sortDisabled; + refreshRenderedCells(); + } + } + + /** + * Table 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 Table."); + } + } + + /* + * Override abstract fields to string method to avoid non-informative null's + * in debugger + */ + @Override + public String toString() { + return "Table:" + getContainerPropertyIds() + ", rows " + + getContainerDataSource().size() + " ,value:" + + super.toString(); + } + + /** + * 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 + */ + public abstract Component 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; + requestRepaint(); + } + + /** + * 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>i-table-cell-content-[style name]</tt>, and + * the row style will be <tt>i-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 i-table-cell-content-[style name], or + * i-table-row-[style name] for rows) + */ + public abstract String getStyle(Object itemId, Object propertyId); + } + + public void addListener(ItemClickListener listener) { + addListener(ItemClickEvent.class, listener, + ItemClickEvent.ITEM_CLICK_METHOD); + clickListenerCount++; + // repaint needed only if click listening became necessary + if (clickListenerCount == 1) { + requestRepaint(); + } + + } + + public void removeListener(ItemClickListener listener) { + removeListener(ItemClickEvent.class, listener, + ItemClickEvent.ITEM_CLICK_METHOD); + clickListenerCount--; + // repaint needed only if click listening is not needed in client + // anymore + if (clickListenerCount == 0) { + requestRepaint(); + } + } + + // 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(); + } + } + + // Virtually identical to AbstractCompoenentContainer.setEnabled(); + public void requestRepaintAll() { + requestRepaint(); + if (visibleComponents != null) { + for (Iterator<Component> childIterator = visibleComponents + .iterator(); childIterator.hasNext();) { + Component c = childIterator.next(); + if (c instanceof Form) { + // Form has children in layout, but is not + // ComponentContainer + c.requestRepaint(); + ((Form) c).getLayout().requestRepaintAll(); + } else if (c instanceof Table) { + ((Table) c).requestRepaintAll(); + } else if (c instanceof ComponentContainer) { + ((ComponentContainer) c).requestRepaintAll(); + } else { + c.requestRepaint(); + } + } + } + } +} diff --git a/src/com/vaadin/ui/TextField.java b/src/com/vaadin/ui/TextField.java new file mode 100644 index 0000000000..626d340922 --- /dev/null +++ b/src/com/vaadin/ui/TextField.java @@ -0,0 +1,556 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +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; + +/** + * <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 IT Mill Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +@SuppressWarnings("serial") +public class TextField extends AbstractField { + + /* Private members */ + + /** + * Value formatter used to format the string contents. + */ + private Format format; + + /** + * Number of visible columns in the TextField. + */ + private int columns = 0; + + /** + * Number of visible rows in a multiline TextField. Value 0 implies a + * single-line text-editor. + */ + private int rows = 0; + + /** + * Tells if word-wrapping should be used in multiline mode. + */ + private boolean wordwrap = true; + + /** + * Tells if input is used to enter sensitive information that is not echoed + * to display. Typically passwords. + */ + private boolean secret = false; + + /** + * Null representation. + */ + private String nullRepresentation = "null"; + + /** + * Is setting to null from non-null value allowed by setting with null + * representation . + */ + private boolean nullSettingAllowed = false; + + private String inputPrompt = null; + + /** + * Maximum character count in text field. + */ + private int maxLength = -1; + + /* Constructors */ + + /** + * 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) { + setValue(""); + 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 text + * the initial text content of the editor. + */ + public TextField(String caption, String 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 { + super.paintContent(target); + + // Sets the secret attribute + if (isSecret()) { + target.addAttribute("secret", true); + } + + if (getMaxLength() >= 0) { + target.addAttribute("maxLength", getMaxLength()); + } + + if (inputPrompt != null) { + target.addAttribute("prompt", inputPrompt); + } + + // Adds the number of column and rows + final int c = getColumns(); + final int r = getRows(); + if (c != 0) { + target.addAttribute("cols", String.valueOf(c)); + } + if (r != 0) { + target.addAttribute("rows", String.valueOf(r)); + target.addAttribute("multiline", true); + if (!wordwrap) { + target.addAttribute("wordwrap", 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); + } + + /** + * 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(); + } + + /* + * Gets the value of the field, but uses formatter is given. Don't add a + * JavaDoc comment here, we use the default documentation from implemented + * interface. + */ + @Override + public Object getValue() { + Object v = super.getValue(); + if (format == null || v == null) { + return v; + } + try { + return format.format(v); + } catch (final IllegalArgumentException e) { + return v; + } + } + + /* + * Gets the components UIDL tag string. Don't add a JavaDoc comment here, we + * use the default documentation from implemented interface. + */ + @Override + public String getTag() { + return "textfield"; + } + + /* + * 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 variables) { + + super.changeVariables(source, 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 = 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(); + } + } + } + + } + + /* Text field configuration */ + + /** + * 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 columns; + } + + /** + * 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; + } + this.columns = columns; + requestRepaint(); + } + + /** + * Gets 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. + * + * @return number of explicitly set rows. + */ + 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. + * + * @param rows + * the number of rows for this editor. + */ + public void setRows(int rows) { + if (rows < 0) { + rows = 0; + } + this.rows = rows; + requestRepaint(); + } + + /** + * Tests if the editor is in word-wrap mode. + * + * @return <code>true</code> if the component is in the word-wrap mode, + * <code>false</code> if not. + */ + public boolean isWordwrap() { + return wordwrap; + } + + /** + * Sets the editor's word-wrap mode on or off. + * + * @param wordwrap + * the boolean value specifying if the editor should be in + * word-wrap mode after the call or not. + */ + public void setWordwrap(boolean wordwrap) { + this.wordwrap = wordwrap; + } + + /* 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 getType() { + return String.class; + } + + /** + * Gets the secret property on and off. If a field is used to enter + * secretinformation the information is not echoed to display. + * + * @return <code>true</code> if the field is used to enter secret + * information, <code>false</code> otherwise. + */ + public boolean isSecret() { + return secret; + } + + /** + * Sets the secret property on and off. If a field is used to enter + * secretinformation the information is not echoed to display. + * + * @param secret + * the value specifying if the field is used to enter secret + * information. + */ + public void setSecret(boolean secret) { + this.secret = secret; + requestRepaint(); + } + + /** + * 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 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; + } + + /** + * Gets the value formatter of TextField. + * + * @return the Format used to format the value. + * @deprecated + */ + @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 + */ + @Deprecated + public void setFormat(Format format) { + this.format = format; + requestRepaint(); + } + + @Override + protected boolean isEmpty() { + return super.isEmpty() || toString().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 maxLength; + } + + /** + * 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) { + this.maxLength = maxLength; + requestRepaint(); + } + +} diff --git a/src/com/vaadin/ui/Tree.java b/src/com/vaadin/ui/Tree.java new file mode 100644 index 0000000000..0d56384994 --- /dev/null +++ b/src/com/vaadin/ui/Tree.java @@ -0,0 +1,1048 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +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.ItemClickEvent; +import com.vaadin.event.ItemClickEvent.ItemClickListener; +import com.vaadin.event.ItemClickEvent.ItemClickSource; +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.MouseEventDetails; + +/** + * Tree component. A Tree can be used to select an item (or multiple items) from + * a hierarchical set of items. + * + * @author IT Mill Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +@SuppressWarnings("serial") +public class Tree extends AbstractSelect implements Container.Hierarchical, + Action.Container, ItemClickSource { + + private static final Method EXPAND_METHOD; + + private static final Method COLLAPSE_METHOD; + + static { + try { + EXPAND_METHOD = ExpandListener.class.getDeclaredMethod( + "nodeExpand", new Class[] { ExpandEvent.class }); + COLLAPSE_METHOD = CollapseListener.class.getDeclaredMethod( + "nodeCollapse", new Class[] { CollapseEvent.class }); + } catch (final java.lang.NoSuchMethodException e) { + // This should never happen + throw new java.lang.RuntimeException( + "Internal error finding methods in Tree"); + } + } + + /* Private members */ + + /** + * Set of expanded nodes. + */ + private final HashSet expanded = new HashSet(); + + /** + * List of action handlers. + */ + private LinkedList<Action.Handler> actionHandlers = null; + + /** + * Action mapper. + */ + private KeyMapper actionMapper = null; + + /** + * Is the tree selectable . + */ + 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; + + /* 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 todo = new Stack(); + 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 todo = new Stack(); + 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; + } + + /** + * Getter for property selectable. + * + * <p> + * The tree is selectable by default. + * </p> + * + * @return the Value of property selectable. + */ + public boolean isSelectable() { + return selectable; + } + + /** + * Setter for property selectable. + * + * <p> + * The tree is 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(); + } + } + + /* Component API */ + + /** + * Gets the UIDL tag corresponding to the component. + * + * @see com.vaadin.ui.AbstractComponent#getTag() + */ + @Override + public String getTag() { + return "tree"; + } + + /** + * Called when one or more variables handled by the implementing class are + * changed. + * + * @see com.vaadin.terminal.VariableOwner#changeVariables(Object + * source, Map variables) + */ + @Override + public void changeVariables(Object source, Map variables) { + + if (clickListenerCount > 0 && 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(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); + } + } + } + + // 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 = (Action) actionMapper.get(st.nextToken()); + if (action != null && containsId(itemId) + && actionHandlers != null) { + for (final Iterator<Action.Handler> i = actionHandlers + .iterator(); i.hasNext();) { + i.next().handleAction(action, this, itemId); + } + } + } + } + } + + /** + * 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")); + } else { + target.addAttribute("selectmode", "none"); + } + if (isNewItemsAllowed()) { + target.addAttribute("allownewitem", true); + } + + if (isNullSelectionAllowed()) { + target.addAttribute("nullselect", true); + } + + if (clickListenerCount > 0) { + target.addAttribute("listenClicks", true); + } + + } + + // Initialize variables + final Set<Action> actionSet = new LinkedHashSet<Action>(); + String[] selectedKeys; + if (isMultiSelect()) { + selectedKeys = new String[((Set) getValue()).size()]; + } else { + selectedKeys = new String[(getValue() == null ? 0 : 1)]; + } + int keyIndex = 0; + final LinkedList expandedKeys = new LinkedList(); + + // 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()); + } + + 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"); + } + + // Adds the attributes + target.addAttribute("caption", getItemCaption(itemId)); + final Resource icon = getItemIcon(itemId); + if (icon != null) { + target.addAttribute("icon", getItemIcon(itemId)); + } + final String key = itemIdMapper.key(itemId); + target.addAttribute("key", key); + if (isSelected(itemId)) { + target.addAttribute("selected", true); + try { + selectedKeys[keyIndex++] = key; + } catch (Exception e) { + // TODO Fix, see TreeExample (featurebrowser) + e.printStackTrace(); + } + } + 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("caption", a.getCaption()); + } + if (a.getIcon() != null) { + target.addAttribute("icon", a.getIcon()); + } + target.addAttribute("key", actionMapper.key(a)); + target.endTag("action"); + } + target.endTag("actions"); + } + + if (partialUpdate) { + // update tree-level selection information in case some selected + // node(s) were collapsed + target.addVariable(this, "selected", selectedKeys); + + partialUpdate = false; + } else { + // Selected + target.addVariable(this, "selected", selectedKeys); + + // Expand and collapse + target.addVariable(this, "expand", new String[] {}); + target.addVariable(this, "collapse", new String[] {}); + + // New items + target.addVariable(this, "newitem", new String[] {}); + } + } + + /* Container.Hierarchical API */ + + /** + * Tests if the Item with given ID can have any children. + * + * @see com.vaadin.data.Container.Hierarchical#areChildrenAllowed(Object) + */ + 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) + */ + 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) + */ + 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) + */ + 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) + */ + 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() + */ + 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) + */ + public boolean setChildrenAllowed(Object itemId, boolean areChildrenAllowed) { + final boolean success = ((Container.Hierarchical) items) + .setChildrenAllowed(itemId, areChildrenAllowed); + if (success) { + fireValueChange(false); + } + return success; + } + + /* + * (non-Javadoc) + * + * @see + * com.vaadin.data.Container.Hierarchical#setParent(java.lang.Object + * , java.lang.Object) + */ + 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 IT Mill Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ + public 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 IT Mill Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ + public interface ExpandListener extends Serializable { + + /** + * 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, EXPAND_METHOD); + } + + /** + * Removes the expand listener. + * + * @param listener + * the Listener to be removed. + */ + public void removeListener(ExpandListener listener) { + removeListener(ExpandEvent.class, listener, 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 IT Mill Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ + public 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 IT Mill Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ + public interface CollapseListener extends Serializable { + + /** + * 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, COLLAPSE_METHOD); + } + + /** + * Removes the collapse listener. + * + * @param listener + * the Listener to be removed. + */ + public void removeListener(CollapseListener listener) { + removeListener(CollapseEvent.class, listener, 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) + */ + public void addActionHandler(Action.Handler actionHandler) { + + if (actionHandler != null) { + + if (actionHandlers == null) { + actionHandlers = new LinkedList<Action.Handler>(); + actionMapper = new KeyMapper(); + } + + if (!actionHandlers.contains(actionHandler)) { + actionHandlers.add(actionHandler); + requestRepaint(); + } + } + } + + /** + * Removes an action handler. + * + * @see com.vaadin.event.Action.Container#removeActionHandler(Action.Handler) + */ + public void removeActionHandler(Action.Handler actionHandler) { + + if (actionHandlers != null && actionHandlers.contains(actionHandler)) { + + actionHandlers.remove(actionHandler); + + if (actionHandlers.isEmpty()) { + actionHandlers = null; + actionMapper = null; + } + + requestRepaint(); + } + } + + /** + * Gets the visible item ids. + * + * @see com.vaadin.ui.Select#getVisibleItemIds() + */ + @Override + public Collection getVisibleItemIds() { + + final LinkedList visible = new LinkedList(); + + // 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(); + } + } + + /** + * Focusing to this component is not supported. + * + * @throws UnsupportedOperationException + * if invoked. + * @see com.vaadin.ui.AbstractField#focus() + */ + @Override + public void focus() throws UnsupportedOperationException { + 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 int clickListenerCount = 0; + + public void addListener(ItemClickListener listener) { + addListener(ItemClickEvent.class, listener, + ItemClickEvent.ITEM_CLICK_METHOD); + clickListenerCount++; + // repaint needed only if click listening became necessary + if (clickListenerCount == 1) { + requestRepaint(); + } + } + + public void removeListener(ItemClickListener listener) { + removeListener(ItemClickEvent.class, listener, + ItemClickEvent.ITEM_CLICK_METHOD); + clickListenerCount++; + // repaint needed only if click listening is not needed in client + // anymore + if (clickListenerCount == 0) { + requestRepaint(); + } + } + +} diff --git a/src/com/vaadin/ui/TwinColSelect.java b/src/com/vaadin/ui/TwinColSelect.java new file mode 100644 index 0000000000..a619a5a0a9 --- /dev/null +++ b/src/com/vaadin/ui/TwinColSelect.java @@ -0,0 +1,115 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.util.Collection; + +import com.vaadin.data.Container; +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.PaintTarget; + +/** + * 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; + + /** + * + */ + 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. + * + * @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(); + } + } + + /** + * @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); + } + super.paintContent(target); + } + +} diff --git a/src/com/vaadin/ui/Upload.java b/src/com/vaadin/ui/Upload.java new file mode 100644 index 0000000000..238217a4c2 --- /dev/null +++ b/src/com/vaadin/ui/Upload.java @@ -0,0 +1,985 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.io.InputStream; +import java.io.OutputStream; +import java.io.Serializable; +import java.lang.reflect.Method; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.Map; + +import com.vaadin.Application; +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.PaintTarget; +import com.vaadin.terminal.UploadStream; + +/** + * Component for uploading files from client to server. + * + * The visible component consists of a file name input box and a browse button + * and an upload submit button to start uploading. + * + * 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. + * + * 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). + * + * 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(). + * + * @author IT Mill Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +@SuppressWarnings("serial") +public class Upload extends AbstractComponent implements Component.Focusable { + + private boolean delayedFocus; + + /** + * Upload buffer size. + */ + private static final int BUFFER_SIZE = 64 * 1024; // 64k + + /** + * 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"; + + /** + * ProgressListener to which information about progress is sent during + * upload + */ + private ProgressListener progressListener; + private LinkedHashSet progressListeners; + + /* TODO: Add a default constructor, receive to temp file. */ + + /** + * Creates a new instance of Upload that redirects the uploaded data to + * stream given by the Receiver. + * + * @param caption + * Normal component caption. You can set the caption of the + * upload submit button with setButtonCaption(). + * @param uploadReceiver + * Receiver to call to retrieve output stream when upload starts. + */ + public Upload(String caption, Receiver uploadReceiver) { + setCaption(caption); + receiver = uploadReceiver; + } + + /** + * Gets the component type. + * + * @return Component type as string. + */ + @Override + public String getTag() { + return "upload"; + } + + /** + * This method is called by terminal when upload is received. + * + * Note, this method is called outside synchronized (Application) block, so + * overriding this may be dangerous. + * + * @param upload + */ + public void receiveUpload(UploadStream upload) { + if (!isUploading) { + throw new IllegalStateException("uploading not started"); + } + + // Gets file properties + final String filename = upload.getContentName(); + final String type = upload.getContentType(); + + final Application application = getApplication(); + + synchronized (application) { + fireStarted(filename, type); + } + + // Gets the output target stream + final OutputStream out = receiver.receiveUpload(filename, type); + if (out == null) { + synchronized (application) { + fireNoOutputStream(filename, type, 0); + endUpload(); + } + return; + } + + final InputStream in = upload.getStream(); + + if (null == in) { + // No file, for instance non-existent filename in html upload + synchronized (application) { + fireNoInputStream(filename, type, 0); + endUpload(); + } + return; + } + + final byte buffer[] = new byte[BUFFER_SIZE]; + int bytesRead = 0; + totalBytes = 0; + try { + while ((bytesRead = in.read(buffer)) > 0) { + out.write(buffer, 0, bytesRead); + totalBytes += bytesRead; + if (progressListener != null && contentLength > 0) { + // update progress if listener set and contentLength + // received + synchronized (application) { + fireUpdateProgress(totalBytes, contentLength); + } + } + } + + // upload successful + out.close(); + synchronized (application) { + fireUploadSuccess(filename, type, totalBytes); + endUpload(); + requestRepaint(); + } + + } catch (final Exception e) { + synchronized (application) { + // Download interrupted + fireUploadInterrupted(filename, type, totalBytes, e); + endUpload(); + } + } + } + + /** + * 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 variables) { + // NOP + + } + + /** + * 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 { + // 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); + + target.addAttribute("buttoncaption", buttonCaption); + + target.addVariable(this, "fake", true); + + target.addUploadStreamVariable(this, "stream"); + } + + /** + * Interface that must be implemented by the upload receivers to provide the + * Upload component an output stream to write the uploaded data. + * + * @author IT Mill 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.Received event 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 IT Mill Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ + public 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.Interrupted event is sent when the upload is received, but the + * reception is interrupted for some reason. + * + * @author IT Mill Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ + public 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 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 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.Success event is sent when the upload is received successfully. + * + * @author IT Mill Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ + public 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.Started event is sent when the upload is started to received. + * + * @author IT Mill Ltd. + * @version + * @VERSION@ + * @since 5.0 + */ + public class StartedEvent extends Component.Event { + + private final String filename; + private final String type; + + /** + * + * @param source + * @param filename + * @param MIMEType + * @param length + */ + public StartedEvent(Upload source, String filename, String MIMEType) { + super(source); + this.filename = filename; + type = MIMEType; + } + + /** + * 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; + } + + } + + /** + * Receives the events when the upload starts. + * + * @author IT Mill 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 IT Mill 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 IT Mill 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 IT Mill 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(); + } + 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)); + } + + /** + * Emit upload finished event. + * + * @param filename + * @param MIMEType + * @param length + */ + protected void fireUploadReceived(String filename, String MIMEType, + long length) { + fireEvent(new Upload.FinishedEvent(this, filename, MIMEType, length)); + } + + /** + * 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 it = progressListeners.iterator(); it.hasNext();) { + ProgressListener l = (ProgressListener) it.next(); + l.updateProgress(totalBytes, contentLength); + } + } + // deprecated: + if (progressListener != null) { + progressListener.updateProgress(totalBytes, contentLength); + } + } + + /** + * Returns the current receiver. + * + * @return the Receiver. + */ + public Receiver getReceiver() { + return receiver; + } + + /** + * Sets the receiver. + * + * @param receiver + * the receiver to set. + */ + public void setReceiver(Receiver receiver) { + this.receiver = receiver; + } + + /* + * (non-Javadoc) + * + * @see com.vaadin.ui.Component.Focusable#focus() + */ + public void focus() { + final Application app = getApplication(); + if (app != null) { + getWindow().setFocusedComponent(this); + delayedFocus = false; + } else { + delayedFocus = true; + } + } + + /** + * Gets the Tabulator index of this Focusable component. + * + * @see com.vaadin.ui.Component.Focusable#getTabIndex() + */ + public int getTabIndex() { + return tabIndex; + } + + /** + * Sets the Tabulator index of this Focusable component. + * + * @see com.vaadin.ui.Component.Focusable#setTabIndex(int) + */ + public void setTabIndex(int tabIndex) { + this.tabIndex = tabIndex; + } + + /** + * Sets the size of the file currently being uploaded. + * + * @param contentLength + */ + public void setUploadSize(long contentLength) { + this.contentLength = contentLength; + } + + /** + * 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; + } + + /** + * 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. + */ + public void endUpload() { + isUploading = false; + contentLength = -1; + } + + 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) { + this.progressListener = progressListener; + } + + /** + * This method is deprecated. + * + * @deprecated Replaced with addListener/removeListener + * @return listener + * + */ + @Deprecated + public ProgressListener getProgressListener() { + return progressListener; + } + + /** + * 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; + } + + /** + * File uploads usually have button that starts actual upload progress. This + * method is used to set text in that button. + * + * @param buttonCaption + * text for uploads button. + */ + public void setButtonCaption(String buttonCaption) { + this.buttonCaption = buttonCaption; + } + + /** + * Notifies the component that it is connected to an application. + * + * @see com.vaadin.ui.Component#attach() + */ + @Override + public void attach() { + super.attach(); + if (delayedFocus) { + focus(); + } + } + +} diff --git a/src/com/vaadin/ui/UriFragmentUtility.java b/src/com/vaadin/ui/UriFragmentUtility.java new file mode 100644 index 0000000000..ad00386369 --- /dev/null +++ b/src/com/vaadin/ui/UriFragmentUtility.java @@ -0,0 +1,153 @@ +package com.vaadin.ui; + +import java.io.Serializable; +import java.lang.reflect.Method; +import java.util.Map; + +import com.vaadin.service.ApplicationContext.TransactionListener; +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.PaintTarget; + +/** + * Experimental web browser dependent component for URI fragment (part after + * hash mark "#") reading and writing. + * + * Component can be used to workaround common ajax web applications pitfalls: + * bookmarking a program state and back button. + * + */ +@SuppressWarnings("serial") +public class UriFragmentUtility extends AbstractComponent { + + /** + * Listener that listens changes in URI fragment. + */ + public interface FragmentChangedListener extends Serializable { + + public void fragmentChanged(FragmentChangedEvent source); + + } + + /** + * Event fired when uri fragment changes. + */ + public class FragmentChangedEvent extends Component.Event { + + /** + * Creates a new instance of UriFragmentReader change event. + * + * @param source + * the Source of the event. + */ + public FragmentChangedEvent(Component source) { + super(source); + } + + /** + * Gets the UriFragmentReader where the event occurred. + * + * @return the Source of the event. + */ + public UriFragmentUtility getUriFragmentUtility() { + return (UriFragmentUtility) getSource(); + } + } + + private static final Method FRAGMENT_CHANGED_METHOD; + + static { + try { + FRAGMENT_CHANGED_METHOD = FragmentChangedListener.class + .getDeclaredMethod("fragmentChanged", + new Class[] { FragmentChangedEvent.class }); + } catch (final java.lang.NoSuchMethodException e) { + // This should never happen + throw new java.lang.RuntimeException( + "Internal error finding methods in FragmentChangedListener"); + } + } + + public void addListener(FragmentChangedListener listener) { + addListener(FragmentChangedEvent.class, listener, + FRAGMENT_CHANGED_METHOD); + } + + public void removeListener(FragmentChangedListener listener) { + removeListener(FragmentChangedEvent.class, listener, + FRAGMENT_CHANGED_METHOD); + } + + private String fragment; + + public UriFragmentUtility() { + // immediate by default + setImmediate(true); + } + + @Override + public String getTag() { + return "urifragment"; + } + + @Override + public void paintContent(PaintTarget target) throws PaintException { + super.paintContent(target); + target.addVariable(this, "fragment", fragment); + } + + @Override + public void changeVariables(Object source, Map variables) { + super.changeVariables(source, variables); + fragment = (String) variables.get("fragment"); + fireEvent(new FragmentChangedEvent(this)); + } + + /** + * Gets currently set URI fragment. + * + * Note that initial URI fragment that user used to enter the application + * will be read after application init. If you absolutely need that you must + * hook to {@link TransactionListener} + * + * To listen changes in fragment, hook a {@link FragmentChangedListener}. + * + * @return the current fragment in browser uri or null if not known + */ + public String getFragment() { + return fragment; + } + + /** + * Sets URI fragment. Optionally fires a {@link FragmentChangedEvent} + * + * @param newFragment + * id of the new fragment + * @param fireEvent + * true to fire event + * @see FragmentChangedEvent + * @see FragmentChangedListener + */ + public void setFragment(String newFragment, boolean fireEvent) { + if ((newFragment == null && fragment != null) + || (newFragment != null && !newFragment.equals(fragment))) { + fragment = newFragment; + if (fireEvent) { + fireEvent(new FragmentChangedEvent(this)); + } + requestRepaint(); + } + } + + /** + * Sets URI fragment. This method fires a {@link FragmentChangedEvent} + * + * @param newFragment + * id of the new fragment + * @see FragmentChangedEvent + * @see FragmentChangedListener + */ + public void setFragment(String newFragment) { + setFragment(newFragment, true); + } + +} diff --git a/src/com/vaadin/ui/VerticalLayout.java b/src/com/vaadin/ui/VerticalLayout.java new file mode 100644 index 0000000000..55f70a740f --- /dev/null +++ b/src/com/vaadin/ui/VerticalLayout.java @@ -0,0 +1,27 @@ +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 IT Mill Ltd. + * @version + * @VERSION@ + * @since 5.3 + */ +@SuppressWarnings("serial") +public class VerticalLayout extends AbstractOrderedLayout { + + public VerticalLayout() { + setWidth("100%"); + } + + @Override + public String getTag() { + return "verticallayout"; + } + +} diff --git a/src/com/vaadin/ui/Window.java b/src/com/vaadin/ui/Window.java new file mode 100644 index 0000000000..90b761652c --- /dev/null +++ b/src/com/vaadin/ui/Window.java @@ -0,0 +1,1627 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.io.Serializable; +import java.lang.reflect.Method; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.Map; +import java.util.Set; + +import com.vaadin.Application; +import com.vaadin.terminal.DownloadStream; +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.PaintTarget; +import com.vaadin.terminal.ParameterHandler; +import com.vaadin.terminal.Resource; +import com.vaadin.terminal.Sizeable; +import com.vaadin.terminal.Terminal; +import com.vaadin.terminal.URIHandler; + +/** + * Application window component. + * + * @author IT Mill Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +@SuppressWarnings("serial") +public class Window extends Panel implements URIHandler, ParameterHandler { + + /** + * Window with no border. + */ + public static final int BORDER_NONE = 0; + + /** + * Window with only minimal border. + */ + public static final int BORDER_MINIMAL = 1; + + /** + * Window with default borders. + */ + public static final int BORDER_DEFAULT = 2; + + /** + * The terminal this window is attached to. + */ + private Terminal terminal = null; + + /** + * The application this window is attached to. + */ + private Application application = null; + + /** + * List of URI handlers for this window. + */ + private LinkedList uriHandlerList = null; + + /** + * List of parameter handlers for this window. + */ + private LinkedList parameterHandlerList = null; + + /** Set of subwindows */ + private final HashSet subwindows = new HashSet(); + + /** + * Explicitly specified theme of this window. If null, application theme is + * used. + */ + private String theme = null; + + /** + * Resources to be opened automatically on next repaint. + */ + private final LinkedList openList = new LinkedList(); + + /** + * The name of the window. + */ + private String name = null; + + /** + * Window border mode. + */ + private int border = BORDER_DEFAULT; + + /** + * Distance of Window top border in pixels from top border of the containing + * (main window) or -1 if unspecified. + */ + private int positionY = -1; + + /** + * Distance of Window left border in pixels from left border of the + * containing (main window) or -1 if unspecified . + */ + private int positionX = -1; + + private LinkedList notifications; + + private boolean modal = false; + + private boolean resizable = true; + + private boolean centerRequested = false; + + private Focusable pendingFocus; + + /* ********************************************************************* */ + + /** + * Creates a new empty unnamed window with default layout. + * + * <p> + * To show the window in application, it must be added to application with + * <code>Application.addWindow</code> method. + * </p> + * + * <p> + * The windows are scrollable by default. + * </p> + * + * @param caption + * the Title of the window. + */ + public Window() { + this("", null); + } + + /** + * Creates a new empty window with default layout. + * + * <p> + * To show the window in application, it must be added to application with + * <code>Application.addWindow</code> method. + * </p> + * + * <p> + * The windows are scrollable by default. + * </p> + * + * @param caption + * the Title of the window. + */ + public Window(String caption) { + this(caption, null); + } + + /** + * Creates a new window. + * + * <p> + * To show the window in application, it must be added to application with + * <code>Application.addWindow</code> method. + * </p> + * + * <p> + * The windows are scrollable by default. + * </p> + * + * @param caption + * the Title of the window. + * @param layout + * the Layout of the window. + */ + public Window(String caption, ComponentContainer content) { + super(caption, content); + setScrollable(true); + setSizeUndefined(); + } + + /** + * Gets the terminal type. + * + * @return the Value of property terminal. + */ + public Terminal getTerminal() { + return terminal; + } + + /* ********************************************************************* */ + + /** + * Gets the window of the component. Returns the window where this component + * belongs to. If the component does not yet belong to a window the returns + * null. + * + * @return the parent window of the component. + */ + @Override + public final Window getWindow() { + return this; + } + + /** + * Gets the application instance of the component. Returns the application + * where this component belongs to. If the component does not yet belong to + * a application the returns null. + * + * @return the parent application of the component. + */ + @Override + public final Application getApplication() { + if (getParent() == null) { + return application; + } + return ((Window) getParent()).getApplication(); + } + + /** + * Getter for property parent. + * + * <p> + * Parent is the visual parent of a component. Each component can belong to + * only one ComponentContainer at time. + * </p> + * + * <p> + * For windows attached directly to the application, parent is + * <code>null</code>. For windows inside other windows, parent is the window + * containing this window. + * </p> + * + * @return the Value of property parent. + */ + @Override + public final Component getParent() { + return super.getParent(); + } + + /** + * Setter for property parent. + * + * <p> + * Parent is the visual parent of a component. This is mostly called by + * containers add method and should not be called directly + * </p> + * + * @param parent + * the New value of property parent. + */ + @Override + public void setParent(Component parent) { + super.setParent(parent); + } + + /** + * Gets the component UIDL tag. + * + * @return the Component UIDL tag as string. + */ + @Override + public String getTag() { + return "window"; + } + + /* ********************************************************************* */ + + /** + * Adds the new URI handler to this window. For sub-windows, URI handlers + * are attached to root level window. + * + * @param handler + * the URI handler to add. + */ + public void addURIHandler(URIHandler handler) { + if (getParent() != null) { + // this is subwindow, attach to main level instead + // TODO hold internal list also and remove on detach + Window mainWindow = (Window) getParent(); + mainWindow.addURIHandler(handler); + } else { + if (uriHandlerList == null) { + uriHandlerList = new LinkedList(); + } + synchronized (uriHandlerList) { + if (!uriHandlerList.contains(handler)) { + uriHandlerList.addLast(handler); + } + } + } + } + + /** + * Removes the given URI handler from this window. + * + * @param handler + * the URI handler to remove. + */ + public void removeURIHandler(URIHandler handler) { + if (getParent() != null) { + // this is subwindow + Window mainWindow = (Window) getParent(); + mainWindow.removeURIHandler(handler); + } else { + if (handler == null || uriHandlerList == null) { + return; + } + synchronized (uriHandlerList) { + uriHandlerList.remove(handler); + if (uriHandlerList.isEmpty()) { + uriHandlerList = null; + } + } + } + } + + /** + * Handles uri recursively. Windows uri handler passes uri to all + * {@link URIHandler}s added to it. + * <p> + * Note, that instead of overriding this method developer should consider + * using {@link Window#addURIHandler(URIHandler)} to add uri handler to + * Window. + * + * @param context + * @param relativeUri + */ + public DownloadStream handleURI(URL context, String relativeUri) { + + DownloadStream result = null; + if (uriHandlerList != null) { + Object[] handlers; + synchronized (uriHandlerList) { + handlers = uriHandlerList.toArray(); + } + for (int i = 0; i < handlers.length; i++) { + final DownloadStream ds = ((URIHandler) handlers[i]).handleURI( + context, relativeUri); + if (ds != null) { + if (result != null) { + throw new RuntimeException("handleURI for " + context + + " uri: '" + relativeUri + + "' returns ambigious result."); + } + result = ds; + } + } + } + return result; + } + + /* ********************************************************************* */ + + /** + * Adds the new parameter handler to this window. For sub windows, parameter + * handlers are attached to parent windows. + * + * @param handler + * the parameter handler to add. + */ + public void addParameterHandler(ParameterHandler handler) { + if (getParent() != null) { + // this is subwindow + // TODO hold internal list also and remove on detach + Window mainWindow = (Window) getParent(); + mainWindow.addParameterHandler(handler); + } else { + if (parameterHandlerList == null) { + parameterHandlerList = new LinkedList(); + } + synchronized (parameterHandlerList) { + if (!parameterHandlerList.contains(handler)) { + parameterHandlerList.addLast(handler); + } + } + } + + } + + /** + * Removes the given URI handler from this window. + * + * @param handler + * the parameter handler to remove. + */ + public void removeParameterHandler(ParameterHandler handler) { + if (getParent() != null) { + // this is subwindow + Window mainWindow = (Window) getParent(); + mainWindow.addParameterHandler(handler); + } else { + if (handler == null || parameterHandlerList == null) { + return; + } + synchronized (parameterHandlerList) { + parameterHandlerList.remove(handler); + if (parameterHandlerList.isEmpty()) { + parameterHandlerList = null; + } + } + } + } + + /* Documented by the interface */ + public void handleParameters(Map parameters) { + if (parameterHandlerList != null) { + Object[] handlers; + synchronized (parameterHandlerList) { + handlers = parameterHandlerList.toArray(); + } + for (int i = 0; i < handlers.length; i++) { + ((ParameterHandler) handlers[i]).handleParameters(parameters); + } + } + } + + /* ********************************************************************* */ + + /** + * Gets the theme for this window. + * + * <p> + * Subwindows do not support themes and thus return theme used by the parent + * </p> + * + * @return the Name of the theme used in window. If the theme for this + * individual window is not explicitly set, the application theme is + * used instead. If application is not assigned the + * terminal.getDefaultTheme is used. If terminal is not set, null is + * returned + */ + public String getTheme() { + if (getParent() != null) { + return ((Window) getParent()).getTheme(); + } + if (theme != null) { + return theme; + } + if ((application != null) && (application.getTheme() != null)) { + return application.getTheme(); + } + if (terminal != null) { + return terminal.getDefaultTheme(); + } + return null; + } + + /** + * Sets the theme for this window. + * + * Setting theme for subwindows is not supported. + * + * In Toolkit 5 terminal will reload its host page on theme changes. + * + * @param theme + * the New theme for this window. Null implies the default theme. + */ + public void setTheme(String theme) { + if (getParent() != null) { + throw new UnsupportedOperationException( + "Setting theme for sub-windows is not supported."); + } + this.theme = theme; + requestRepaint(); + } + + /** + * Paints the content of this component. + * + * @param event + * the Paint Event. + * @throws PaintException + * if the paint operation failed. + */ + @Override + public synchronized void paintContent(PaintTarget target) + throws PaintException { + + // Sets the window name + final String name = getName(); + target.addAttribute("name", name == null ? "" : name); + + // Sets the window theme + final String theme = getTheme(); + target.addAttribute("theme", theme == null ? "" : theme); + + if (modal) { + target.addAttribute("modal", true); + } + + if (resizable) { + target.addAttribute("resizable", true); + } + + if (centerRequested) { + target.addAttribute("center", true); + centerRequested = false; + } + + // Marks the main window + if (getApplication() != null + && this == getApplication().getMainWindow()) { + target.addAttribute("main", true); + } + + if (getContent() != null) { + if (getContent().getHeightUnits() == Sizeable.UNITS_PERCENTAGE) { + target.addAttribute("layoutRelativeHeight", true); + } + if (getContent().getWidthUnits() == Sizeable.UNITS_PERCENTAGE) { + target.addAttribute("layoutRelativeWidth", true); + } + } + + // Open requested resource + synchronized (openList) { + if (!openList.isEmpty()) { + for (final Iterator i = openList.iterator(); i.hasNext();) { + ((OpenResource) i.next()).paintContent(target); + } + openList.clear(); + } + } + + // Contents of the window panel is painted + super.paintContent(target); + + // Window position + target.addVariable(this, "positionx", getPositionX()); + target.addVariable(this, "positiony", getPositionY()); + + // Window closing + target.addVariable(this, "close", false); + + // Paint subwindows + for (final Iterator i = subwindows.iterator(); i.hasNext();) { + final Window w = (Window) i.next(); + w.paint(target); + } + + // Paint notifications + if (notifications != null) { + target.startTag("notifications"); + for (final Iterator it = notifications.iterator(); it.hasNext();) { + final Notification n = (Notification) it.next(); + target.startTag("notification"); + if (n.getCaption() != null) { + target.addAttribute("caption", n.getCaption()); + } + if (n.getMessage() != null) { + target.addAttribute("message", n.getMessage()); + } + if (n.getIcon() != null) { + target.addAttribute("icon", n.getIcon()); + } + target.addAttribute("position", n.getPosition()); + target.addAttribute("delay", n.getDelayMsec()); + if (n.getStyleName() != null) { + target.addAttribute("style", n.getStyleName()); + } + target.endTag("notification"); + } + target.endTag("notifications"); + notifications = null; + } + + if (pendingFocus != null) { + // ensure focused component is still attached to this main window + if (pendingFocus.getWindow() == this + || (pendingFocus.getWindow() != null && pendingFocus + .getWindow().getParent() == this)) { + target.paintReference(pendingFocus, "focused"); + } + pendingFocus = null; + } + + } + + /* ********************************************************************* */ + + /** + * Opens the given resource in this window. + * + * @param resource + */ + public void open(Resource resource) { + synchronized (openList) { + if (!openList.contains(resource)) { + openList.add(new OpenResource(resource, null, -1, -1, + BORDER_DEFAULT)); + } + } + requestRepaint(); + } + + /* ********************************************************************* */ + + /** + * Opens the given resource in named terminal window. Empty or + * <code>null</code> window name results the resource to be opened in this + * window. + * + * @param resource + * the resource. + * @param windowName + * the name of the window. + */ + public void open(Resource resource, String windowName) { + synchronized (openList) { + if (!openList.contains(resource)) { + openList.add(new OpenResource(resource, windowName, -1, -1, + BORDER_DEFAULT)); + } + } + requestRepaint(); + } + + /* ********************************************************************* */ + + /** + * Opens the given resource in named terminal window with given size and + * border properties. Empty or <code>null</code> window name results the + * resource to be opened in this window. + * + * @param resource + * @param windowName + * @param width + * @param height + * @param border + */ + public void open(Resource resource, String windowName, int width, + int height, int border) { + synchronized (openList) { + if (!openList.contains(resource)) { + openList.add(new OpenResource(resource, windowName, width, + height, border)); + } + } + requestRepaint(); + } + + /* ********************************************************************* */ + + /** + * Returns the full url of the window, this returns window specific url even + * for the main window. + * + * @return the URL of the window. + */ + public URL getURL() { + + if (application == null) { + return null; + } + + try { + return new URL(application.getURL(), getName() + "/"); + } catch (final MalformedURLException e) { + throw new RuntimeException( + "Internal problem getting window URL, please report"); + } + } + + /** + * Gets the unique name of the window that indentifies it on the terminal. + * + * <p> + * Name identifies the URL used to access application-level windows, but is + * not used for windows inside other windows. all application-level windows + * can be accessed by their names in url + * <code>http://host:port/foo/bar/</code> where + * <code>http://host:port/foo/</code> is the application url as returned by + * getURL() and <code>bar</code> is the name of the window. Also note that + * not all windows should be added to application - one can also add windows + * inside other windows - these windows show as smaller windows inside those + * windows. + * </p> + * + * @return the Name of the Window. + */ + public String getName() { + return name; + } + + /** + * Returns the border. + * + * @return the border. + */ + public int getBorder() { + return border; + } + + /** + * Sets the border. + * + * @param border + * the border to set. + */ + public void setBorder(int border) { + this.border = border; + } + + /** + * Sets the application this window is connected to. + * + * <p> + * This method should not be invoked directly. Instead the + * {@link com.vaadin.Application#addWindow(Window)} method should be + * used to add the window to an application and + * {@link com.vaadin.Application#removeWindow(Window)} method for + * removing the window from the applicion. These methods call this method + * implicitly. + * </p> + * + * <p> + * The method invokes {@link Component#attach()} and + * {@link Component#detach()} methods when necessary. + * <p> + * + * @param application + * the application to set. + */ + public void setApplication(Application application) { + + // If the application is not changed, dont do nothing + if (application == this.application) { + return; + } + + // Sends detach event if the window is connected to application + if (this.application != null) { + detach(); + } + + // Connects to new parent + this.application = application; + + // Sends the attach event if connected to a window + if (application != null) { + attach(); + } + } + + /** + * Sets the name. + * <p> + * The name of the window must be unique inside the application. + * </p> + * + * <p> + * If the name is null, the the window is given name automatically when it + * is added to an application. + * </p> + * + * @param name + * the name to set. + */ + public void setName(String 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"); + } + + this.name = name; + } + + /** + * Sets the terminal type. The terminal type is set by the the terminal + * adapter and may change from time to time. + * + * @param type + * the terminal type to set. + */ + public void setTerminal(Terminal type) { + terminal = type; + } + + /** + * Private data structure for storing opening window properties. + */ + private class OpenResource implements Serializable { + + private final Resource resource; + + private final String name; + + private final int width; + + private final int height; + + private final int border; + + /** + * Creates a new open resource. + * + * @param resource + * @param name + * @param width + * @param height + * @param border + */ + private OpenResource(Resource resource, String name, int width, + int height, int border) { + this.resource = resource; + this.name = name; + this.width = width; + this.height = height; + this.border = border; + } + + /** + * Paints the open-tag inside the window. + * + * @param target + * the Paint Event. + * @throws PaintException + * if the Paint Operation fails. + */ + private void paintContent(PaintTarget target) throws PaintException { + target.startTag("open"); + target.addAttribute("src", resource); + if (name != null && name.length() > 0) { + target.addAttribute("name", name); + } + if (width >= 0) { + target.addAttribute("width", width); + } + if (height >= 0) { + target.addAttribute("height", height); + } + switch (border) { + case Window.BORDER_MINIMAL: + target.addAttribute("border", "minimal"); + break; + case Window.BORDER_NONE: + target.addAttribute("border", "none"); + break; + } + + target.endTag("open"); + } + } + + /** + * Called when one or more variables handled by the implementing class are + * changed. + * + * @see com.vaadin.terminal.VariableOwner#changeVariables(java.lang.Object, + * java.util.Map) + */ + @Override + public void changeVariables(Object source, Map variables) { + + 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() != UNITS_PIXELS || (Integer) variables + .get("height") != getHeight())) { + sizeHasChanged = true; + } + if (variables.containsKey("width") + && (getWidthUnits() != UNITS_PIXELS || (Integer) variables + .get("width") != getWidth())) { + sizeHasChanged = true; + } + + super.changeVariables(source, variables); + + // Positioning + final Integer positionx = (Integer) variables.get("positionx"); + if (positionx != null) { + final int x = positionx.intValue(); + setPositionX(x < 0 ? -1 : x); + } + final Integer positiony = (Integer) variables.get("positiony"); + if (positiony != null) { + final int y = positiony.intValue(); + setPositionY(y < 0 ? -1 : y); + } + + if (!isReadOnly()) { + // Closing + final Boolean close = (Boolean) variables.get("close"); + if (close != null && close.booleanValue()) { + close(); + } + } + + // fire event if size has really changed + if (sizeHasChanged) { + fireResize(); + } + + } + + /** + * 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> + * If one wants change the default behavior, register a window close + * listenter and do something else. For example, you could re-open the + * browser-level window with mainWindow.open(), re-add the removed + * sub-window back to its parent or remove browser-level window + * automatically from the application. + * </p> + */ + protected void close() { + Window parent = (Window) getParent(); + if (parent == null) { + fireClose(); + } else { + // subwindow is removed from parent + parent.removeWindow(this); + fireClose(); + } + } + + /** + * 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 positionX; + } + + /** + * 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) { + this.positionX = positionX; + 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 positionY; + } + + /** + * 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) { + this.positionY = positionY; + 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(); + } + } + + public interface CloseListener extends Serializable { + public void windowClose(CloseEvent e); + } + + /** + * Adds the listener. + * + * @param listener + * the listener to add. + */ + public void addListener(CloseListener listener) { + addListener(CloseEvent.class, listener, WINDOW_CLOSE_METHOD); + } + + /** + * Removes the listener. + * + * @param listener + * the listener to remove. + */ + public void removeListener(CloseListener listener) { + addListener(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, this); + } + + /** + * Fire the resize event. + */ + protected void fireResize() { + fireEvent(new ResizeEvent(this)); + } + + private void attachWindow(Window w) { + subwindows.add(w); + w.setParent(this); + requestRepaint(); + } + + /** + * Adds a window inside another window. + * + * <p> + * Adding windows inside another window creates "subwindows". These windows + * should not be added to application directly and are not accessible + * directly with any url. Addding windows implicitly sets their parents. + * </p> + * + * <p> + * Only one level of subwindows are supported. Thus you can add windows + * inside such windows whose parent is <code>null</code>. + * </p> + * + * @param window + * @throws IllegalArgumentException + * if a window is added inside non-application level window. + * @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 was already added to application" + + " - it can not be added to another window also."); + } else if (getParent() != null) { + throw new IllegalArgumentException( + "You can only add windows inside application-level windows."); + } else if (window.subwindows.size() > 0) { + throw new IllegalArgumentException( + "Only one level of subwindows are supported."); + } + + attachWindow(window); + } + + /** + * Remove the given subwindow from this window. + * + * @param window + * Window to be removed. + */ + public void removeWindow(Window window) { + subwindows.remove(window); + window.setParent(null); + requestRepaint(); + + } + + /** + * Get the set of all child windows. + * + * @return Set of child windows. + */ + public Set getChildWindows() { + return Collections.unmodifiableSet(subwindows); + } + + /** + * Sets sub-window modal, so that widgets behind it cannot be accessed. + * <b>Note:</b> affects sub-windows only. + * + * @param modality + * true if modality is to be turned on + */ + public void setModal(boolean modality) { + modal = modality; + center(); + requestRepaint(); + } + + /** + * @return true if this window is modal. + */ + public boolean isModal() { + return modal; + } + + /** + * 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 resizeability) { + resizable = resizeability; + requestRepaint(); + } + + /** + * + * @return true if window is resizable by the end-user, otherwise false. + */ + public boolean isResizable() { + return resizable; + } + + /** + * Request to center this window on the screen. <b>Note:</b> affects + * sub-windows only. + */ + public void center() { + centerRequested = true; + requestRepaint(); + } + + /** + * Shows a notification message on the middle of the window. The message + * automatically disappears ("humanized message"). + * + * @see #showNotification(com.vaadin.ui.Window.Notification) + * @see Notification + * + * @param caption + * The message + */ + public void showNotification(String caption) { + addNotification(new Notification(caption)); + } + + /** + * Shows a notification message the window. 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. + * + * @see #showNotification(com.vaadin.ui.Window.Notification) + * @see Notification + * + * @param caption + * The message + * @param type + * The message type + */ + public void showNotification(String caption, int type) { + addNotification(new Notification(caption, type)); + } + + /** + * Shows a notification consisting of a bigger caption and a smaller + * description on the middle of the window. The message automatically + * disappears ("humanized message"). + * + * @see #showNotification(com.vaadin.ui.Window.Notification) + * @see Notification + * + * @param caption + * The caption of the message + * @param description + * The message description + * + */ + public void showNotification(String caption, String description) { + addNotification(new Notification(caption, description)); + } + + /** + * 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. + * + * @see #showNotification(com.vaadin.ui.Window.Notification) + * @see Notification + * + * @param caption + * The caption of the message + * @param description + * The message description + * @param type + * The message type + */ + public void showNotification(String caption, String description, int type) { + addNotification(new Notification(caption, description, type)); + } + + /** + * 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 + */ + public void showNotification(Notification notification) { + addNotification(notification); + } + + private void addNotification(Notification notification) { + if (notifications == null) { + notifications = new LinkedList(); + } + notifications.add(notification); + requestRepaint(); + } + + /** + * 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 Toolkit application, use Focusable.focus(). See + * {@link Focusable}. + * + * @param focusable + * to be focused on next paint + */ + void setFocusedComponent(Focusable focusable) { + if (getParent() != null) { + // focus is handled by main windows + ((Window) getParent()).setFocusedComponent(focusable); + } else { + pendingFocus = focusable; + requestRepaint(); + } + } + + /** + * 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 static 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; + + /** + * Creates a "humanized" notification message. + * + * @param caption + * The message to show + */ + public Notification(String caption) { + this(caption, null, TYPE_HUMANIZED_MESSAGE); + } + + /** + * Creates a notification message of the specified type. + * + * @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. + * + * @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. + * + * @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 = caption; + this.description = description; + 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; + } + + /** + * @deprecated Use {@link #getDescription()} instead. + * @return + */ + @Deprecated + public String getMessage() { + return description; + } + + /** + * @deprecated Use {@link #setDescription(String)} instead. + * @param description + */ + @Deprecated + public void setMessage(String description) { + this.description = description; + } + + /** + * 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; + } + } + +} diff --git a/src/com/vaadin/ui/doc-files/component_class_hierarchy.gif b/src/com/vaadin/ui/doc-files/component_class_hierarchy.gif Binary files differnew file mode 100644 index 0000000000..936c220d11 --- /dev/null +++ b/src/com/vaadin/ui/doc-files/component_class_hierarchy.gif diff --git a/src/com/vaadin/ui/doc-files/component_interfaces.gif b/src/com/vaadin/ui/doc-files/component_interfaces.gif Binary files differnew file mode 100644 index 0000000000..44c99826bb --- /dev/null +++ b/src/com/vaadin/ui/doc-files/component_interfaces.gif diff --git a/src/com/vaadin/ui/package.html b/src/com/vaadin/ui/package.html new file mode 100644 index 0000000000..2a26453a1f --- /dev/null +++ b/src/com/vaadin/ui/package.html @@ -0,0 +1,72 @@ +<!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 the IT Mill Toolkit.</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 subinterfaces 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 UI components. It defines the +common properties of the components and how the framework will handle +them. Most simple components (like {@link com.vaadin.ui.Button} for +example} won't need to implement the lower level interfaces described +below. Note that 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 whose contents can be positioned arbitrarily.</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> |