diff options
author | Joonas Lehtinen <joonas.lehtinen@itmill.com> | 2006-11-01 09:11:32 +0000 |
---|---|---|
committer | Joonas Lehtinen <joonas.lehtinen@itmill.com> | 2006-11-01 09:11:32 +0000 |
commit | 13af8cba414fbb6f02ef458a86c5afcad70c5275 (patch) | |
tree | 959ccae1696d9c208124ec3982f166bca6c28f0a /src/com/itmill/toolkit/ui | |
parent | de5565e87dc08be0a577c663bb2e009d0838c872 (diff) | |
download | vaadin-framework-13af8cba414fbb6f02ef458a86c5afcad70c5275.tar.gz vaadin-framework-13af8cba414fbb6f02ef458a86c5afcad70c5275.zip |
Refactoring: Enably -> IT Mill Toolkit
svn changeset:92/svn branch:toolkit
Diffstat (limited to 'src/com/itmill/toolkit/ui')
31 files changed, 13913 insertions, 0 deletions
diff --git a/src/com/itmill/toolkit/ui/AbstractComponent.java b/src/com/itmill/toolkit/ui/AbstractComponent.java new file mode 100644 index 0000000000..4c08b3811f --- /dev/null +++ b/src/com/itmill/toolkit/ui/AbstractComponent.java @@ -0,0 +1,817 @@ +/* ************************************************************************* + + IT Mill Toolkit + + Development of Browser User Intarfaces Made Easy + + Copyright (C) 2000-2006 IT Mill Ltd + + ************************************************************************* + + This product is distributed under commercial license that can be found + from the product package on license/license.txt. Use of this product might + require purchasing a commercial license from IT Mill Ltd. For guidelines + on usage, see license/licensing-guidelines.html + + ************************************************************************* + + For more information, contact: + + IT Mill Ltd phone: +358 2 4802 7180 + Ruukinkatu 2-4 fax: +358 2 4802 7181 + 20540, Turku email: info@itmill.com + Finland company www: www.itmill.com + + Primary source for information and releases: www.itmill.com + + ********************************************************************** */ + +package com.itmill.toolkit.ui; + +import com.itmill.toolkit.Application; +import com.itmill.toolkit.event.EventRouter; +import com.itmill.toolkit.event.MethodEventSource; +import com.itmill.toolkit.terminal.*; + +import java.util.Collection; +import java.util.LinkedList; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.HashSet; +import java.lang.reflect.Method; + +/** 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 + * MillStone component. Most components in the MillStone base UI package do + * just that. + * + * @author IT Mill Ltd. + * @version @VERSION@ + * @since 3.0 + */ +public abstract class AbstractComponent + implements Component, MethodEventSource { + + /* Private members ************************************************* */ + + /** Look-and-feel style of the component. */ + private String style; + + /** Caption text. */ + private String caption; + + /** Application specific data object. */ + private Object applicationData; + + /** Icon to be shown together with caption. */ + private Resource icon; + + /** Is the component enable (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 MillStone event model. */ + private EventRouter eventRouter = null; + + /** The internal error message of the component. */ + private ErrorMessage componentError = null; + + /** List of event variable change event handling dependencies */ + private Set dependencies = null; + + /** Immediate mode: if true, all variable changes are required to be sent + * from the terminal immediately + */ + private boolean immediate = false; + + /** Debug mode: if true, the component may output visual debug information + */ + private boolean debug = 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; + + /* Constructor ***************************************************** */ + + /** Constructs a new Component */ + public AbstractComponent() { + } + + /* Get/Set component properties ************************************ */ + + /** Gets the UIDL tag corresponding to the component. + * + * @return component's UIDL tag as <code>String</code> + */ + public abstract String getTag(); + + /* Gets the component's style. + * Don't add a JavaDoc comment here, we use the default documentation + * from implemented interface. + */ + public String getStyle() { + return this.style; + } + + /* Sets the component's style. + * Don't add a JavaDoc comment here, we use the default documentation + * from implemented interface. + */ + public void setStyle(String style) { + this.style = 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 this.caption; + } + + /** Sets the component's caption <code>String</code>. Caption is the + * visible name of the component. This method will trigger a + * {@link com.itmill.toolkit.terminal.Paintable.RepaintRequestEvent RepaintRequestEvent}. + * + * @param caption 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 (this.locale != null) + return this.locale; + if (this.parent != null) + return parent.getLocale(); + Application app = this.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 this.icon; + } + + /** Sets the component's icon. This method will trigger a + * {@link com.itmill.toolkit.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 this.enabled && 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) { + this.enabled = enabled; + 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.itmill.toolkit.terminal.Paintable.RepaintRequestEvent RepaintRequestEvent}. + * + * @param immediate 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(); + } + + /* Tests if the component is visible. + * Don't add a JavaDoc comment here, we use the default documentation + * from implemented interface. + */ + public boolean isVisible() { + return this.visible; + } + + /* Sets the components visibility. + * Don't add a JavaDoc comment here, we use the default documentation + * from implemented interface. + */ + 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 this.description; + } + + /** Sets the component's description. See {@link #getDescription()} for + * more information on what the description is. This method will trigger + * a {@link com.itmill.toolkit.terminal.Paintable.RepaintRequestEvent RepaintRequestEvent}. + * + * @param description 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 this.parent; + } + + /* Set 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, dont do nothing + if (parent == this.parent) + return; + + // Send detach event if the component have been connected to a window + if (getApplication() != null) { + detach(); + this.parent = null; + } + + // Connect to new parent + this.parent = parent; + + // Send attach event if connected to a window + if (getApplication() != null) + attach(); + } + + /** Get 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 this.componentError; + } + + /** Gets the component's error message. + * @link Terminal.ErrorMessage#ErrorMessage(String, int) + * + * @return component's error message + */ + public ErrorMessage getComponentError() { + return this.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 errorMessage 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; + } + + /* Set 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(); + } + + /* Get 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() { + } + + /* Detach the component from application. + * Don't add a JavaDoc comment here, we use the default documentation + * from implemented interface. + */ + public void detach() { + } + + /* Get 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, this.getTag())) { + if (getStyle() != null && getStyle().length() > 0) + target.addAttribute("style", getStyle()); + if (isReadOnly()) + target.addAttribute("readonly", true); + if (!isVisible()) + target.addAttribute("invisible", 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()); + + // Only paint content of visible components. + if (isVisible()) { + paintContent(target); + + String desc = getDescription(); + if (desc != null && description.length() > 0) { + target.startTag("description"); + target.addUIDL(getDescription()); + target.endTag("description"); + } + + ErrorMessage error = getErrorMessage(); + if (error != null) + error.paint(target); + } + } + target.endTag(this.getTag()); + + repaintRequestListenersNotified = false; + } + + /** 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 target UIDL stream where the component should paint + * itself to + * @throws PaintException if the 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 do not need repaints + if (!isVisible()) + return; + + fireRequestRepaintEvent(alreadyNotified); + } + + /** Fire repaint request event */ + private void fireRequestRepaintEvent(Collection alreadyNotified) { + + // Notify listeners only once + if (!repaintRequestListenersNotified) { + + // Notify the listeners + if (repaintRequestListeners != null + && !repaintRequestListeners.isEmpty()) { + Object[] listeners = repaintRequestListeners.toArray(); + 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 + 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) { + + } + + /* Adds a variable-change dependency to this component. + * Don't add a JavaDoc comment here, we use the default documentation + * from implemented interface. + */ + public void dependsOn(VariableOwner depended) { + + // Assure that the list exists + if (dependencies == null) + dependencies = new HashSet(); + + // Add to the list of dependencies + if (depended != null) + dependencies.add(depended); + } + + /* Removes a dependency from the component. + * Don't add a JavaDoc comment here, we use the default documentation + * from implemented interface. + */ + public void removeDirectDependency(VariableOwner depended) { + + // Remove the listener if necessary + if (dependencies != null && depended != null) + dependencies.remove(depended); + } + + /* Gets the set of depended components. + * Don't add a JavaDoc comment here, we use the default documentation + * from implemented interface. + */ + public Set getDirectDependencies() { + return dependencies; + } + + /* 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 (java.lang.NoSuchMethodException e) { + // This should never happen + e.printStackTrace(); + throw new java.lang.RuntimeException(); + } + } + + /** <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 MillStone inheritable event mechanism + * see the + * {@link com.itmill.toolkit.event com.itmill.toolkit.event package documentation}.</p> + * + * @param eventType 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 + * @throws java.lang.IllegalArgumentException unless <code>method</code> + * has exactly one match in <code>object</code> + */ + public void addListener(Class eventType, Object object, Method method) { + if (eventRouter == null) + eventRouter = new EventRouter(); + eventRouter.addListener(eventType, object, method); + } + + /** <p>Registers a new listener with the specified activation method to + * listen events generated by this component. If the activation method + * does not have any arguments the event object will not be passed to it + * when it's called.</p> + * + * <p>This 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 MillStone inheritable event mechanism + * see the + * {@link com.itmill.toolkit.event com.itmill.toolkit.event package documentation}.</p> + * + * @param eventType 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 + * @throws java.lang.IllegalArgumentException unless <code>method</code> + * has exactly one match in <code>object</code> + */ + 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 MillStone inheritable event mechanism + * see the + * {@link com.itmill.toolkit.event com.itmill.toolkit.event package documentation}.</p> + * + * @param eventType 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 + */ + 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 MillStone inheritable event mechanism + * see the + * {@link com.itmill.toolkit.event com.itmill.toolkit.event package documentation}.</p> + * + * @param eventType 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 MillStone inheritable event mechanism + * see the + * {@link com.itmill.toolkit.event com.itmill.toolkit.event package documentation}.</p> + * + * @param eventType 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 methodName 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); + } + + /** Send event to all listeners + * @param event 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 a component event. It is transmitted to all registered + * listeners interested in such events. + */ + protected void fireComponentEvent() { + fireEvent(new Component.Event(this)); + } + + /** Emits a component error event. It is transmitted to all registered + * listeners interested in such events. + */ + protected void fireComponentErrorEvent() { + fireEvent(new Component.ErrorEvent(this.getComponentError(),this)); + } + + /** Sets application specific data object. + * + * @param data Application specific data. + * @since 3.1 + */ + public void setData(Object data) { + this.applicationData = data; + } + + /** Gets application specific data. + * + * @return Application specific data set with setData function. + * @since 3.1 + */ + public Object getData() { + return this.applicationData; + } +}
\ No newline at end of file diff --git a/src/com/itmill/toolkit/ui/AbstractComponentContainer.java b/src/com/itmill/toolkit/ui/AbstractComponentContainer.java new file mode 100644 index 0000000000..e7b142f828 --- /dev/null +++ b/src/com/itmill/toolkit/ui/AbstractComponentContainer.java @@ -0,0 +1,189 @@ +/* ************************************************************************* + + IT Mill Toolkit + + Development of Browser User Intarfaces Made Easy + + Copyright (C) 2000-2006 IT Mill Ltd + + ************************************************************************* + + This product is distributed under commercial license that can be found + from the product package on license/license.txt. Use of this product might + require purchasing a commercial license from IT Mill Ltd. For guidelines + on usage, see license/licensing-guidelines.html + + ************************************************************************* + + For more information, contact: + + IT Mill Ltd phone: +358 2 4802 7180 + Ruukinkatu 2-4 fax: +358 2 4802 7181 + 20540, Turku email: info@itmill.com + Finland company www: www.itmill.com + + Primary source for information and releases: www.itmill.com + + ********************************************************************** */ + +package com.itmill.toolkit.ui; + +import java.lang.reflect.Method; +import java.util.LinkedList; +import java.util.Iterator; + +/** 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 MillStone component container. + * + * @author IT Mill Ltd + * @version @VERSION@ + * @since 3.0 + */ +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 + * reimplemented in extending classes for a more powerfu + * implementation. + */ + public void removeAllComponents() { + LinkedList l = new LinkedList(); + + // Add all components + for (Iterator i = getComponentIterator(); i.hasNext();) + l.add(i.next()); + + // Remove all component + for (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) { + LinkedList components = new LinkedList(); + for (Iterator i = source.getComponentIterator(); i.hasNext();) + components.add(i.next()); + + for (Iterator i = components.iterator(); i.hasNext();) { + Component c = (Component) i.next(); + source.removeComponent(c); + addComponent(c); + } + } + + /** Notifies all contained components that the container is attached to + * a window. + * + * @see com.itmill.toolkit.ui.Component#attach() + */ + public void attach() { + super.attach(); + + for (Iterator i = getComponentIterator(); i.hasNext();) + ((Component)i.next()).attach(); + } + + /** Notifies all contained components that the container is detached + * from a window. + * + * @see com.itmill.toolkit.ui.Component#detach() + */ + public void detach() { + super.detach(); + + for (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 (java.lang.NoSuchMethodException e) { + // This should never happen + throw new java.lang.RuntimeException(); + } + } + + /* 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); + } + + /** Fire 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)); + } + + /** Fire 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 ComponentAttachEvent(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.itmill.toolkit.ui.ComponentContainer#addComponent(Component) + */ + public void addComponent(Component c) { + c.setParent(this); + if (getApplication() != null) + c.attach(); + 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.itmill.toolkit.ui.ComponentContainer#removeComponent(Component) + */ + public void removeComponent(Component c) { + if (getApplication() != null) + c.detach(); + c.setParent(null); + fireComponentDetachEvent(c); + } +} diff --git a/src/com/itmill/toolkit/ui/AbstractField.java b/src/com/itmill/toolkit/ui/AbstractField.java new file mode 100644 index 0000000000..13a20ad23d --- /dev/null +++ b/src/com/itmill/toolkit/ui/AbstractField.java @@ -0,0 +1,922 @@ +/* ************************************************************************* + + IT Mill Toolkit + + Development of Browser User Intarfaces Made Easy + + Copyright (C) 2000-2006 IT Mill Ltd + + ************************************************************************* + + This product is distributed under commercial license that can be found + from the product package on license/license.txt. Use of this product might + require purchasing a commercial license from IT Mill Ltd. For guidelines + on usage, see license/licensing-guidelines.html + + ************************************************************************* + + For more information, contact: + + IT Mill Ltd phone: +358 2 4802 7180 + Ruukinkatu 2-4 fax: +358 2 4802 7181 + 20540, Turku email: info@itmill.com + Finland company www: www.itmill.com + + Primary source for information and releases: www.itmill.com + + ********************************************************************** */ + +package com.itmill.toolkit.ui; + +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 com.itmill.toolkit.data.Buffered; +import com.itmill.toolkit.data.Property; +import com.itmill.toolkit.data.Validatable; +import com.itmill.toolkit.data.Validator; +import com.itmill.toolkit.terminal.ErrorMessage; +import com.itmill.toolkit.terminal.PaintException; +import com.itmill.toolkit.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.itmill.toolkit.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.itmill.toolkit.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.itmill.toolkit.data.Validator validators} + * to make sure the value contained in the field is valid. + * </p> + * + * @author IT Mill Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +public abstract class AbstractField extends AbstractComponent implements Field, + Property.ReadOnlyStatusChangeNotifier { + + /* Private members ************************************************* */ + + private boolean delayedFocus; + + /** Value of the datafield */ + 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; + + /** Read 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 alloved 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; + + /** Unique focusable id */ + private long focusableId = -1; + + /** Required field */ + private boolean required = false; + + /* Component basics ************************************************ */ + + public AbstractField() { + this.focusableId = Window.getNewFocusableId(this); + } + + /* + * Paint the field. Don't add a JavaDoc comment here, we use the default + * documentation from the implemented interface. + */ + public void paintContent(PaintTarget target) throws PaintException { + + // Focus control id + if (this.focusableId > 0) { + target.addAttribute("focusid", this.focusableId); + } + + // The tab ordering number + if (this.tabIndex > 0) + target.addAttribute("tabindex", this.tabIndex); + + // If the field is modified, but not committed, set modified attribute + if (isModified()) + target.addAttribute("modified", true); + + // Add required attribute + if (isRequired()) + target.addAttribute("required", 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 readonly + * mode. + */ + public boolean isReadOnly() { + return super.isReadOnly() + || (dataSource != null && dataSource.isReadOnly()); + } + + /** + * Change the readonly state and throw read-only status change events. + * + * @see com.itmill.toolkit.ui.Component#setReadOnly(boolean) + */ + public void setReadOnly(boolean readOnly) { + super.setReadOnly(readOnly); + fireReadOnlyStatusChange(); + } + + /* Buffering ******************************************************* */ + + public boolean isInvalidCommitted() { + return invalidCommitted; + } + + public void setInvalidCommitted(boolean isCommitted) { + this.invalidCommitted = isCommitted; + } + + /* + * Save 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 { + if (dataSource != null && (isInvalidCommitted() || isValid()) + && !dataSource.isReadOnly()) { + Object newValue = getValue(); + try { + + // Commit the value to datasource + dataSource.setValue(newValue); + + } catch (Throwable e) { + + // Set the buffering state + currentBufferedSourceException = new Buffered.SourceException( + this, e); + requestRepaint(); + + // Throw the source exception + throw currentBufferedSourceException; + } + } + + 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(); + } + + /* + * Update 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) { + + // Get the correct value from datasource + Object newValue; + try { + + // Discard buffer by overwriting from datasource + newValue = dataSource.getValue(); + + // If successful, remove set the buffering state to be ok + if (currentBufferedSourceException != null) { + currentBufferedSourceException = null; + requestRepaint(); + } + } catch (Throwable e) { + + // Set the buffering state + currentBufferedSourceException = new Buffered.SourceException( + this, e); + requestRepaint(); + + // Throw the source exception + throw currentBufferedSourceException; + } + + 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(); + } + + // 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; + } + + /* + * Test 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; + } + + /* + * Set 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 { + if (writeTroughMode == writeTrough) + return; + writeTroughMode = writeTrough; + if (writeTroughMode) + commit(); + } + + /* + * Test 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; + } + + /* + * Set 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(dataSource.getValue()); + fireValueChange(); + } + } + + /* Property interface implementation ******************************* */ + + /** + * Returns the value of the Property in human readable textual format. + * + * @return <code>String</code> representation of the value stored in the + * Property + */ + public String toString() { + Object value = getValue(); + if (value == null) + return null; + return getValue().toString(); + } + + /** + * Gets the current value of the field. 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. + * + * @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 = dataSource.getValue(); + if ((newValue == null && value != null) + || (newValue != null && !newValue.equals(value))) { + setInternalValue(newValue); + fireValueChange(); + } + + return newValue; + } + + /** + * Set the value of the field. + * + * @param newValue + * New value of the field. + */ + public void setValue(Object newValue) 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(); + + // If invalid values are not allowed, the value must be checked + if (!isInvalidAllowed()) { + Collection v = getValidators(); + if (v != null) + for (Iterator i = v.iterator(); i.hasNext();) + ((Validator) i.next()).validate(newValue); + } + + // Change the value + setInternalValue(newValue); + modified = dataSource != null; + + // In write trough mode , try to commit + if (isWriteThrough() && dataSource != null + && (isInvalidCommitted() || isValid())) { + try { + + // Commit the value to datasource + dataSource.setValue(newValue); + + // The buffer is now unmodified + modified = false; + + } catch (Throwable e) { + + // Set the buffering state + currentBufferedSourceException = new Buffered.SourceException( + this, e); + requestRepaint(); + + // Throw the source exception + throw currentBufferedSourceException; + } + } + + // If successful, remove set the buffering state to be ok + if (currentBufferedSourceException != null) { + currentBufferedSourceException = null; + requestRepaint(); + } + + // Fire value change + fireValueChange(); + } + } + + /* 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) { + + // Save the old value + Object oldValue = value; + + // Discard all changes to old datasource + try { + discard(); + } catch (Buffered.SourceException ignored) { + } + + // Stop listening the old data source changes + if (dataSource != null + && Property.ValueChangeNotifier.class + .isAssignableFrom(dataSource.getClass())) + ((Property.ValueChangeNotifier) dataSource).removeListener(this); + + // Set the new data source + dataSource = newDataSource; + + // Get the value from source + try { + if (dataSource != null) + setInternalValue(dataSource.getValue()); + modified = false; + } catch (Throwable e) { + currentBufferedSourceException = new Buffered.SourceException(this, + e); + modified = true; + } + + // Listen 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) { + Collection validators = ((Validatable) dataSource).getValidators(); + if (validators != null) + for (Iterator i = validators.iterator(); i.hasNext();) + addValidator((Validator) i.next()); + } + + // Fire value change if the value has changed + if ((value != oldValue) + && ((value != null && !value.equals(oldValue)) || value == null)) + fireValueChange(); + } + + /* 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); + } + + /** + * Gets the validators of the field. + * + * @return Unmodifiable collection that holds all validators for the field. + */ + public Collection getValidators() { + if (validators == null || validators.isEmpty()) + return null; + return Collections.unmodifiableCollection(validators); + } + + /** + * Removes a validator from the field. + * + * @param validator + * the validator to remove + */ + public void removeValidator(Validator validator) { + if (validators != null) + validators.remove(validator); + } + + /** + * 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 (validators == null) + return true; + + Object value = getValue(); + for (Iterator i = validators.iterator(); i.hasNext();) + if (!((Validator) i.next()).isValid(value)) + return false; + + return true; + } + + public void validate() throws Validator.InvalidValueException { + + // If there is no validator, there can not be any errors + if (validators == null) + return; + + // Initialize temps + Validator.InvalidValueException firstError = null; + LinkedList errors = null; + Object value = getValue(); + + // Get all the validation errors + for (Iterator i = validators.iterator(); i.hasNext();) + try { + ((Validator) i.next()).validate(value); + } catch (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; + + // Create composite validator + Validator.InvalidValueException[] exceptions = new Validator.InvalidValueException[errors + .size()]; + int index = 0; + for (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. + * + * @see com.itmill.toolkit.data.Validatable#isInvalidAllowed() + * + * @return true iff the invalid values are allowed. + */ + 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. + * 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. + * + * @see com.itmill.toolkit.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.itmill.toolkit.ui.AbstractComponent#getErrorMessage() + */ + public ErrorMessage getErrorMessage() { + ErrorMessage superError = super.getErrorMessage(); + return superError; + /* + * TODO: Check the logic of this ErrorMessage validationError = null; + * try { validate(); } catch (Validator.InvalidValueException e) { + * validationError = e; } + * + * 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 (java.lang.NoSuchMethodException e) { + // This should never happen + throw new java.lang.RuntimeException(); + } + } + + /* + * Add 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); + } + + /* + * Remove 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); + } + + /** + * Emit a value change event. The value contained in the field is validated + * before the event is created. + */ + protected void fireValueChange() { + fireEvent(new AbstractField.ValueChangeEvent(this)); + 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 (java.lang.NoSuchMethodException e) { + // This should never happen + throw new java.lang.RuntimeException(); + } + } + + /** + * 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 { + + /** + * Serial generated by eclipse. + */ + private static final long serialVersionUID = 3258688823264161846L; + + /** + * New instance of text change event + * + * @param source + * Source of the event. + */ + public ReadOnlyStatusChangeEvent(AbstractField source) { + super(source); + } + + /** + * Property where the event occurred + * + * @return Source of the event. + */ + public Property getProperty() { + return (Property) getSource(); + } + } + + /* + * Add 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); + } + + /** + * Emit a 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(); + } + + /** Ask the terminal to place the cursor to this field. */ + public void focus() { + Window w = getWindow(); + if (w != null) { + w.setFocusedComponent(this); + } else { + this.delayedFocus = true; + } + } + + /** + * Create abstract field by the type of the property. + * + * <p> + * This returns most suitable field type for editing property of given type + * </p> + * + * @param propertyType + * 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)) { + Button button = new Button(""); + button.setSwitchMode(true); + button.setImmediate(false); + return button; + } + + // Text field is used by default + return new TextField(); + } + + /** + * Get the tab index of this field. The tab index property is used to + * specify the natural tab ordering of fields. + * + * @return Tab index of this field. Negative value means unspecified. + */ + public int getTabIndex() { + return tabIndex; + } + + /** + * Get the tab index of this field. The tab index property is used to + * specify the natural tab ordering of fields. + * + * @param tabIndex + * The tab order of this component. Negative value means + * unspecified. + */ + public void setTabIndex(int tabIndex) { + this.tabIndex = tabIndex; + } + + /** + * Set the internal field value. This is purely used by AbstractField to + * change the internal Field value. It does not trigger any 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) { + this.value = newValue; + } + + /** + * @see com.itmill.toolkit.ui.Component.Focusable#getFocusableId() + */ + public long getFocusableId() { + return this.focusableId; + } + + /** + * @see com.itmill.toolkit.ui.Component#attach() + */ + public void attach() { + super.attach(); + if (this.delayedFocus) { + this.delayedFocus = false; + this.focus(); + } + } + + /** + * Is this field required. + * + * Required fields must filled by the user. + * + * @return true if the + */ + public boolean isRequired() { + return required; + } + + /** + * Set the field required. Required fields must filled by the user. + * + * @param required + * Is the field required + */ + public void setRequired(boolean required) { + this.required = required; + } + + /** + * Free used resources. + */ + public void finalize() throws Throwable { + if (focusableId > -1) { + Window.removeFocusableId(focusableId); + } + super.finalize(); + } +}
\ No newline at end of file diff --git a/src/com/itmill/toolkit/ui/BaseFieldFactory.java b/src/com/itmill/toolkit/ui/BaseFieldFactory.java new file mode 100644 index 0000000000..9ea6d3efc8 --- /dev/null +++ b/src/com/itmill/toolkit/ui/BaseFieldFactory.java @@ -0,0 +1,130 @@ +/* ************************************************************************* + + IT Mill Toolkit + + Development of Browser User Intarfaces Made Easy + + Copyright (C) 2000-2006 IT Mill Ltd + + ************************************************************************* + + This product is distributed under commercial license that can be found + from the product package on license/license.txt. Use of this product might + require purchasing a commercial license from IT Mill Ltd. For guidelines + on usage, see license/licensing-guidelines.html + + ************************************************************************* + + For more information, contact: + + IT Mill Ltd phone: +358 2 4802 7180 + Ruukinkatu 2-4 fax: +358 2 4802 7181 + 20540, Turku email: info@itmill.com + Finland company www: www.itmill.com + + Primary source for information and releases: www.itmill.com + + ********************************************************************** */ + +package com.itmill.toolkit.ui; + +import java.util.Date; + +import com.itmill.toolkit.data.Container; +import com.itmill.toolkit.data.Item; +import com.itmill.toolkit.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) + * <b>Item</b>: Form<br/> + * <b>default field type</b>: TextField + * <p> + * @author IT Mill Ltd. + * @version @VERSION@ + * @since 3.1 + */ + +public class BaseFieldFactory implements FieldFactory { + + /** Creates 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.itmill.toolkit.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)) { + DateField df = new DateField(); + df.setResolution(DateField.RESOLUTION_DAY); + return df; + } + + // Boolean field + if (Boolean.class.isAssignableFrom(type)) { + Button button = new Button(); + button.setSwitchMode(true); + button.setImmediate(false); + return button; + } + + // Nested form is used by default + return new TextField(); + } + + /** Create field based on the datasource property. + * + * @see com.itmill.toolkit.ui.FieldFactory#createField(Property, Component) + */ + public Field createField(Property property, Component uiContext) { + if (property != null) + return createField(property.getType(),uiContext); + else + return null; + } + + /** Creates field based on the item and property id. + * + * @see com.itmill.toolkit.ui.FieldFactory#createField(Item, Object, Component) + */ + public Field createField(Item item, Object propertyId, Component uiContext) { + if (item != null && propertyId != null){ + Field f= createField(item.getItemProperty(propertyId),uiContext); + if (f instanceof AbstractComponent) + ((AbstractComponent)f).setCaption(propertyId.toString()); + return f; + } + else + return null; + } + + /** + * @see com.itmill.toolkit.ui.FieldFactory#createField(com.itmill.toolkit.data.Container, java.lang.Object, java.lang.Object, com.itmill.toolkit.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/itmill/toolkit/ui/Button.java b/src/com/itmill/toolkit/ui/Button.java new file mode 100644 index 0000000000..1b1d22d3d9 --- /dev/null +++ b/src/com/itmill/toolkit/ui/Button.java @@ -0,0 +1,286 @@ +/* ************************************************************************* + + IT Mill Toolkit + + Development of Browser User Intarfaces Made Easy + + Copyright (C) 2000-2006 IT Mill Ltd + + ************************************************************************* + + This product is distributed under commercial license that can be found + from the product package on license/license.txt. Use of this product might + require purchasing a commercial license from IT Mill Ltd. For guidelines + on usage, see license/licensing-guidelines.html + + ************************************************************************* + + For more information, contact: + + IT Mill Ltd phone: +358 2 4802 7180 + Ruukinkatu 2-4 fax: +358 2 4802 7181 + 20540, Turku email: info@itmill.com + Finland company www: www.itmill.com + + Primary source for information and releases: www.itmill.com + + ********************************************************************** */ + +package com.itmill.toolkit.ui; + +import java.util.Map; +import java.lang.reflect.Method; + +import com.itmill.toolkit.data.Property; +import com.itmill.toolkit.terminal.PaintException; +import com.itmill.toolkit.terminal.PaintTarget; + +/** A generic button component. + * + * @author IT Mill Ltd. + * @version @VERSION@ + * @since 3.0 + */ +public class Button extends AbstractField { + + /* Final members *************************************************** */ + + /** strings to be catched at adatapter (transformer) */ + private static final String BUTTON_VAR_NAME = "clicked"; + + /* Private members ************************************************* */ + + boolean switchMode = false; + + /** Creates a new push button. + * + * The value of the push button is allways false and they are + * immediate by default. + * + */ + public Button() { + setSwitchMode(false); + } + + /** Creates a new push button. + * + * The value of the push button is allways false and they are + * immediate by default. + * + * @param caption Button caption + */ + public Button(String caption) { + setCaption(caption); + setSwitchMode(false); + } + + /** Creates a new push button with click listener. + * @param caption Button caption + * @param listener Button click listener + */ + public Button(String caption, ClickListener listener) { + this(caption); + addListener(listener); + } + + /** Creates a new push button with a method listening button clicks. + * The method must have either no parameters, or only one parameter of + * Button.ClickEvent type. + * @param caption Button caption + * @param target 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 new switch button with initial value. + * @param state Initial state of the switch-button. + */ + public Button(String caption, boolean initialState) { + setCaption(caption); + setValue(new Boolean(initialState)); + setSwitchMode(true); + } + + /** Creates new switch button that is connected to a boolean property. + * @param state Initial state of the switch-button. + */ + public Button(String caption, Property dataSource) { + setCaption(caption); + setSwitchMode(true); + setPropertyDataSource(dataSource); + } + + /** Get component UIDL tag. + * @return Component UIDL tag as string. + */ + public String getTag() { + return "button"; + } + + /** Paint the content of this component. + * @param event PaintEvent. + * @throws IOException Passed from the UIDLStream. + * @throws PaintException The paint operation failed. + */ + public void paintContent(PaintTarget target) throws PaintException { + super.paintContent(target); + + if (isSwitchMode()) + target.addAttribute("type", "switch"); + boolean state; + try { + state = ((Boolean) getValue()).booleanValue(); + } catch (NullPointerException e) { + state = false; + } + target.addVariable(this, "state", state); + } + + /** Invoked when the value of a variable has changed. Button + * listeners are notified if the button is clicked. + * @param event Variable change event. + */ + public void changeVariables(Object source, Map variables) { + if (variables.containsKey("state")) { + // Get the new and old button states + Boolean newValue = (Boolean) variables.get("state"); + 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)); + } + } + } + + /** + * Returns the switchMode. + * @return boolean + */ + 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); + setValue(new Boolean(false)); + } + } + + /** Set immediate mode. + * @see com.itmill.toolkit.ui.AbstractComponent#setImmediate(boolean) + * + * Push buttons can not be set in non-immediate mode. + */ + public void setImmediate(boolean immediate) { + // Push buttons are allways immediate + super.setImmediate(!isSwitchMode() || immediate); + } + + /** The type of the button as a property. + * @see com.itmill.toolkit.data.Property#getType() + */ + public Class getType() { + return Boolean.class; + } + + /* Click event ************************************************ */ + + private static final Method BUTTON_CLICK_METHOD; + static { + try { + BUTTON_CLICK_METHOD = + ClickListener.class.getDeclaredMethod( + "buttonClick", + new Class[] { ClickEvent.class }); + } catch (java.lang.NoSuchMethodException e) { + // This should never happen + throw new java.lang.RuntimeException(); + } + } + + /** 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 { + + /** + * Serial generated by eclipse. + */ + private static final long serialVersionUID = 3546647602931118393L; + + /** New instance of text change event + * @param source Source of the event. + */ + public ClickEvent(Component source) { + super(source); + } + + /** Button where the event occurred + * @return 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 { + + /** Button has been pressed. + * @param event Button click event. + */ + public void buttonClick(ClickEvent event); + } + + /** Add button click listener + * @param listener Listener to be added. + */ + public void addListener(ClickListener listener) { + addListener(ClickEvent.class, listener, BUTTON_CLICK_METHOD); + } + + /** Remove button click listener + * @param listener Listener to be removed. + */ + public void removeListener(ClickListener listener) { + removeListener(ClickEvent.class, listener, BUTTON_CLICK_METHOD); + } + + /** Emit options change event. */ + protected void fireClick() { + fireEvent(new Button.ClickEvent(this)); + } +} diff --git a/src/com/itmill/toolkit/ui/Component.java b/src/com/itmill/toolkit/ui/Component.java new file mode 100644 index 0000000000..d1e6bb8039 --- /dev/null +++ b/src/com/itmill/toolkit/ui/Component.java @@ -0,0 +1,324 @@ +/* ************************************************************************* + + IT Mill Toolkit + + Development of Browser User Intarfaces Made Easy + + Copyright (C) 2000-2006 IT Mill Ltd + + ************************************************************************* + + This product is distributed under commercial license that can be found + from the product package on license/license.txt. Use of this product might + require purchasing a commercial license from IT Mill Ltd. For guidelines + on usage, see license/licensing-guidelines.html + + ************************************************************************* + + For more information, contact: + + IT Mill Ltd phone: +358 2 4802 7180 + Ruukinkatu 2-4 fax: +358 2 4802 7181 + 20540, Turku email: info@itmill.com + Finland company www: www.itmill.com + + Primary source for information and releases: www.itmill.com + + ********************************************************************** */ + +package com.itmill.toolkit.ui; + +import com.itmill.toolkit.Application; +import com.itmill.toolkit.terminal.ErrorMessage; +import com.itmill.toolkit.terminal.Paintable; +import com.itmill.toolkit.terminal.Resource; +import com.itmill.toolkit.terminal.VariableOwner; + +import java.util.Collection; +import java.util.EventListener; +import java.util.EventObject; +import java.util.Locale; + +/** The top-level component interface which must be implemented by all + * MillStone UI components. It contains the methods the MillStone framework + * needs to handle the components in a user interface. + * + * @author IT Mill Ltd. + * @version @VERSION@ + * @since 3.0 + */ +public interface Component extends Paintable, VariableOwner { + + /** Gets the look-and-feel style of the component. + * + * @return component's styleValue of property style. + */ + public String getStyle(); + + /** Sets the look-and-feel style of the component. This method will + * trigger a {@link com.itmill.toolkit.terminal.Paintable.RepaintRequestEvent RepaintRequestEvent}. + * . + * @param style new style of the component + */ + public void setStyle(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>Components should be enabled by default.</p> + * + * @return <code>true</code> if the component is enabled, + * <code>false</code> if not + * @see VariableOwner#isEnabled() + */ + public boolean isEnabled(); + + /** Enable or disable the component. Being enabled means that the + * component can be edited. This method will trigger a + * {@link com.itmill.toolkit.terminal.Paintable.RepaintRequestEvent RepaintRequestEvent}. + * + * @param enabled boolean value specifying if the component should be + * enabled after the call or not + */ + public void setEnabled(boolean enabled); + + /** Tests if the component is visible or not. Visibility defines if the + * component is shown in the UI or not. Default is <code>true</code>. + * + * @return <code>true</code> if the component is visible in the UI, + * <code>false</code> if not + */ + public boolean isVisible(); + + /** Sets the components visibility status. Visibility defines if the + * component is shown in the UI or not. + * + * @param visible 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.itmill.toolkit.terminal.Paintable.RepaintRequestEvent RepaintRequestEvent}. + * + * @param readOnly 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 component's caption <code>String</code> + */ + public String getCaption(); + + /** 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(); + + /** Gets the component's parent window. If the component does not yet + * belong to a window <code>null</code> is returned. + * + * @return 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 parent application of the component or <code>null</code> + */ + public Application getApplication(); + + /** 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> functions 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 Locale.getDefautl(). 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 A 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 { + + /** + * Serial generated by eclipse. + */ + private static final long serialVersionUID = 4048791277653274933L; + + /** Constructs a new event with a specified source component. + * + * @param source 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 { + + /** 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. */ + public class ErrorEvent extends Event { + + /** + * Serial generated by eclipse. + */ + private static final long serialVersionUID = 4051323457293857333L; + + private ErrorMessage message; + private Throwable throwable; + + /** Constructs a new event with a specified source component. + * + * @param source source component of the event + */ + public ErrorEvent( + ErrorMessage message, + Component component) { + super(component); + this.message = message; + } + + /** Return the error message */ + public ErrorMessage getErrorMessage() { + return this.message; + } + } + + /** Listener interface for receiving <code>Component.Errors</code>s */ + public interface ErrorListener extends EventListener { + + /** 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 { + + /** Set focus to this component.*/ + public void focus(); + + /** Get Tabulator index of this Focusable component. + * @return Positive tab order of this focusable. Negative of zero means + * unspecified tab order. + */ + public int getTabIndex(); + + /** Set Tabulator index of this Focusable component. + * @param tabIndex Positive tab order of this focusable. Negative of + * zero means unspecified tab order. + */ + public void setTabIndex(int tabIndex); + + /** Get unique ID of focusable. + * This will be used to move input focus directly to this + * component. + * @return Unique id of focusable. + */ + public long getFocusableId(); + + } +} diff --git a/src/com/itmill/toolkit/ui/ComponentContainer.java b/src/com/itmill/toolkit/ui/ComponentContainer.java new file mode 100644 index 0000000000..0ea1b9b9c4 --- /dev/null +++ b/src/com/itmill/toolkit/ui/ComponentContainer.java @@ -0,0 +1,173 @@ +/* ************************************************************************* + + IT Mill Toolkit + + Development of Browser User Intarfaces Made Easy + + Copyright (C) 2000-2006 IT Mill Ltd + + ************************************************************************* + + This product is distributed under commercial license that can be found + from the product package on license/license.txt. Use of this product might + require purchasing a commercial license from IT Mill Ltd. For guidelines + on usage, see license/licensing-guidelines.html + + ************************************************************************* + + For more information, contact: + + IT Mill Ltd phone: +358 2 4802 7180 + Ruukinkatu 2-4 fax: +358 2 4802 7181 + 20540, Turku email: info@itmill.com + Finland company www: www.itmill.com + + Primary source for information and releases: www.itmill.com + + ********************************************************************** */ + +package com.itmill.toolkit.ui; + +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 a component into this container. + * + * @param c the component to be added + */ + public void addComponent(Component c); + + /** Removes a 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(); + + /** Replace a 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 component iterator + */ + public Iterator getComponentIterator(); + + /** 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); + + /** Listen component attach events */ + public void addListener(ComponentAttachListener listener); + + /** Stop listening component attach events */ + public void removeListener(ComponentAttachListener listener); + + /** Listen component detach events */ + public void addListener(ComponentDetachListener listener); + + /** Stop listening component detach events */ + public void removeListener(ComponentDetachListener listener); + + /** Component attach listener interface. */ + public interface ComponentAttachListener { + + /** A new component is attached to container */ + public void componentAttachedToContainer(ComponentAttachEvent event); + } + + /** Component detach listener interface. */ + public interface ComponentDetachListener { + + /** A component has been detached from container */ + public void componentDetachedFromContainer(ComponentDetachEvent event); + } + + /** Component attach event sent when a component is attached to container */ + public class ComponentAttachEvent extends Component.Event { + + /** + * Serial generated by eclipse. + */ + private static final long serialVersionUID = 3257285812184692019L; + + private Component component; + + /** Create 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); + this.component = attachedComponent; + } + + /** Get the component container */ + public ComponentContainer getContainer() { + return (ComponentContainer) getSource(); + } + + /** Get the attached component */ + public Component getAttachedComponent() { + return component; + } + } + + /** Component detach event sent when a component is detached from container */ + public class ComponentDetachEvent extends Component.Event { + + /** + * Serial generated by eclipse. + */ + private static final long serialVersionUID = 3618140052337930290L; + + private Component component; + + /** Create 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); + this.component = detachedComponent; + } + + /** Get the component container */ + public ComponentContainer getContainer() { + return (ComponentContainer) getSource(); + } + + /** Get the detached component */ + public Component getDetachedComponent() { + return component; + } + } + +} diff --git a/src/com/itmill/toolkit/ui/CustomComponent.java b/src/com/itmill/toolkit/ui/CustomComponent.java new file mode 100644 index 0000000000..1df4abc134 --- /dev/null +++ b/src/com/itmill/toolkit/ui/CustomComponent.java @@ -0,0 +1,387 @@ +/* ************************************************************************* + + IT Mill Toolkit + + Development of Browser User Intarfaces Made Easy + + Copyright (C) 2000-2006 IT Mill Ltd + + ************************************************************************* + + This product is distributed under commercial license that can be found + from the product package on license/license.txt. Use of this product might + require purchasing a commercial license from IT Mill Ltd. For guidelines + on usage, see license/licensing-guidelines.html + + ************************************************************************* + + For more information, contact: + + IT Mill Ltd phone: +358 2 4802 7180 + Ruukinkatu 2-4 fax: +358 2 4802 7181 + 20540, Turku email: info@itmill.com + Finland company www: www.itmill.com + + Primary source for information and releases: www.itmill.com + + ********************************************************************** */ + +package com.itmill.toolkit.ui; + +import java.util.Collection; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +import com.itmill.toolkit.Application; +import com.itmill.toolkit.terminal.PaintException; +import com.itmill.toolkit.terminal.PaintTarget; +import com.itmill.toolkit.terminal.Resource; +import com.itmill.toolkit.terminal.VariableOwner; + +/** 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 + */ +public class CustomComponent implements Component { + + /** The root component implementing the custom component */ + private Component root = null; + + /** The visibility of the component */ + private boolean visible = true; + + /** The parent of the component */ + private Component parent = null; + + /** Dependencies of the component, or null */ + private HashSet dependencies = null; + + /** Type of the component */ + private String componentType = null; + + /** 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; + + /** Construct 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() { + } + + /** Construct 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) { + setCompositionRoot(compositionRoot); + } + + /** Returns the composition root. + * @return 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 && root != null) + root.setParent(null); + this.root = compositionRoot; + if (root != null) + root.setParent(this); + } + + /* Basic component features ------------------------------------------ */ + + public void attach() { + if (root != null) + root.attach(); + } + + public void detach() { + if (root != null) + root.detach(); + } + + public Application getApplication() { + if (parent == null) + return null; + return parent.getApplication(); + } + + /** The caption of the custom component is by default the the + * caption of the root component, or null if the root is not set + */ + public String getCaption() { + if (root == null) + return null; + return root.getCaption(); + } + + /** The icon of the custom component is by default the the + * icon of the root component, or null if the root is not set + */ + public Resource getIcon() { + if (root == null) + return null; + return root.getIcon(); + } + + /** The icon of the custom component is by default the the + * locale of the parent or null if the parent is not set. + */ + public Locale getLocale() { + if (parent == null) + return null; + return parent.getLocale(); + } + + public Component getParent() { + return parent; + } + + /** Custom component does not implement custom styles by default and + * this function returns null. + */ + public String getStyle() { + return null; + } + + public Window getWindow() { + if (parent == null) + return null; + return parent.getWindow(); + } + + /** Custom component is allways enabled by default */ + public boolean isEnabled() { + return true; + } + + /** Custom component is by default in the non-immediate mode. The immediateness + * of the custom component is defined by the components it is composed of. + */ + public boolean isImmediate() { + return false; + } + + /** The custom components are not readonly by default. */ + public boolean isReadOnly() { + return false; + } + + public boolean isVisible() { + return visible; + } + + /* 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) { + + // Notify listeners only once + if (!repaintRequestListenersNotified) { + + // Notify the listeners + if (repaintRequestListeners != null + && !repaintRequestListeners.isEmpty()) { + Object[] listeners = repaintRequestListeners.toArray(); + 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 + 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; + } + } + + /** The custom component is allways enabled by default. */ + public void setEnabled(boolean enabled) { + } + + public void setParent(Component parent) { + + // If the parent is not changed, dont do nothing + if (parent == this.parent) + return; + + // Send detach event if the component have been connected to a window + if (getApplication() != null) { + detach(); + this.parent = null; + } + + // Connect to new parent + this.parent = parent; + + // Send attach event if connected to a window + if (getApplication() != null) + attach(); + } + + /** Changing the read-only mode of the component is not supported by default. + */ + public void setReadOnly(boolean readOnly) { + } + + /** Changing the style of the component is not supported by default. + */ + public void setStyle(String style) { + } + + public void setVisible(boolean visible) { + this.visible = visible; + } + + /* Documented in super interface */ + public void requestRepaintRequests() { + repaintRequestListenersNotified = false; + } + + /* Documented in super interface */ + public void paint(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 (isVisible()) { + String type = getComponentType(); + if (type != null) { + if (!target.startTag(this, "component")) { + target.addAttribute("type", type); + root.paint(target); + } + target.endTag("component"); + } else + root.paint(target); + } + repaintRequestListenersNotified = false; + } + + /** The custom component does not have any variables by default */ + public void changeVariables(Object source, Map variables) { + } + + public void dependsOn(VariableOwner depended) { + if (depended == null) + return; + if (dependencies == null) + dependencies = new HashSet(); + dependencies.add(depended); + } + + public Set getDirectDependencies() { + return dependencies; + } + + public void removeDirectDependency(VariableOwner depended) { + if (dependencies == null) + return; + dependencies.remove(depended); + if (dependencies.isEmpty()) + dependencies = null; + } + + /* Event functions are not implemented by default -------------------- */ + + /** Custom component does not implement any component events by default */ + public void addListener(Component.Listener listener) { + } + + /** Custom component does not implement any component events by default */ + public void removeListener(Component.Listener listener) { + } + + /** Gets the component type. + * + * The component type is textual type of the component. This is included + * in the UIDL as component tag attribute. If the component type is null + * (default), the component tag is not included in the UIDL at all. + * + * Returns the componentType. + * @return String + */ + 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. If the component type is null + * (default), the component tag is not included in the UIDL at all. + * + * @param componentType The componentType to set + */ + public void setComponentType(String componentType) { + this.componentType = componentType; + } + +} diff --git a/src/com/itmill/toolkit/ui/CustomLayout.java b/src/com/itmill/toolkit/ui/CustomLayout.java new file mode 100644 index 0000000000..c3ced267d4 --- /dev/null +++ b/src/com/itmill/toolkit/ui/CustomLayout.java @@ -0,0 +1,183 @@ +/* ************************************************************************* + + IT Mill Toolkit + + Development of Browser User Intarfaces Made Easy + + Copyright (C) 2000-2006 IT Mill Ltd + + ************************************************************************* + + This product is distributed under commercial license that can be found + from the product package on license/license.txt. Use of this product might + require purchasing a commercial license from IT Mill Ltd. For guidelines + on usage, see license/licensing-guidelines.html + + ************************************************************************* + + For more information, contact: + + IT Mill Ltd phone: +358 2 4802 7180 + Ruukinkatu 2-4 fax: +358 2 4802 7181 + 20540, Turku email: info@itmill.com + Finland company www: www.itmill.com + + Primary source for information and releases: www.itmill.com + + ********************************************************************** */ + +package com.itmill.toolkit.ui; + +import com.itmill.toolkit.terminal.PaintException; +import com.itmill.toolkit.terminal.PaintTarget; + +import java.util.Iterator; +import java.util.HashMap; + +/** <p>A container component with freely designed layout and style. The + * container consists of items with textually represented locations. Each + * item contains one sub-component. The adapter and theme are resposible for + * rendering the layout with given style by placing the items on the screen + * in defined locations.</p> + * + * <p>The definition of locations is not fixed - the each style can define + * its locations in a way that is suitable for it. One typical example would + * be to create visual design for a website as a custom layout: the visual + * design could define locations for "menu", "body" and "title" for example. + * The layout would then be implemented as XLS-template with for given + * style.</p> + * + * <p>The default theme handles the styles that are not defined by just + * drawing the subcomponents with flowlayout.</p> + * + * @author IT Mill Ltd. + * @version @VERSION@ + * @since 3.0 + */ +public class CustomLayout extends AbstractComponentContainer implements Layout { + + /** Custom layout slots containing the components */ + private HashMap slots = new HashMap(); + + /** Constructor for custom layout with given style */ + public CustomLayout(String style) { + setStyle(style); + } + + /** Get component UIDL tag. + * @return Component UIDL tag as string. + */ + public String getTag() { + return "customlayout"; + } + + /** Add a component into this container to given location. + * @param c The component to be added. + * @param location The location of the component + */ + public void addComponent(Component c, String location) { + Component old = (Component)slots.get(location); + if (old != null) { + removeComponent(old); + } + slots.put(location, c); + c.setParent(this); + fireComponentAttachEvent(c); + requestRepaint(); + } + + /** Add a 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. + */ + public void addComponent(Component c) { + this.addComponent(c, ""); + } + + /** Remove a component from this container. + * @param c The component to be removed. + */ + public void removeComponent(Component c) { + if (c == null) return; + slots.values().remove(c); + c.setParent(null); + fireComponentDetachEvent(c); + requestRepaint(); + } + + /** Remove a component from this container from given location. + * @param location Location identifier of the component + */ + public void removeComponent(String location) { + this.removeComponent((Component) slots.get(location)); + } + + /** Get component container iterator for going trough all the components in + * the container. + * @return Iterator of the components inside the container. + */ + public Iterator getComponentIterator() { + return slots.values().iterator(); + } + + /** Get child-component by its location. + * + * @param location The name of the location where the requested + * component resides + * @return Component in the given location or null if not found. + */ + public Component getComponent(String location) { + return (Component) slots.get(location); + } + + /** Paint the content of this component. + * @param event PaintEvent. + * @throws PaintException The paint operation failed. + */ + public void paintContent(PaintTarget target) throws PaintException { + + // Add all items in all the locations + for (Iterator i = slots.keySet().iterator(); i.hasNext();) { + + // Get the (location,component) + String location = (String) i.next(); + Component c = (Component) slots.get(location); + + // Write 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) { + + // Get the locations + String oldLocation = null; + String newLocation = null; + for (Iterator i=slots.keySet().iterator(); i.hasNext();) { + String location = (String) i.next(); + 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(); + } + } + +} diff --git a/src/com/itmill/toolkit/ui/DateField.java b/src/com/itmill/toolkit/ui/DateField.java new file mode 100644 index 0000000000..facb8d3f30 --- /dev/null +++ b/src/com/itmill/toolkit/ui/DateField.java @@ -0,0 +1,407 @@ +/* ************************************************************************* + + IT Mill Toolkit + + Development of Browser User Intarfaces Made Easy + + Copyright (C) 2000-2006 IT Mill Ltd + + ************************************************************************* + + This product is distributed under commercial license that can be found + from the product package on license/license.txt. Use of this product might + require purchasing a commercial license from IT Mill Ltd. For guidelines + on usage, see license/licensing-guidelines.html + + ************************************************************************* + + For more information, contact: + + IT Mill Ltd phone: +358 2 4802 7180 + Ruukinkatu 2-4 fax: +358 2 4802 7181 + 20540, Turku email: info@itmill.com + Finland company www: www.itmill.com + + Primary source for information and releases: www.itmill.com + + ********************************************************************** */ + +package com.itmill.toolkit.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.itmill.toolkit.data.Property; +import com.itmill.toolkit.terminal.PaintException; +import com.itmill.toolkit.terminal.PaintTarget; + +/** + * <p> + * A date editor component that can be bound to any bindable Property. that is + * compatible with java.util.Date. + * + * <p> + * Since <code>DateField</code> extends <code>AbstractField</code> it + * implements the {@link com.itmill.toolkit.data.Buffered}interface. A + * <code>DateField</code> is in write-through mode by default, so + * {@link com.itmill.toolkit.ui.AbstractField#setWriteThrough(boolean)}must be + * called to enable buffering. + * </p> + * + * @author IT Mill Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +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; + + /** 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; + + /* 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 + * 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.itmill.toolkit.data.Property.Viewer#setPropertyDataSource(Property)} + * is called to bind it. + * + * @param caption + * caption <code>String</code> for the editor + * @param text + * initial text content of the editor + */ + public DateField(String caption, Date value) { + setValue(value); + setCaption(caption); + } + + /* Component basic features ********************************************* */ + + /* + * Paint this component. Don't add a JavaDoc comment here, we use the + * default documentation from implemented interface. + */ + public void paintContent(PaintTarget target) throws PaintException { + super.paintContent(target); + + // Add locale as attribute + Locale l = getLocale(); + if (l != null) { + target.addAttribute("locale",l.toString()); + } + + // Get the calendar + Calendar calendar = getCalendar(); + 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. + */ + 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. + */ + public void changeVariables(Object source, Map variables) { + + if (!isReadOnly() + && (variables.containsKey("year") + || variables.containsKey("month") + || variables.containsKey("day") + || variables.containsKey("hour") + || variables.containsKey("min") + || variables.containsKey("sec") || variables + .containsKey("msec"))) { + + // Old and new dates + Date oldDate = (Date) getValue(); + Date newDate = null; + + // Get 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 + Calendar cal = (Calendar) 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; + + // Set 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); + + // Assign the date + newDate = cal.getTime(); + } + + if (newDate != oldDate + && (newDate == null || !newDate.equals(oldDate))) + setValue(newDate); + } + } + + /* Property features **************************************************** */ + + /* + * Gets the edited property's type. Don't add a JavaDoc comment here, we use + * the default documentation from implemented interface. + */ + public Class getType() { + return Date.class; + } + + /* + * Return 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. + */ + public String toString() { + Date value = (Date) getValue(); + if (value != null) + return value.toString(); + return null; + } + + /* + * Set the value of the property. Don't add a JavaDoc comment here, we use + * the default documentation from implemented interface. + */ + public void setValue(Object newValue) throws Property.ReadOnlyException, + Property.ConversionException { + + // Allow setting dates directly + if (newValue == null || newValue instanceof Date) + super.setValue(newValue); + else { + + // Try to parse as string + try { + SimpleDateFormat parser = new SimpleDateFormat(); + Date val = parser.parse(newValue.toString()); + super.setValue(val); + } catch (ParseException e) { + throw new Property.ConversionException(e.getMessage()); + } + } + } + + /** + * Set DateField datasource. Datasource type must assignable to Date. + * + * @see com.itmill.toolkit.data.Property.Viewer#setPropertyDataSource(Property) + */ + 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"); + } + + /** + * Returns 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 Calendar.getInstance() is used. + * + * @see #setCalendar(Calendar) + * @return Calendar + */ + private Calendar getCalendar() { + + // Make sure we have an calendar instance + if (this.calendar == null) { + this.calendar = Calendar.getInstance(); + } + + // Clone the instance + Calendar newCal = (Calendar) this.calendar.clone(); + + // Assign the current time tom calendar. + Date currentDate = (Date) getValue(); + if (currentDate != null) + newCal.setTime(currentDate); + + return newCal; + } +} diff --git a/src/com/itmill/toolkit/ui/Embedded.java b/src/com/itmill/toolkit/ui/Embedded.java new file mode 100644 index 0000000000..9422077c14 --- /dev/null +++ b/src/com/itmill/toolkit/ui/Embedded.java @@ -0,0 +1,433 @@ +/* ************************************************************************* + + IT Mill Toolkit + + Development of Browser User Intarfaces Made Easy + + Copyright (C) 2000-2006 IT Mill Ltd + + ************************************************************************* + + This product is distributed under commercial license that can be found + from the product package on license/license.txt. Use of this product might + require purchasing a commercial license from IT Mill Ltd. For guidelines + on usage, see license/licensing-guidelines.html + + ************************************************************************* + + For more information, contact: + + IT Mill Ltd phone: +358 2 4802 7180 + Ruukinkatu 2-4 fax: +358 2 4802 7181 + 20540, Turku email: info@itmill.com + Finland company www: www.itmill.com + + Primary source for information and releases: www.itmill.com + + ********************************************************************** */ + +package com.itmill.toolkit.ui; + +import java.util.Hashtable; +import java.util.Iterator; + +import com.itmill.toolkit.terminal.PaintException; +import com.itmill.toolkit.terminal.PaintTarget; +import com.itmill.toolkit.terminal.Resource; +import com.itmill.toolkit.terminal.Sizeable; + +/** Component for embedding external objects. + * + * @author IT Mill Ltd. + * @version @VERSION@ + * @since 3.0 + */ +public class Embedded extends AbstractComponent implements Sizeable { + + /** General object type */ + public static final int TYPE_OBJECT = 0; + + /** Image types */ + public static final int TYPE_IMAGE = 1; + + /** Type of the object */ + private int type = TYPE_OBJECT; + + /** Source of the embedded object */ + private Resource source = null; + + /** Dimensions of the object. */ + private int width = -1; + private int height = -1; + private int widthUnits = Sizeable.UNITS_PIXELS; + private int heightUnits = Sizeable.UNITS_PIXELS; + + /** Generic object attributes */ + private String mimeType = null; + private String standby = null; + + /** Hash of object parameteres. */ + private 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. + */ + 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. + */ + public Embedded(String caption, Resource source) { + setCaption(caption); + setSource(source); + } + + /** Get component UIDL tag. + * @return Component UIDL tag as string. + */ + public String getTag() { + return "embedded"; + } + + /** Invoked when the component state should be painted */ + public void paintContent(PaintTarget target) throws PaintException { + + if (type == TYPE_IMAGE) { + target.addAttribute("type", "image"); + } + + if (source != null) + target.addAttribute("src", source); + + // Dimensions + if (width > 0) + target.addAttribute( + "width", + "" + width + Sizeable.UNIT_SYMBOLS[this.widthUnits]); + if (height > 0) + target.addAttribute( + "height", + "" + height + Sizeable.UNIT_SYMBOLS[this.heightUnits]); + 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 (Iterator i = this.getParameterNames(); i.hasNext();) { + target.startTag("embeddedparam"); + String key = (String) i.next(); + target.addAttribute("name", key); + target.addAttribute("value", (String) getParameter(key)); + target.endTag("embeddedparam"); + } + } + + /** Set 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(); + } + + /** Get 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 Value of parameter or null if not found. + */ + public String getParameter(String name) { + return (String) parameters.get(name); + } + + /** Remove an object parameter from the list. + * @param name - The name of the parameter to remove. + */ + public void removeParameter(String name) { + parameters.remove(name); + requestRepaint(); + } + + /** Get embedded object parameter names. + * @return Iterator of parameters names. + */ + public Iterator getParameterNames() { + return parameters.keySet().iterator(); + } + + /** + * Returns the codebase, the root-path used to access resources with relative paths. + * @return String + */ + public String getCodebase() { + return codebase; + } + + /** + * Returns the MIME-Type of the code. + * @return String + */ + public String getCodetype() { + return codetype; + } + + /** + * Returns the MIME-Type of the object + * @return String + */ + public String getMimeType() { + return mimeType; + } + + /** + * Returns the standby text displayed when + * the object is loading. + * @return String + */ + 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(); + } + } + + /** + * Returns the visual height of the object. + * Default height is -1, which is interpreted as "unspecified". + * @return The height in units specified by heightUnits property. + */ + public int getHeight() { + return height; + } + + /** + * Returns the visual width of the object. + * Default width is -1, which is interpreted as "unspecified". + * @return The width in units specified by widthUnits property. + */ + public int getWidth() { + return width; + } + + /** Sets the visual height of the object. + * Default height is -1, which is interpreted as "unspecified". + * @param height The height in units specified by heightUnits property. + */ + public void setHeight(int height) { + if (this.height != height) { + this.height = height; + requestRepaint(); + } + } + + /** Sets the visual width of the object. + * Default width is -1, which is interpreted as "unspecified". + * @param width The width in units specified by widthUnits property. + */ + public void setWidth(int width) { + if (this.width != width) { + this.width = width; + requestRepaint(); + } + } + + /** + * Returns the classId attribute. + * @return String + */ + 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(); + } + } + + /** Get the resource contained in the embedded object. + * @return Resource + */ + public Resource getSource() { + return source; + } + + /** Get 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 int + */ + public int getType() { + return type; + } + + /** Set 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; + String mt = source.getMIMEType(); + if ((mt.substring(0, mt.indexOf("/")).equalsIgnoreCase("image"))) { + type = TYPE_IMAGE; + } else { + type = TYPE_OBJECT; + } + 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) + throw new IllegalArgumentException("Unsupported type"); + if (type != this.type) { + this.type = type; + requestRepaint(); + } + } + + /** + * Returns the archive attribute. + * @return String + */ + 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(); + } + } + + /**Get height property units. + * Default units are <code>Sizeable.UNITS_PIXELS</code>. + * @see com.itmill.toolkit.terminal.Sizeable#getHeightUnits() + */ + public int getHeightUnits() { + return this.heightUnits; + } + + /**Get width property units. + * Default units are <code>Sizeable.UNITS_PIXELS</code>. + * @see com.itmill.toolkit.terminal.Sizeable#getWidthUnits() + */ + public int getWidthUnits() { + return this.widthUnits; + } + + /**Set height property units. + * @see com.itmill.toolkit.terminal.Sizeable#setHeightUnits(int) + */ + public void setHeightUnits(int units) { + if (units >= 0 && units <= Sizeable.UNITS_PERCENTAGE && this.heightUnits != units) { + this.heightUnits = units; + requestRepaint(); + } + } + + /**Set width property units. + * @see com.itmill.toolkit.terminal.Sizeable#setWidthUnits(int) + */ + public void setWidthUnits(int units) { + if (units >= 0 && units <= Sizeable.UNITS_PERCENTAGE && this.widthUnits != units) { + this.widthUnits = units; + requestRepaint(); + } + } + +} diff --git a/src/com/itmill/toolkit/ui/Field.java b/src/com/itmill/toolkit/ui/Field.java new file mode 100644 index 0000000000..837162bbde --- /dev/null +++ b/src/com/itmill/toolkit/ui/Field.java @@ -0,0 +1,104 @@ +/* ************************************************************************* + + IT Mill Toolkit + + Development of Browser User Intarfaces Made Easy + + Copyright (C) 2000-2006 IT Mill Ltd + + ************************************************************************* + + This product is distributed under commercial license that can be found + from the product package on license/license.txt. Use of this product might + require purchasing a commercial license from IT Mill Ltd. For guidelines + on usage, see license/licensing-guidelines.html + + ************************************************************************* + + For more information, contact: + + IT Mill Ltd phone: +358 2 4802 7180 + Ruukinkatu 2-4 fax: +358 2 4802 7181 + 20540, Turku email: info@itmill.com + Finland company www: www.itmill.com + + Primary source for information and releases: www.itmill.com + + ********************************************************************** */ + +package com.itmill.toolkit.ui; + +import com.itmill.toolkit.data.BufferedValidatable; +import com.itmill.toolkit.data.Property; +import com.itmill.toolkit.ui.Component.Focusable; + +/** + * @author Sami Ekblad + * + */ +public interface Field + extends + Component, + BufferedValidatable, + Property, + Property.ValueChangeNotifier, + Property.ValueChangeListener, + Property.Editor, + Focusable { + + void setCaption(String caption); + + String getDescription(); + + void setDescription(String caption); + + /** Is this field required. + * + * Required fields must filled by the user. + * + * @return true if the + * @since 3.1 + */ + public boolean isRequired(); + + /** Set the field required. + * Required fields must filled by the user. + * + * @param required Is the field required + * @since 3.1 + */ + public void setRequired(boolean required); + + /** An <code>Event</code> object specifying the Field whose value + * has been changed. + * @author IT Mill Ltd. + * @version @VERSION@ + * @since 3.0 + */ + public class ValueChangeEvent + extends Component.Event + implements Property.ValueChangeEvent { + + /** + * Serial generated by eclipse. + */ + private static final long serialVersionUID = 3545803169444672816L; + + /** 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 Source Property of the event. + */ + public Property getProperty() { + return (Property) getSource(); + } + } +} diff --git a/src/com/itmill/toolkit/ui/FieldFactory.java b/src/com/itmill/toolkit/ui/FieldFactory.java new file mode 100644 index 0000000000..041b2761a8 --- /dev/null +++ b/src/com/itmill/toolkit/ui/FieldFactory.java @@ -0,0 +1,82 @@ +/* ************************************************************************* + + IT Mill Toolkit + + Development of Browser User Intarfaces Made Easy + + Copyright (C) 2000-2006 IT Mill Ltd + + ************************************************************************* + + This product is distributed under commercial license that can be found + from the product package on license/license.txt. Use of this product might + require purchasing a commercial license from IT Mill Ltd. For guidelines + on usage, see license/licensing-guidelines.html + + ************************************************************************* + + For more information, contact: + + IT Mill Ltd phone: +358 2 4802 7180 + Ruukinkatu 2-4 fax: +358 2 4802 7181 + 20540, Turku email: info@itmill.com + Finland company www: www.itmill.com + + Primary source for information and releases: www.itmill.com + + ********************************************************************** */ + +package com.itmill.toolkit.ui; + +import com.itmill.toolkit.data.Container; +import com.itmill.toolkit.data.Item; +import com.itmill.toolkit.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 { + + + /** Creates 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 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 field based on the item and property id. + * + * @param item The item where the property belongs to. + * @param propertyId 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 field based on the container item id and property id. + * + * @param container Container where the property belongs to. + * @param itemId The item Id. + * @param propertyId 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/itmill/toolkit/ui/Form.java b/src/com/itmill/toolkit/ui/Form.java new file mode 100644 index 0000000000..b80159683d --- /dev/null +++ b/src/com/itmill/toolkit/ui/Form.java @@ -0,0 +1,822 @@ +/* ************************************************************************* + + IT Mill Toolkit + + Development of Browser User Intarfaces Made Easy + + Copyright (C) 2000-2006 IT Mill Ltd + + ************************************************************************* + + This product is distributed under commercial license that can be found + from the product package on license/license.txt. Use of this product might + require purchasing a commercial license from IT Mill Ltd. For guidelines + on usage, see license/licensing-guidelines.html + + ************************************************************************* + + For more information, contact: + + IT Mill Ltd phone: +358 2 4802 7180 + Ruukinkatu 2-4 fax: +358 2 4802 7181 + 20540, Turku email: info@itmill.com + Finland company www: www.itmill.com + + Primary source for information and releases: www.itmill.com + + ********************************************************************** */ + +package com.itmill.toolkit.ui; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedList; + +import com.itmill.toolkit.data.*; +import com.itmill.toolkit.data.Validator.InvalidValueException; +import com.itmill.toolkit.data.util.BeanItem; +import com.itmill.toolkit.terminal.PaintException; +import com.itmill.toolkit.terminal.PaintTarget; + +/** Form component provides easy way of creating and managing sets fields. + * + * <p>Form 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>Form provides customizable editor for classes implementing + * {@link com.itmill.toolkit.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.itmill.toolkit.data.Item} interface, most properties of any + * class following bean pattern, can be accessed trough + * {@link com.itmill.toolkit.data.util.BeanItem}.</p> + * + * @author IT Mill Ltd. + * @version @VERSION@ + * @since 3.0 + */ +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 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 HashMap fields = new HashMap(); + + /** Field factory for this form */ + private FieldFactory fieldFactory; + + /** Registered Validators */ + private LinkedList validators; + + /** Visible item properties */ + private Collection visibleItemProperties; + + /** Contruct a new form with default layout. + * + * <p>By default the form uses <code>OrderedLayout</code> + * with <code>form</code>-style. + * + * @param formLayout The layout of the form. + */ + public Form() { + this(null); + } + + /** Contruct a new form with given layout. + * + * @param formLayout The layout of the form. + */ + public Form(Layout formLayout) { + this(formLayout, new BaseFieldFactory()); + } + + /** Contruct a new form with given layout and FieldFactory. + * + * @param formLayout The layout of the form. + * @param fieldFactory FieldFactory of the form + */ + public Form(Layout formLayout, FieldFactory fieldFactory) { + super(); + setLayout(formLayout); + setFieldFactory(fieldFactory); + } + + /* Documented in interface */ + public String getTag() { + return "component"; + } + + /* Documented in interface */ + public void paintContent(PaintTarget target) throws PaintException { + super.paintContent(target); + layout.paint(target); + + } + + /* Commit changes to the data source + * Don't add a JavaDoc comment here, we use the default one from the + * interface. + */ + public void commit() throws Buffered.SourceException { + + LinkedList problems = null; + + // Try to commit all + for (Iterator i = propertyIds.iterator(); i.hasNext();) + try { + Field f = ((Field) fields.get(i.next())); + //Commit only non-readonly fields. + if (!f.isReadOnly()) { + f.commit(); + } + } catch (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 + Throwable[] causes = new Throwable[problems.size()]; + int index = 0; + for (Iterator i = problems.iterator(); i.hasNext();) + causes[index++] = (Throwable) i.next(); + Buffered.SourceException e = new Buffered.SourceException(this, causes); + currentBufferedSourceException = e; + requestRepaint(); + throw e; + } + + /* Discard local changes and refresh values from the data source + * Don't add a JavaDoc comment here, we use the default one from the + * interface. + */ + public void discard() throws Buffered.SourceException { + + LinkedList problems = null; + + // Try to discard all changes + for (Iterator i = propertyIds.iterator(); i.hasNext();) + try { + ((Field) fields.get(i.next())).discard(); + } catch (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; + } + + // Discard problems occurred + Throwable[] causes = new Throwable[problems.size()]; + int index = 0; + for (Iterator i = problems.iterator(); i.hasNext();) + causes[index++] = (Throwable) i.next(); + 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. + */ + public boolean isModified() { + for (Iterator i = propertyIds.iterator(); i.hasNext();) { + 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. + */ + 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. + */ + 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. + */ + public void setReadThrough(boolean readThrough) { + if (readThrough != this.readThrough) { + this.readThrough = readThrough; + for (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. + */ + public void setWriteThrough(boolean writeThrough) { + if (writeThrough != this.writeThrough) { + this.writeThrough = writeThrough; + for (Iterator i = propertyIds.iterator(); i.hasNext();) + ((Field) fields.get(i.next())).setWriteThrough(writeThrough); + } + } + + /** Add a new property to form and create corresponding field. + * + * @see com.itmill.toolkit.data.Item#addItemProperty(Object, Property) + */ + public boolean addItemProperty(Object id, Property property) { + + // Check inputs + if (id == null || property == null) + throw new NullPointerException("Id and property must be non-null"); + + // Check that the property id is not reserved + if (propertyIds.contains(id)) + return false; + + // Get suitable field + Field field = this.fieldFactory.createField(property, this); + if (field == null) + return false; + + // Configure 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 (Throwable ignored) { + return false; + } + + addField(id, field); + + return true; + } + + /** Add 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 Property id the the field. + * @param field New field added to the form. + */ + public void addField(Object propertyId, Field field) { + + if (propertyId != null && field != null) { + this.dependsOn(field); + field.dependsOn(this); + fields.put(propertyId, field); + propertyIds.addLast(propertyId); + field.setReadThrough(readThrough); + field.setWriteThrough(writeThrough); + + 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.itmill.toolkit.data.Item#getItemProperty(Object) + */ + public Property getItemProperty(Object id) { + Field field = (Field) fields.get(id); + if (field == null) + return null; + Property property = field.getPropertyDataSource(); + + if (property != null) + return property; + else + return field; + } + + /** Get the field identified by the propertyid */ + 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.itmill.toolkit.data.Item#removeItemProperty(Object) + */ + public boolean removeItemProperty(Object id) { + + Field field = (Field) fields.get(id); + + if (field != null) { + propertyIds.remove(id); + fields.remove(id); + this.removeDirectDependency(field); + field.removeDirectDependency(this); + layout.removeComponent(field); + return true; + } + + return false; + } + + /** Removes all properties and fields from the form. + * + * @return Success of the operation. Removal of all fields succeeded + * if (and only if) the return value is true. + */ + public boolean removeAllProperties() { + 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; + } + + /** Set 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.itmill.toolkit.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.itmill.toolkit.data.Item.Viewer#setItemDataSource(Item) + */ + public void setItemDataSource(Item newDataSource, Collection propertyIds) { + + // Remove all fields first from the form + removeAllProperties(); + + // Set the datasource + itemDatasource = newDataSource; + + //If the new datasource is null, just set null datasource + if (itemDatasource == null) + return; + + // Add all the properties to this form + for (Iterator i = propertyIds.iterator(); i.hasNext();) { + Object id = i.next(); + Property property = itemDatasource.getItemProperty(id); + if (id != null && property != null) { + Field f = + this.fieldFactory.createField(itemDatasource, id, this); + if (f != null) { + f.setPropertyDataSource(property); + addField(id, f); + } + } + } + } + + /** Get the layout of the form. + * + * <p>By default form uses <code>OrderedLayout</code> with <code>form</code>-style.</p> + * + * @return Layout of the form. + */ + public Layout getLayout() { + return layout; + } + + /** Set the layout of the form. + * + * <p>By default form uses <code>OrderedLayout</code> with <code>form</code>-style.</p> + * + * @param layout Layout of the form. + */ + public void setLayout(Layout newLayout) { + + // Use orderedlayout by default + if (newLayout == null) { + newLayout = new OrderedLayout(); + newLayout.setStyle("form"); + } + + // Move components from previous layout + if (this.layout != null) { + newLayout.moveComponentsFrom(this.layout); + this.layout.setParent(null); + } + + // Replace the previous layout + newLayout.setParent(this); + this.layout = newLayout; + } + + /** Set a 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> + * + * @return The select property generated + */ + public Select replaceWithSelect( + Object propertyId, + Object[] values, + Object[] descriptions) { + + // Check 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"); + + // Get the old field + Field oldField = (Field) fields.get(propertyId); + if (oldField == null) + throw new IllegalArgumentException( + "Field with given propertyid '" + + propertyId.toString() + + "' can not be found."); + Object value = oldField.getValue(); + + // Check 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 (Iterator it = ((Collection) value).iterator(); + it.hasNext(); + ) { + 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"); + } + + // Create new field matching to old field parameters + 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()); + + // Create 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); + } + Item item = newField.addItem(id); + if (item != null) + item.getItemProperty("desc").setValue( + descriptions[i].toString()); + } + + // Set the property data source + Property property = oldField.getPropertyDataSource(); + oldField.setPropertyDataSource(null); + newField.setPropertyDataSource(property); + + // Replace the old field with new one + layout.replaceComponent(oldField, newField); + fields.put(propertyId, newField); + this.removeDirectDependency(oldField); + oldField.removeDirectDependency(this); + this.dependsOn(newField); + newField.dependsOn(this); + + return newField; + } + + /** + * @see com.itmill.toolkit.ui.Component#attach() + */ + public void attach() { + super.attach(); + layout.attach(); + } + + /** + * @see com.itmill.toolkit.ui.Component#detach() + */ + public void detach() { + super.detach(); + layout.detach(); + } + + /** + * @see com.itmill.toolkit.data.Validatable#addValidator(com.itmill.toolkit.data.Validator) + */ + public void addValidator(Validator validator) { + + if (this.validators == null) { + this.validators = new LinkedList(); + } + this.validators.add(validator); + } + /** + * @see com.itmill.toolkit.data.Validatable#removeValidator(com.itmill.toolkit.data.Validator) + */ + public void removeValidator(Validator validator) { + if (this.validators != null) { + this.validators.remove(validator); + } + } + /** + * @see com.itmill.toolkit.data.Validatable#getValidators() + */ + public Collection getValidators() { + if (this.validators == null) { + this.validators = new LinkedList(); + } + return null; + } + /** + * @see com.itmill.toolkit.data.Validatable#isValid() + */ + public boolean isValid() { + boolean valid = true; + for (Iterator i = propertyIds.iterator(); i.hasNext();) + valid &= ((Field) fields.get(i.next())).isValid(); + return valid; + } + /** + * @see com.itmill.toolkit.data.Validatable#validate() + */ + public void validate() throws InvalidValueException { + for (Iterator i = propertyIds.iterator(); i.hasNext();) + ((Field) fields.get(i.next())).validate(); + } + + /** + * @see com.itmill.toolkit.data.Validatable#isInvalidAllowed() + */ + public boolean isInvalidAllowed() { + return true; + } + /** + * @see com.itmill.toolkit.data.Validatable#setInvalidAllowed(boolean) + */ + public void setInvalidAllowed(boolean invalidValueAllowed) + throws UnsupportedOperationException { + throw new UnsupportedOperationException(); + } + /** + * @see com.itmill.toolkit.ui.Component#setReadOnly(boolean) + */ + public void setReadOnly(boolean readOnly) { + super.setReadOnly(readOnly); + for (Iterator i = propertyIds.iterator(); i.hasNext();) + ((Field) fields.get(i.next())).setReadOnly(readOnly); + } + + /** Set the field factory of Form. + * + * FieldFacroty is used to create fields for form properties. + * By default the form uses BaseFieldFactory to create Field instances. + * + * @param fieldFactory 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 FieldFactory Factory used to create the fields + */ + public FieldFactory getFieldFactory() { + return this.fieldFactory; + } + + /** + * @see com.itmill.toolkit.ui.AbstractField#getType() + */ + public Class getType() { + if (getPropertyDataSource() != null) + return getPropertyDataSource().getType(); + return Object.class; + } + + /** Set the internal value. + * + * This is relevant when the Form is used as Field. + * @see com.itmill.toolkit.ui.AbstractField#setInternalValue(java.lang.Object) + */ + protected void setInternalValue(Object newValue) { + // Store old value + Object oldValue = this.propertyValue; + + // Set the current Value + super.setInternalValue(newValue); + this.propertyValue = newValue; + + // Ignore form updating if data object has not changed. + if (oldValue != newValue) { + setFormDataSource(newValue, getVisibleItemProperties()); + } + } + + /**Get first field in form. + * @return Field + */ + private Field getFirstField() { + Object id = null; + if (this.getItemPropertyIds() != null) { + id = this.getItemPropertyIds().iterator().next(); + } + if (id != null) + return this.getField(id); + return null; + } + + /** Update the internal form datasource. + * + * Method setFormDataSource. + * @param value + */ + 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); + } + + // Set the datasource to form + if (item != null && properties != null) { + // Show only given properties + this.setItemDataSource(item, properties); + } else { + // Show all properties + this.setItemDataSource(item); + } + } + + /** + * Returns the visibleProperties. + * @return Collection + */ + public Collection getVisibleItemProperties() { + return visibleItemProperties; + } + + /** + * Sets the visibleProperties. + * @param visibleProperties The visibleProperties to set + */ + public void setVisibleItemProperties(Collection visibleProperties) { + this.visibleItemProperties = visibleProperties; + Object value = getValue(); + setFormDataSource(value, getVisibleItemProperties()); + } + + /** Focuses the first field in the form. + * @see com.itmill.toolkit.ui.Component.Focusable#focus() + */ + public void focus() { + Field f = getFirstField(); + if (f != null) { + f.focus(); + } + } + + /** + * @see com.itmill.toolkit.ui.Component.Focusable#setTabIndex(int) + */ + public void setTabIndex(int tabIndex) { + super.setTabIndex(tabIndex); + for (Iterator i = this.getItemPropertyIds().iterator(); i.hasNext();) + (this.getField(i.next())).setTabIndex(tabIndex); + } +} diff --git a/src/com/itmill/toolkit/ui/FrameWindow.java b/src/com/itmill/toolkit/ui/FrameWindow.java new file mode 100644 index 0000000000..4588f02e17 --- /dev/null +++ b/src/com/itmill/toolkit/ui/FrameWindow.java @@ -0,0 +1,469 @@ +/* ************************************************************************* + + IT Mill Toolkit + + Development of Browser User Intarfaces Made Easy + + Copyright (C) 2000-2006 IT Mill Ltd + + ************************************************************************* + + This product is distributed under commercial license that can be found + from the product package on license/license.txt. Use of this product might + require purchasing a commercial license from IT Mill Ltd. For guidelines + on usage, see license/licensing-guidelines.html + + ************************************************************************* + + For more information, contact: + + IT Mill Ltd phone: +358 2 4802 7180 + Ruukinkatu 2-4 fax: +358 2 4802 7181 + 20540, Turku email: info@itmill.com + Finland company www: www.itmill.com + + Primary source for information and releases: www.itmill.com + + ********************************************************************** */ + +package com.itmill.toolkit.ui; + +import java.net.URL; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; + +import com.itmill.toolkit.Application; +import com.itmill.toolkit.terminal.PaintException; +import com.itmill.toolkit.terminal.PaintTarget; +import com.itmill.toolkit.terminal.Resource; + +/** <p>An application frame window component. This component implements a + * window that contains a hierarchical set of frames. Each frame can contain + * a web-page, window or a set of frames that divides the space horizontally + * or vertically.</p> + * + * <p>A <code>FrameWindow</code> can't contain any components directly (as + * it contains only a set of frames) and thus the container interface + * methods do nothing.</p> + * + * @author IT Mill Ltd. + * @version @VERSION@ + * @since 3.0 + */ +public class FrameWindow extends Window { + + private Frameset frameset = new Frameset(); + + /** <p>Constructs a new frame window. + * + */ + public FrameWindow() { + } + + + /** <p>Constructs a new frame window. + * + * @param caption title of the window + */ + public FrameWindow(String caption) { + super(caption); + } + + /** Gets the window's UIDL tag. + * + * @return window's UIDL tag as <code>String</code> + */ + public String getTag() { + return "framewindow"; + } + + /** Gets the main frameset of the window. This set contains all the + * top-level frames of the window. New contents are added by adding + * frames to this frameset. + * + * @return top-level frame set of this frame window + */ + public Frameset getFrameset() { + return frameset; + } + + /** Paints the window contents. + * + * @param target A paint target event + * @throws PaintException if the paint operation fails + * + * @see com.itmill.toolkit.ui.AbstractComponent#paintContent(PaintTarget) + */ + public void paintContent(PaintTarget target) throws PaintException { + + super.paintContent(target); + + // Paint frameset + getFrameset().paint(target); + } + + /** An individual frame that contains either a window or the contents of the + * url set to frame. + * + * <p>The frames can be only created to framesets using the + * <code>newFrame()</code> functions of the frameset.</p> + */ + public class Frame { + + /** URL of the frame contents */ + private URL url; + + /** Name of the frame */ + private String name; + + /** Window connected to frame or null */ + private Window window; + + /** Window connected to frame or null */ + private Resource resource; + + /** String representation of the width */ + private String width = "*"; + + /** Parent frameset */ + protected Frameset parentFrameset; + + /** URL of the frame */ + public URL getURL() { + return window == null ? url : window.getURL(); + } + + /** Get the parent frameset */ + public Frameset getParentFrameset() { + return parentFrameset; + } + + /** Name of the freame */ + public String getName() { + return window == null ? name : window.getName(); + } + + /** Window connected to frame */ + public Window getWindow() { + return window; + } + + /** Resource connected to frame */ + public Resource getResource() { + return resource; + } + + /** Absolute width/height of the frame in pixels */ + public void setAbsoluteSize(int widthInPixels) { + width = String.valueOf(widthInPixels); + requestRepaint(); + } + + /** Set the frame size to be freely specified by the terminal */ + public void setFreeSize() { + width = "*"; + requestRepaint(); + } + + /** Set the frame width/height as a percentage of the containing + * frameset size */ + public void setRelativeSize(int widthInPercents) { + if (widthInPercents < 0 || widthInPercents > 100) + throw new IllegalArgumentException( + "Relative width must " + "be between 0% and 100%"); + width = String.valueOf(widthInPercents) + "%"; + requestRepaint(); + } + + /** Paint the frame */ + private void paint(PaintTarget target) throws PaintException { + target.startTag("frame"); + if (getResource() != null) + target.addAttribute("src", getResource()); + else + target.addAttribute("src", getURL().toString()); + target.addAttribute("name", getName()); + target.endTag("frame"); + } + } + + /** Vertical or horizontal set of frames */ + public class Frameset extends Frame { + + /** List of frames ordered from left to right or from top to bottom */ + private LinkedList frames = new LinkedList(); + + /** True iff the frames are on top of each other. If false the frames + * are side by side. + */ + private boolean vertical = false; + + /** Get a list of frames. + * + * @return unmodifiable list of frames. + */ + public List getFrames() { + return Collections.unmodifiableList(frames); + } + + /** Create new frame containing a window. + * + * <p>The new frame will be in the end of the frames list.</p> + */ + public Frame newFrame(Window window) { + return newFrame(window, size()); + } + + /** Create new frame containing a window. + * + * <p>The new frame will be put before the frame identified + * by the given index. The indexes of the frame previously in the + * given position and all the positions after it are incremented + * by one.</p> + */ + public Frame newFrame(Window window, int index) { + Frame f = new Frame(); + f.window = window; + f.parentFrameset = this; + frames.add(index, f); + if (getApplication() != null) + getApplication().addWindow(window); + requestRepaint(); + return f; + } + + /** Create new frame containing a url. + * + * <p>The new frame will be put in the end of the frames list..</p> + */ + public Frame newFrame(URL url, String name) { + return newFrame(url, name, size()); + } + + /** Create new frame containing a resource. + * + * <p>The new frame will be put in the end of the frames list..</p> + */ + public Frame newFrame(Resource resource, String name) { + return newFrame(resource, name, size()); + } + + /** Create new frame containing a url. + * + * <p>The new frame will be put before the frame identified + * by the given index. The indexes of the frame previously in the + * given position and all the positions after it are incremented + * by one.</p> + */ + public Frame newFrame(URL url, String name, int index) { + Frame f = new Frame(); + f.url = url; + f.name = name; + f.parentFrameset = this; + frames.add(index, f); + requestRepaint(); + return f; + } + + /** Create new frame containing a resource. + * + * <p>The new frame will be put before the frame identified + * by the given index. The indexes of the frame previously in the + * given position and all the positions after it are incremented + * by one.</p> + */ + public Frame newFrame(Resource resource, String name, int index) { + Frame f = new Frame(); + f.resource = resource; + f.name = name; + f.parentFrameset = this; + frames.add(index, f); + requestRepaint(); + return f; + } + + /** Create new frameset. + * + * <p>The new frame will be put before the frame identified + * by the given index. The indexes of the frame previously in the + * given position and all the positions after it are incremented + * by one.</p> + */ + public Frameset newFrameset(boolean isVertical, int index) { + Frameset f = new Frameset(); + f.setVertical(isVertical); + f.parentFrameset = this; + frames.add(index, f); + requestRepaint(); + return f; + } + + /** Remove a frame from this frameset */ + public void removeFrame(Frame frame) { + frames.remove(frame); + frame.parentFrameset = null; + requestRepaint(); + } + + /** Remove all frames from this frameset */ + public void removeAllFrames() { + for (Iterator i = frames.iterator(); i.hasNext();) + ((Frame) i.next()).parentFrameset = null; + frames.clear(); + requestRepaint(); + } + + /** Number of frames in this frameset */ + public int size() { + return frames.size(); + } + + /** Set the framaset to be vertical. + * + * <p>By setting this true, the frames will be ordered on top + * of each other from top to bottom. Setting this false, the + * frames will be ordered side by side from left to right.</p> + */ + public void setVertical(boolean isVertical) { + this.vertical = isVertical; + requestRepaint(); + } + + /** Check if the frameset is vertical. + * + * <p>If this is true, the frames will be ordered on top + * of each other from top to bottom, otherwise the + * frames will be ordered side by side from left to right.</p> + */ + public boolean isVertical() { + return vertical; + } + + /** Get frame by name. + * @return Frame having the given name or null if the frame is + * not found + */ + public Frame getFrame(String name) { + if (name == null) + return null; + for (Iterator i = frames.iterator(); i.hasNext();) { + Frame f = (Frame) i.next(); + if (name.equals(f.getName())) + return f; + } + return null; + } + + /** Get frame by index. + * @return Frame having the given index or null if the frame is + * not found + */ + public Frame getFrame(int index) { + if (index >= 0 && index < frames.size()) + return (Frame) frames.get(index); + return null; + } + + /** Paint the frameset */ + private void paint(PaintTarget target) throws PaintException { + target.startTag("frameset"); + if (!frames.isEmpty()) { + StringBuffer widths = null; + for (Iterator i = frames.iterator(); i.hasNext();) { + Frame f = (Frame) i.next(); + if (widths == null) + widths = new StringBuffer(); + else + widths.append(','); + widths.append(f.width); + } + if (vertical) + target.addAttribute("rows", widths.toString()); + else + target.addAttribute("cols", widths.toString()); + for (Iterator i = frames.iterator(); i.hasNext();) { + Frame f = (Frame) i.next(); + if (Frameset.class.isAssignableFrom(f.getClass())) + ((Frameset) f).paint(target); + else + f.paint(target); + } + } + target.endTag("frameset"); + } + + /** Set the application for all the frames in this frameset */ + private void setApplication( + Application fromApplication, + Application toApplication) { + for (Iterator i = frames.iterator(); i.hasNext();) { + Frame f = (Frame) i.next(); + if (f instanceof Frameset) + ((Frameset) f).setApplication( + fromApplication, + toApplication); + else if (f.window != null) { + if (toApplication == null) { + fromApplication.removeWindow(f.window); + } else + toApplication.addWindow(f.window); + } + } + } + } + + /** Setting the application for frame window also sets the application + * for all the frames. + * @see com.itmill.toolkit.ui.Window#setApplication(Application) + */ + public void setApplication(Application application) { + Application fromApplication = getApplication(); + super.setApplication(application); + Frameset fs = getFrameset(); + if (fs != null) + fs.setApplication(fromApplication, application); + } + + /** Frame windows does not support scrolling. + */ + public boolean isScrollable() { + return false; + } + + /** Frame windows does not support scrolling. + * + * @see com.itmill.toolkit.terminal.Scrollable#setScrollable(boolean) + */ + public void setScrollable(boolean isScrollingEnabled) { + } + + /** Frame windows does not support scrolling. + * + * @see com.itmill.toolkit.terminal.Scrollable#setScrollOffsetX(int) + */ + public void setScrollOffsetX(int pixelsScrolledLeft) { + } + + /** Frame windows does not support scrolling. + * + * @see com.itmill.toolkit.terminal.Scrollable#setScrollOffsetY(int) + */ + public void setScrollOffsetY(int pixelsScrolledDown) { + } + + /** Frame window does not support adding components directly. + * + * <p>To add component to frame window, normal window must be + * first created and then attached to frame window as a frame.</p> + * + * @see com.itmill.toolkit.ui.ComponentContainer#addComponent(Component) + * @throws UnsupportedOperationException if invoked. + */ + public void addComponent(Component c) + throws UnsupportedOperationException { + throw new UnsupportedOperationException(); + } + +} diff --git a/src/com/itmill/toolkit/ui/GridLayout.java b/src/com/itmill/toolkit/ui/GridLayout.java new file mode 100644 index 0000000000..3af8f34ac7 --- /dev/null +++ b/src/com/itmill/toolkit/ui/GridLayout.java @@ -0,0 +1,757 @@ +/* ************************************************************************* + + IT Mill Toolkit + + Development of Browser User Intarfaces Made Easy + + Copyright (C) 2000-2006 IT Mill Ltd + + ************************************************************************* + + This product is distributed under commercial license that can be found + from the product package on license/license.txt. Use of this product might + require purchasing a commercial license from IT Mill Ltd. For guidelines + on usage, see license/licensing-guidelines.html + + ************************************************************************* + + For more information, contact: + + IT Mill Ltd phone: +358 2 4802 7180 + Ruukinkatu 2-4 fax: +358 2 4802 7181 + 20540, Turku email: info@itmill.com + Finland company www: www.itmill.com + + Primary source for information and releases: www.itmill.com + + ********************************************************************** */ + +package com.itmill.toolkit.ui; + +import java.util.Collections; +import java.util.Iterator; +import java.util.HashMap; +import java.util.LinkedList; + +import com.itmill.toolkit.terminal.PaintException; +import com.itmill.toolkit.terminal.PaintTarget; + +/** <p>A container that consists of components with certain coordinates 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} (x1,y1,x2,y2) 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 + */ +public class GridLayout extends AbstractComponentContainer implements Layout { + + /** Initial grid x size */ + private int width = 0; + + /** Initial grid y size */ + private int height = 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 LinkedList areas = new LinkedList(); + + /** Mapping from components to threir respective areas. */ + private LinkedList components = new LinkedList(); + + /** Constructor for grid of given size. + * 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 width Width of the grid. + * @param height Height of the grid. + */ + public GridLayout(int width, int height) { + setWidth(width); + setHeight(height); + } + + /** 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 (x1, y1) and the lower right corner (x2, y2) 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 x1 The X-coordinate of the upper left corner of the area + * <code>c</code> is supposed to occupy + * @param y1 The Y-coordinate of the upper left corner of the area + * <code>c</code> is supposed to occupy + * @param x2 The X-coordinate of the lower right corner of the area + * <code>c</code> is supposed to occupy + * @param y2 The Y-coordinate 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 coordinates are outside of the + * grid area. + */ + public void addComponent( + Component component, + int x1, + int y1, + int x2, + int y2) + throws OverlapsException, OutOfBoundsException { + + if (component == null) + throw new NullPointerException("Component must not be null"); + + // Check that the component does not already exist in the container + if (components.contains(component)) + throw new IllegalArgumentException("Component is already in the container"); + + // Create area + Area area = new Area(component, x1, y1, x2, y2); + + // Check the validity of the coordinates + if (x2 < x1 || y2 < y2) + throw new IllegalArgumentException("Illegal coordinates for the component"); + if (x1 < 0 || y1 < 0 || x2 >= width || y2 >= height) + throw new OutOfBoundsException(area); + + // Check that newItem does not overlap with existing items + checkExistingOverlaps(area); + + // Insert the component to right place at the list + // Respect top-down, left-right ordering + component.setParent(this); + Iterator i = areas.iterator(); + int index = 0; + boolean done = false; + while (!done && i.hasNext()) { + Area existingArea = (Area) i.next(); + if ((existingArea.y1 >= y1 && existingArea.x1 > x1) + || existingArea.y1 > y1) { + areas.add(index, area); + components.add(index, component); + done = true; + } + index++; + } + if (!done) { + areas.addLast(area); + components.addLast(component); + } + + super.addComponent(component); + requestRepaint(); + } + + /** Tests if the given area overlaps with any of the items already on + * the grid. + * + * @param area 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 (Iterator i = areas.iterator(); i.hasNext();) { + Area existingArea = (Area) i.next(); + if (existingArea.overlaps(area)) + + // Component not added, overlaps with existing component + throw new OverlapsException(existingArea); + } + } + + /** Add component into this container to coordinates x1,y1 (NortWest corner of the area.) + * End coordinates (SouthEast corner of the area) are the same as x1,y1. Component width + * and height is 1. + * @param c The component to be added. + * @param x X-coordinate + * @param y Y-coordinate + */ + public void addComponent(Component c, int x, int y) { + this.addComponent(c, x, y, x, y); + } + + /** 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++; + } + + /** Move 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 >= width) { + cursorX = 0; + cursorY++; + } + } + + /** Add a 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 automaticly extended. + * @param c The component to be added. + */ + public void addComponent(Component component) { + + // Find 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 (OverlapsException ignored) { + space(); + } + + // Extend the grid if needed + width = cursorX >= width ? cursorX + 1 : width; + height = cursorY >= height ? cursorY + 1 : height; + + addComponent(component, cursorX, cursorY); + } + + /** Removes the given component from this + * container. + * + * @param c The component to be removed. + */ + 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 (Iterator i = areas.iterator(); area == null && i.hasNext();) { + Area a = (Area) i.next(); + if (a.getComponent() == component) + area = a; + } + + components.remove(component); + if (area != null) + areas.remove(area); + + requestRepaint(); + } + + /** Removes a component specified with it's top-left corner coordinates + * from this grid. + * + * @param x Component's top-left corner's X-coordinate + * @param y Component's top-left corner's Y-coordinate + */ + public void removeComponent(int x, int y) { + + // Find area + for (Iterator i = areas.iterator(); i.hasNext();) { + Area area = (Area) i.next(); + if (area.getX1() == x && area.getY1() == y) { + 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 Iterator of the components inside the container. + */ + public Iterator getComponentIterator() { + return Collections.unmodifiableCollection(components).iterator(); + } + + /** Paints the contents of this component. + * + * @param event PaintEvent. + * @throws PaintException The paint operation failed. + */ + public void paintContent(PaintTarget target) throws PaintException { + + target.addAttribute("h", height); + target.addAttribute("w", width); + + // Area iterator + Iterator areaiterator = areas.iterator(); + + // Current item to be processed (fetch first item) + Area area = areaiterator.hasNext() ? (Area) areaiterator.next() : null; + + // Collect rowspan related information here + HashMap cellUsed = new HashMap(); + + // Empty cell collector + int emptyCells = 0; + + // Iterate every applicable row + for (int cury = 0; cury < height; cury++) { + target.startTag("gr"); + + // Iterate every applicable column + for (int curx = 0; curx < width; curx++) { + + // Check if current item is located at curx,cury + if (area != null && (area.y1 == cury) && (area.x1 == 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 + int cols = (area.x2 - area.x1) + 1; + int rows = (area.y2 - area.y1) + 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); + + target.endTag("gc"); + + // Fetch next item + if (areaiterator.hasNext()) { + area = (Area) areaiterator.next(); + } else { + area = null; + } + + // Update 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++; + } + } + + // Skip current item's spanned columns + if (cols > 1) { + curx += cols - 1; + } + + } else { + + // Check 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 + 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++; + + // Remove cellUsed key as it has become obsolete + cellUsed.remove(new Integer(curx)); + } + } else { + + // empty cell is needed + emptyCells++; + } + } + + } // iterate every column + + // Last column handled of current row + + // Check if empty cell needs to be rendered + if (emptyCells > 0) { + target.startTag("gc"); + target.addAttribute("x", width - emptyCells); + target.addAttribute("y", cury); + if (emptyCells > 1) { + target.addAttribute("w", emptyCells); + } + target.endTag("gc"); + + emptyCells = 0; + } + + target.endTag("gr"); + } // iterate every row + + // Last row handled + } + + /** Gets the components UIDL tag. + * + * @return Component UIDL tag as string. + * @see com.itmill.toolkit.ui.AbstractComponent#getTag() + */ + public String getTag() { + return "gridlayout"; + } + + /** This class defines an area on a grid. An Area is defined by the + * coordinates of its upper left corner (x1,y1) and lower right corner + * (x2,y2) + * @author IT Mill Ltd. + * @version @VERSION@ + * @since 3.0 + */ + public class Area { + + /** X-coordinate of the upper left corner of the area */ + private int x1; + + /** Y-coordinate of the upper left corner of the area */ + private int y1; + + /** X-coordinate of the lower right corner of the area */ + private int x2; + + /** Y-coordinate of the lower right corner of the area */ + private int y2; + + /** Component painted on the area */ + private Component component; + + /** <p>Construct a new area on a grid. + * + * @param x1 The X-coordinate of the upper left corner of the area + * <code>c</code> is supposed to occupy + * @param y1 The Y-coordinate of the upper left corner of the area + * <code>c</code> is supposed to occupy + * @param x2 The X-coordinate of the lower right corner of the area + * <code>c</code> is supposed to occupy + * @param y2 The Y-coordinate 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 + */ + public Area(Component component, int x1, int y1, int x2, int y2) { + this.x1 = x1; + this.y1 = y1; + this.x2 = x2; + this.y2 = y2; + this.component = component; + } + + /** Tests if the given Area overlaps with an another Area. + * + * @param other 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 x1 <= other.getX2() + && y1 <= other.getY2() + && x2 >= other.getX1() + && y2 >= other.getY1(); + + } + + /** Returns the component connected to the area. + * @return 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; + } + + /** Returns the top-left corner x-coordinate. + * @return int + */ + public int getX1() { + return x1; + } + + /** + * Returns the bottom-right corner x-coordinate. + * @return int + */ + public int getX2() { + return x2; + } + + /** + * Returns the top-left corner y-coordinate. + * @return int + */ + public int getY1() { + return y1; + } + + /** + * Returns the bottom-right corner y-coordinate. + * @return int + */ + public int getY2() { + return y2; + } + + } + + /** 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 { + + /** + * Serial generated by eclipse. + */ + private static final long serialVersionUID = 3978144339870101561L; + + private Area existingArea; + + /** Constructs an <code>OverlapsException</code>. + * @param msg the detail message. + */ + public OverlapsException(Area existingArea) { + this.existingArea = existingArea; + } + + /** Get the 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 { + + /** + * Serial generated by eclipse. + */ + private static final long serialVersionUID = 3618985589664592694L; + + private Area areaOutOfBounds; + + /** Constructs an <code>OoutOfBoundsException</code> with the specified + * detail message. + * + * @param msg the detail message. + */ + public OutOfBoundsException(Area areaOutOfBounds) { + this.areaOutOfBounds = areaOutOfBounds; + } + + /** Get the area that is out of bounds */ + public Area getArea() { + return areaOutOfBounds; + } + } + + /** Set the width of the grid. The width can not be reduced if there are + * any areas that would be outside of the shrunk grid. + * @param width New width of the grid. + * @throws OutOfBoundsException if the one of the areas would exceed the + * bounds of the grid after the modification of the grid size. + */ + public void setWidth(int width) { + + // The the param + if (width < 1) + throw new IllegalArgumentException("The grid width and height must be at least 1"); + + // In case of no change + if (this.width == width) + return; + + // Check for overlaps + if (this.width > width) + for (Iterator i = areas.iterator(); i.hasNext();) { + Area area = (Area) i.next(); + if (area.x2 >= width) + throw new OutOfBoundsException(area); + } + + this.width = width; + + requestRepaint(); + } + + /** Get the width of the grids. + * @return The width of the grid + */ + public final int getWidth() { + return this.width; + } + + /** Set the height of the grid. The width can not be reduced if there are + * any areas that would be outside of the shrunk grid. + * @param Height of the grid + */ + public void setHeight(int height) { + + // The the param + if (height < 1) + throw new IllegalArgumentException("The grid width and height must be at least 1"); + + // In case of no change + if (this.height == height) + return; + + // Check for overlaps + if (this.height > height) + for (Iterator i = areas.iterator(); i.hasNext();) { + Area area = (Area) i.next(); + if (area.y2 >= height) + throw new OutOfBoundsException(area); + } + + this.height = height; + + requestRepaint(); + } + + /** Get the height of the grid. + * @return int - how many cells high the grid is + */ + public final int getHeight() { + return this.height; + } + + /** Get the current cursor x-position. + * The cursor position points the position for the next component + * that is added without specifying its coordinates. When the + * cursor position is occupied, the next component will be added + * to first free position after the cursor. + * @return Cursor x-coordinate. + */ + public int getCursorX() { + return cursorX; + } + + /** Get the current cursor y-position. + * The cursor position points the position for the next component + * that is added without specifying its coordinates. When the + * cursor position is occupied, the next component will be added + * to first free position after the cursor. + * @return Cursor y-coordinate. + */ + public int getCursorY() { + return cursorY; + } + + /* Documented in superclass */ + public void replaceComponent( + Component oldComponent, + Component newComponent) { + + // Get the locations + Area oldLocation = null; + Area newLocation = null; + for (Iterator i=areas.iterator(); i.hasNext();) { + Area location = (Area) i.next(); + Component 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.getX1(),oldLocation.getY1(),oldLocation.getX2(),oldLocation.getY2()); + } else { + oldLocation.setComponent(newComponent); + newLocation.setComponent(oldComponent); + requestRepaint(); + } + } + + /* + * @see com.itmill.toolkit.ui.ComponentContainer#removeAllComponents() + */ + public void removeAllComponents() { + super.removeAllComponents(); + this.cursorX = 0; + this.cursorY = 0; + } + +} diff --git a/src/com/itmill/toolkit/ui/Label.java b/src/com/itmill/toolkit/ui/Label.java new file mode 100644 index 0000000000..c2897a5652 --- /dev/null +++ b/src/com/itmill/toolkit/ui/Label.java @@ -0,0 +1,455 @@ +/* ************************************************************************* + + IT Mill Toolkit + + Development of Browser User Intarfaces Made Easy + + Copyright (C) 2000-2006 IT Mill Ltd + + ************************************************************************* + + This product is distributed under commercial license that can be found + from the product package on license/license.txt. Use of this product might + require purchasing a commercial license from IT Mill Ltd. For guidelines + on usage, see license/licensing-guidelines.html + + ************************************************************************* + + For more information, contact: + + IT Mill Ltd phone: +358 2 4802 7180 + Ruukinkatu 2-4 fax: +358 2 4802 7181 + 20540, Turku email: info@itmill.com + Finland company www: www.itmill.com + + Primary source for information and releases: www.itmill.com + + ********************************************************************** */ + +package com.itmill.toolkit.ui; + +import java.lang.reflect.Method; + +import com.itmill.toolkit.data.Property; +import com.itmill.toolkit.data.util.ObjectProperty; +import com.itmill.toolkit.terminal.PaintException; +import com.itmill.toolkit.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 + */ +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. + */ + 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; + + private Property dataSource; + private int contentMode = CONTENT_DEFAULT; + + /** Creates an empty Label. */ + public Label() { + setPropertyDataSource(new ObjectProperty("", String.class)); + } + + /** Creates a new instance of Label with text-contents. */ + public Label(String content) { + setPropertyDataSource(new ObjectProperty(content, String.class)); + } + + /** Creates a new instance of Label with text-contents read from given datasource. */ + public Label(Property contentSource) { + setPropertyDataSource(contentSource); + } + + /** Creates a new instance of Label with text-contents. */ + public Label(String content, int contentMode) { + setPropertyDataSource(new ObjectProperty(content, String.class)); + setContentMode(contentMode); + } + + /** Creates a new instance of Label with text-contents read from given datasource. */ + public Label(Property contentSource, int contentMode) { + setPropertyDataSource(contentSource); + setContentMode(contentMode); + } + + /** Get component UIDL tag. + * @return Component UIDL tag as string. + */ + 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 + */ + 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 label - this returns allways false. + * @return True iff the component is in read only mode + */ + public boolean isReadOnly() { + if (dataSource == null) + throw new IllegalStateException("Datasource must be se"); + return dataSource.isReadOnly(); + } + + /** Paint the content of this component. + * @param event PaintEvent. + */ + public void paintContent(PaintTarget target) throws PaintException { + 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.addCharacterData(toString()); + target.endTag("data"); + } + + } + + /** Get the value of the label. + * Value of the label is the XML contents of the label. + * @return Value of the label + */ + public Object getValue() { + if (dataSource == null) + throw new IllegalStateException("Datasource must be se"); + return dataSource.getValue(); + } + + /** Set the value of the label. + * Value of the label is the XML contents of the label. + * @param newValue New value of the label + */ + public void setValue(Object newValue) { + if (dataSource == null) + throw new IllegalStateException("Datasource must be se"); + this.dataSource.setValue(newValue); + } + + public String toString() { + if (dataSource == null) + throw new IllegalStateException("Datasource must be se"); + return dataSource.toString(); + } + + public Class getType() { + if (dataSource == null) + throw new IllegalStateException("Datasource must be se"); + return dataSource.getType(); + } + + /** Get viewing data-source property. */ + public Property getPropertyDataSource() { + return dataSource; + } + + /** Set the property as data-source for viewing. */ + public void setPropertyDataSource(Property newDataSource) { + // Stop listening the old data source changes + if (dataSource != null + && Property.ValueChangeNotifier.class.isAssignableFrom( + dataSource.getClass())) + ((Property.ValueChangeNotifier) dataSource).removeListener(this); + + // Set the new data source + dataSource = newDataSource; + + // Listen the new data source if possible + if (dataSource != null + && Property.ValueChangeNotifier.class.isAssignableFrom( + dataSource.getClass())) + ((Property.ValueChangeNotifier) dataSource).addListener(this); + } + + /** Get 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 Content mode of the label. + */ + public int getContentMode() { + return contentMode; + } + + /** Set 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 New content mode of the label. + */ + public void setContentMode(int contentMode) { + if (contentMode >= CONTENT_TEXT && contentMode <= CONTENT_RAW) + this.contentMode = contentMode; + } + + /* 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 (java.lang.NoSuchMethodException e) { + // This should never happen + throw new java.lang.RuntimeException(); + } + } + + /** Value change event + * @author IT Mill Ltd. + * @version @VERSION@ + * @since 3.0 + */ + public class ValueChangeEvent + extends Component.Event + implements Property.ValueChangeEvent { + + /** + * Serial generated by eclipse. + */ + private static final long serialVersionUID = 3906084563938586935L; + + /** New instance of text change event + * @param source Source of the event. + */ + public ValueChangeEvent(Label source) { + super(source); + } + + /** Value where the event occurred + * @return Source of the event. + */ + public Property getProperty() { + return (Property) getSource(); + } + } + + /** Add value change listener + * @param listener Listener to be added. + */ + public void addListener(Property.ValueChangeListener listener) { + addListener( + Label.ValueChangeEvent.class, + listener, + VALUE_CHANGE_METHOD); + } + + /** Remove value change listener + * @param listener Listener to be removed. + */ + public void removeListener(Property.ValueChangeListener listener) { + removeListener( + Label.ValueChangeEvent.class, + listener, + VALUE_CHANGE_METHOD); + } + + /** Emit options change event. */ + protected void fireValueChange() { + // Set the error message + fireEvent(new Label.ValueChangeEvent(this)); + requestRepaint(); + } + + /** Listen value change events from data source. + * @see com.itmill.toolkit.data.Property.ValueChangeListener#valueChange(Property.ValueChangeEvent) + */ + public void valueChange(Property.ValueChangeEvent event) { + fireValueChange(); + } + + /** Compare 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> + * + * @see java.lang.Comparable#compareTo(java.lang.Object) + * @param other 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. + */ + 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); + } + + /** Strip tags from the XML. + * + * @param xml String containing a XML snippet + * @return The original XML without tags + */ + private String stripTags(String xml) { + + StringBuffer res = new StringBuffer(); + + int processed = 0; + 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/itmill/toolkit/ui/Layout.java b/src/com/itmill/toolkit/ui/Layout.java new file mode 100644 index 0000000000..e3805f6dd1 --- /dev/null +++ b/src/com/itmill/toolkit/ui/Layout.java @@ -0,0 +1,43 @@ +/* ************************************************************************* + + IT Mill Toolkit + + Development of Browser User Intarfaces Made Easy + + Copyright (C) 2000-2006 IT Mill Ltd + + ************************************************************************* + + This product is distributed under commercial license that can be found + from the product package on license/license.txt. Use of this product might + require purchasing a commercial license from IT Mill Ltd. For guidelines + on usage, see license/licensing-guidelines.html + + ************************************************************************* + + For more information, contact: + + IT Mill Ltd phone: +358 2 4802 7180 + Ruukinkatu 2-4 fax: +358 2 4802 7181 + 20540, Turku email: info@itmill.com + Finland company www: www.itmill.com + + Primary source for information and releases: www.itmill.com + + ********************************************************************** */ + +package com.itmill.toolkit.ui; + +/** 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. This interface is empty in + * MillStone 3.0, but will be extended in MillStone 3.1. + * + * @author IT Mill Ltd. + * @version @VERSION@ + * @since 3.0 + */ +public interface Layout extends ComponentContainer { + +} diff --git a/src/com/itmill/toolkit/ui/Link.java b/src/com/itmill/toolkit/ui/Link.java new file mode 100644 index 0000000000..c09859dcda --- /dev/null +++ b/src/com/itmill/toolkit/ui/Link.java @@ -0,0 +1,256 @@ +/* ************************************************************************* + + IT Mill Toolkit + + Development of Browser User Intarfaces Made Easy + + Copyright (C) 2000-2006 IT Mill Ltd + + ************************************************************************* + + This product is distributed under commercial license that can be found + from the product package on license/license.txt. Use of this product might + require purchasing a commercial license from IT Mill Ltd. For guidelines + on usage, see license/licensing-guidelines.html + + ************************************************************************* + + For more information, contact: + + IT Mill Ltd phone: +358 2 4802 7180 + Ruukinkatu 2-4 fax: +358 2 4802 7181 + 20540, Turku email: info@itmill.com + Finland company www: www.itmill.com + + Primary source for information and releases: www.itmill.com + + ********************************************************************** */ + +package com.itmill.toolkit.ui; + +import com.itmill.toolkit.terminal.PaintException; +import com.itmill.toolkit.terminal.PaintTarget; +import com.itmill.toolkit.terminal.Resource; + +/** Link component. + * Link is used to create external or internal URL links. + * + * Internal links can be used to create action items, which + * change the state to application to one of the predefined states. + * For example, a link can be created for existing MenuTree items. + * + * @author IT Mill Ltd. + * @version @VERSION@ + * @since 3.0 + */ +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 Window window = 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 link to a window. */ + public Link(Window window) { + + // Set the link caption to match window caption + setCaption(window.getCaption()); + + // Set the target + setTargetName(window.getName()); + + setTargetName(window.getName()); + setTargetWidth(window.getWidth()); + setTargetHeight(window.getHeight()); + setTargetBorder(window.getBorder()); + } + + /** Creates a new instance of Link */ + public Link(String caption, Resource resource) { + setCaption(caption); + this.resource = resource; + } + + /** Creates a new instance of Link that opens a new window. + * + * + * @param caption 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 Width of the target window. + * @param height Height of the target window. + * @param border Borget 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); + } + + /** Get component UIDL tag. + * @return Component UIDL tag as string. + */ + public String getTag() { + return "link"; + } + + /** Paint the content of this component. + * @param event PaintEvent. + * @throws PaintException The paint operation failed. + */ + public void paintContent(PaintTarget target) throws PaintException { + + if (window != null) + target.addAttribute("src", window.getURL().toString()); + else if (resource != null) + target.addAttribute("src", resource); + else + return; + + // Target window name + String name = getTargetName(); + if (name != null && name.length()>0) + target.addAttribute("name", name); + + // Target window size + if (getTargetWidth() >= 0) + target.addAttribute("width", getTargetWidth()); + if (getTargetHeight() >= 0) + target.addAttribute("height", 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 int + */ + public int getTargetBorder() { + return targetBorder; + } + + /** Returns the target window height or -1 if not set. + * @return int + */ + 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 String + */ + public String getTargetName() { + return targetName; + } + + /** Returns the target window width or -1 if not set. + * @return int + */ + 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 Resource + */ + public Resource getResource() { + return resource; + } + + /** Returns the window this link opens. + * @return Window + */ + public Window getWindow() { + return window; + } + + /** Sets the resource this link opens. + * @param resource The resource to set + */ + public void setResource(Resource resource) { + this.resource = resource; + if (resource != null) { + window = null; + } + requestRepaint(); + } + + /** Sets the window this link opens. + * @param window The window to set + */ + public void setWindow(Window window) { + this.window = window; + if (window != null) { + this.resource = null; + } + requestRepaint(); + } + +} diff --git a/src/com/itmill/toolkit/ui/OrderedLayout.java b/src/com/itmill/toolkit/ui/OrderedLayout.java new file mode 100644 index 0000000000..eacdbac926 --- /dev/null +++ b/src/com/itmill/toolkit/ui/OrderedLayout.java @@ -0,0 +1,215 @@ +/* ************************************************************************* + + IT Mill Toolkit + + Development of Browser User Intarfaces Made Easy + + Copyright (C) 2000-2006 IT Mill Ltd + + ************************************************************************* + + This product is distributed under commercial license that can be found + from the product package on license/license.txt. Use of this product might + require purchasing a commercial license from IT Mill Ltd. For guidelines + on usage, see license/licensing-guidelines.html + + ************************************************************************* + + For more information, contact: + + IT Mill Ltd phone: +358 2 4802 7180 + Ruukinkatu 2-4 fax: +358 2 4802 7181 + 20540, Turku email: info@itmill.com + Finland company www: www.itmill.com + + Primary source for information and releases: www.itmill.com + + ********************************************************************** */ + +package com.itmill.toolkit.ui; + +import com.itmill.toolkit.terminal.PaintException; +import com.itmill.toolkit.terminal.PaintTarget; + +import java.util.Iterator; +import java.util.LinkedList; + +/** Ordered layout. + * + * Ordered layout 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 + */ +public class OrderedLayout + extends AbstractComponentContainer + implements Layout { + + /* Predefined orientations ***************************************** */ + + /** Components are to be layed out vertically. */ + public static int ORIENTATION_VERTICAL = 0; + /** Components are to be layed out horizontally. */ + public static int ORIENTATION_HORIZONTAL = 1; + + /** Custom layout slots containing the components */ + private LinkedList components = new LinkedList(); + + /** Orientation of the layout. */ + private int orientation; + + /** Create a new ordered layout. + * The order of the layout is ORIENTATION_VERTICAL. + */ + public OrderedLayout() { + orientation = ORIENTATION_VERTICAL; + } + + /** Create a new ordered layout. + * The orientation of the layout is given as parameters. + * + * @param orientation Orientation of the layout. + */ + public OrderedLayout(int orientation) { + this.orientation = orientation; + } + + /** Get component UIDL tag. + * @return Component UIDL tag as string. + */ + 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. + */ + public void addComponent(Component c) { + components.add(c); + super.addComponent(c); + requestRepaint(); + } + + /** Add 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) { + components.addFirst(c); + super.addComponent(c); + requestRepaint(); + } + + /** Add a component into indexed position in this container. + * @param c The component to be added. + * @param index Index of the component position. + * The components currently in and after the position are shifted forwards. + */ + public void addComponent(Component c, int index) { + components.add(index, c); + super.addComponent(c); + requestRepaint(); + } + + /** Remove a component from this container. + * @param c The component to be removed. + */ + public void removeComponent(Component c) { + super.removeComponent(c); + components.remove(c); + requestRepaint(); + } + + /** Get component container iterator for going trough all the components in + * the container. + * @return Iterator of the components inside the container. + */ + public Iterator getComponentIterator() { + return components.iterator(); + } + + /** Paint the content of this component. + * @param event PaintEvent. + * @throws PaintException The paint operation failed. + */ + public void paintContent(PaintTarget target) throws PaintException { + + // Add the attributes: orientation + // note that the default values (b/vertival) are omitted + if (orientation == ORIENTATION_HORIZONTAL) + target.addAttribute("orientation", "horizontal"); + + // Add all items in all the locations + for (Iterator i = components.iterator(); i.hasNext();) { + Component c = (Component) i.next(); + if (c != null) { + c.paint(target); + } + } + } + + /** Get the orientation of the container. + * @return Value of property orientation. + */ + public int getOrientation() { + return this.orientation; + } + + /** Set the orientation of the container. + * @param orientation New value of property orientation. + */ + public void setOrientation(int orientation) { + + // Check the validity of the argument + if (orientation < ORIENTATION_VERTICAL + || orientation > ORIENTATION_HORIZONTAL) + throw new IllegalArgumentException(); + + this.orientation = orientation; + } + + /* Documented in superclass */ + public void replaceComponent( + Component oldComponent, + Component newComponent) { + + // Get the locations + int oldLocation = -1; + int newLocation = -1; + int location = 0; + for (Iterator i = components.iterator(); i.hasNext();) { + 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); + components.add(oldLocation, newComponent); + } else { + components.remove(newComponent); + components.add(oldLocation, newComponent); + components.remove(oldComponent); + components.add(newLocation, oldComponent); + } + + requestRepaint(); + } + } +} diff --git a/src/com/itmill/toolkit/ui/Panel.java b/src/com/itmill/toolkit/ui/Panel.java new file mode 100644 index 0000000000..08d6482d43 --- /dev/null +++ b/src/com/itmill/toolkit/ui/Panel.java @@ -0,0 +1,375 @@ +/* ************************************************************************* + + IT Mill Toolkit + + Development of Browser User Intarfaces Made Easy + + Copyright (C) 2000-2006 IT Mill Ltd + + ************************************************************************* + + This product is distributed under commercial license that can be found + from the product package on license/license.txt. Use of this product might + require purchasing a commercial license from IT Mill Ltd. For guidelines + on usage, see license/licensing-guidelines.html + + ************************************************************************* + + For more information, contact: + + IT Mill Ltd phone: +358 2 4802 7180 + Ruukinkatu 2-4 fax: +358 2 4802 7181 + 20540, Turku email: info@itmill.com + Finland company www: www.itmill.com + + Primary source for information and releases: www.itmill.com + + ********************************************************************** */ + +package com.itmill.toolkit.ui; + +import java.util.Iterator; +import java.util.Map; + +import com.itmill.toolkit.terminal.PaintException; +import com.itmill.toolkit.terminal.PaintTarget; +import com.itmill.toolkit.terminal.Scrollable; +import com.itmill.toolkit.terminal.Sizeable; + +/** Panel - a simple single component container. + * @author IT Mill Ltd. + * @version @VERSION@ + * @since 3.0 + */ +public class Panel + extends AbstractComponentContainer + implements + Sizeable, + Scrollable, + ComponentContainer.ComponentAttachListener, + ComponentContainer.ComponentDetachListener { + + /** Layout of the panel */ + private Layout layout; + + /** Width of the panel or -1 if unspecified */ + private int width = -1; + + /** Height of the panel or -1 if unspecified */ + private int height = -1; + + /** Width unit */ + private int widthUnit = Sizeable.UNITS_PIXELS; + + /** Height unit */ + private int heightUnit = Sizeable.UNITS_PIXELS; + + /** Scroll X position */ + private int scrollOffsetX = 0; + + /** Scroll Y position */ + private int scrollOffsetY = 0; + + /** Scrolling mode */ + private boolean scrollable = false; + + /** Create new empty panel. + * Ordered layout is used. + */ + public Panel() { + this(new OrderedLayout()); + } + + /** Create new empty panel with given layout. + * Layout must be non-null. + * + * @param layout The layout used in the panel. + */ + public Panel(Layout layout) { + setLayout(layout); + } + + /** Create new empty panel with caption. + * Ordered layout is used. + * + * @param caption The caption used in the panel. + */ + public Panel(String caption) { + this(caption, new OrderedLayout()); + } + + /** Create new empty panel with caption. + * + * @param caption The caption of the panel. + * @param layout The layout used in the panel. + */ + public Panel(String caption, Layout layout) { + this(layout); + setCaption(caption); + } + + /** Get the current layout of the panel. + * @return Current layout of the panel. + */ + public Layout getLayout() { + return this.layout; + } + + /** Set the layout of the panel. + * All the components are moved to new layout. + * + * @param layout New layout of the panel. + */ + public void setLayout(Layout layout) { + + // Only allow non-null layouts + if (layout == null) + layout = new OrderedLayout(); + + // Set the panel to be parent for the layout + layout.setParent(this); + dependsOn(layout); + + // If panel already contains a layout, move the contents to new one + // and detach old layout from the panel + if (this.layout != null) { + layout.moveComponentsFrom(this.layout); + removeDirectDependency(this.layout); + this.layout.setParent(null); + } + + // Remove the event listeners from the old layout + if (this.layout != null) { + this.layout.removeListener((ComponentContainer.ComponentAttachListener) this); + this.layout.removeListener((ComponentContainer.ComponentDetachListener) this); + } + + // Set the new layout + this.layout = layout; + + // Add event listeners for new layout + layout.addListener((ComponentContainer.ComponentAttachListener) this); + layout.addListener((ComponentContainer.ComponentDetachListener) this); + } + + /** Paint the content of this component. + * @param event PaintEvent. + * @throws PaintException The paint operation failed. + */ + public void paintContent(PaintTarget target) throws PaintException { + layout.paint(target); + target.addVariable(this, "height", getHeight()); + target.addVariable(this, "width", getWidth()); + if (isScrollable()) { + target.addVariable(this, "scrollleft", getScrollOffsetX()); + target.addVariable(this, "scrolldown", getScrollOffsetY()); + } + } + + /** Get component UIDL tag. + * @return Component UIDL tag as string. + */ + public String getTag() { + return "panel"; + } + + /** Add a component into this container. + * @param c The component to be added. + */ + public void addComponent(Component c) { + layout.addComponent(c); + // No repaint request is made as we except the underlaying container to + // request repaints + } + + /** Remove a component from this container. + * @param c The component to be added. + */ + public void removeComponent(Component c) { + layout.removeComponent(c); + // No repaint request is made as we except the underlaying container to + // request repaints + } + + /** Get component container iterator for going trough all the components in the container. + * @return Iterator of the components inside the container. + */ + public Iterator getComponentIterator() { + return layout.getComponentIterator(); + } + + /** + * @return The height in pixels or negative value if not assigned. + */ + public int getHeight() { + return height; + } + + /** + * @return The width in pixels or negative value if not assigned. + */ + public int getWidth() { + return width; + } + + /** Sets the height in pixels. + * Use negative value to let the client decide the height. + * @param height The height to set + */ + public void setHeight(int height) { + this.height = height; + requestRepaint(); + } + + /** Sets the width in pixels. + * Use negative value to allow the client decide the width. + * @param width The width to set + */ + public void setWidth(int width) { + this.width = width; + requestRepaint(); + } + + /** + * @see com.itmill.toolkit.terminal.VariableOwner#changeVariables(Object, Map) + */ + public void changeVariables(Object source, Map variables) { + super.changeVariables(source, variables); + + // Get new size + Integer newWidth = (Integer) variables.get("width"); + Integer newHeight = (Integer) variables.get("height"); + if (newWidth != null && newWidth.intValue() != getWidth()) + setWidth(newWidth.intValue()); + if (newHeight != null && newHeight.intValue() != getHeight()) + setHeight(newHeight.intValue()); + + // Scrolling + Integer newScrollX = (Integer) variables.get("scrollleft"); + Integer newScrollY = (Integer) variables.get("scrolldown"); + if (newScrollX != null && newScrollX.intValue() != getScrollOffsetX()) + setScrollOffsetX(newScrollX.intValue()); + if (newScrollY != null && newScrollY.intValue() != getScrollOffsetY()) + setScrollOffsetY(newScrollY.intValue()); + } + + /** + * @see com.itmill.toolkit.terminal.Sizeable#getHeightUnits() + */ + public int getHeightUnits() { + return heightUnit; + } + + /** + * @see com.itmill.toolkit.terminal.Sizeable#getWidthUnits() + */ + public int getWidthUnits() { + return widthUnit; + } + + /** Set height units. + * Panel supports only Sizeable.UNITS_PIXELS and this is ignored. + * @see com.itmill.toolkit.terminal.Sizeable#setHeightUnits(int) + */ + public void setHeightUnits(int units) { + // Ignored + } + + /** Set width units. + * Panel supports only Sizeable.UNITS_PIXELS, and this is ignored. + * @see com.itmill.toolkit.terminal.Sizeable#setWidthUnits(int) + */ + public void setWidthUnits(int units) { + // Ignored + } + + /* Scrolling functionality */ + + /* Documented in interface */ + public int getScrollOffsetX() { + return scrollOffsetX; + } + + /* Documented in interface */ + public int getScrollOffsetY() { + return scrollOffsetY; + } + + /* 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 setScrollOffsetX(int pixelsScrolledLeft) { + if (pixelsScrolledLeft < 0) + throw new IllegalArgumentException("Scroll offset must be at least 0"); + if (this.scrollOffsetX != pixelsScrolledLeft) { + scrollOffsetX = pixelsScrolledLeft; + requestRepaint(); + } + } + + /* Documented in interface */ + public void setScrollOffsetY(int pixelsScrolledDown) { + if (pixelsScrolledDown < 0) + throw new IllegalArgumentException("Scroll offset must be at least 0"); + if (this.scrollOffsetY != pixelsScrolledDown) { + scrollOffsetY = pixelsScrolledDown; + requestRepaint(); + } + } + + /* Documented in superclass */ + public void replaceComponent( + Component oldComponent, + Component newComponent) { + + layout.replaceComponent(oldComponent, newComponent); + } + + /** Pass the events from underlying layout forwards. + * @see com.itmill.toolkit.ui.ComponentContainer.ComponentAttachListener#componentAttachedToContainer(com.itmill.toolkit.ui.ComponentContainer.ComponentAttachEvent) + */ + public void componentAttachedToContainer(ComponentAttachEvent event) { + if (event.getContainer() == layout) + fireComponentAttachEvent(event.getAttachedComponent()); + } + + /** Pass the events from underlying layout forwards. + * @see com.itmill.toolkit.ui.ComponentContainer.ComponentDetachListener#componentDetachedFromContainer(com.itmill.toolkit.ui.ComponentContainer.ComponentDetachEvent) + */ + public void componentDetachedFromContainer(ComponentDetachEvent event) { + if (event.getContainer() == layout) + fireComponentDetachEvent(event.getDetachedComponent()); + } + + /* + * @see com.itmill.toolkit.ui.Component#attach() + */ + public void attach() { + if (layout != null) layout.attach(); + } + + /* + * @see com.itmill.toolkit.ui.Component#detach() + */ + public void detach() { + if (layout != null) layout.detach(); + } + /* + * @see com.itmill.toolkit.ui.ComponentContainer#removeAllComponents() + */ + public void removeAllComponents() { + layout.removeAllComponents(); + } + +} diff --git a/src/com/itmill/toolkit/ui/Select.java b/src/com/itmill/toolkit/ui/Select.java new file mode 100644 index 0000000000..be73c31420 --- /dev/null +++ b/src/com/itmill/toolkit/ui/Select.java @@ -0,0 +1,1202 @@ +/* ************************************************************************* + + IT Mill Toolkit + + Development of Browser User Intarfaces Made Easy + + Copyright (C) 2000-2006 IT Mill Ltd + + ************************************************************************* + + This product is distributed under commercial license that can be found + from the product package on license/license.txt. Use of this product might + require purchasing a commercial license from IT Mill Ltd. For guidelines + on usage, see license/licensing-guidelines.html + + ************************************************************************* + + For more information, contact: + + IT Mill Ltd phone: +358 2 4802 7180 + Ruukinkatu 2-4 fax: +358 2 4802 7181 + 20540, Turku email: info@itmill.com + Finland company www: www.itmill.com + + Primary source for information and releases: www.itmill.com + + ********************************************************************** */ + +package com.itmill.toolkit.ui; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.Map; +import java.util.Set; + +import com.itmill.toolkit.data.Container; +import com.itmill.toolkit.data.Item; +import com.itmill.toolkit.data.Property; +import com.itmill.toolkit.data.util.IndexedContainer; +import com.itmill.toolkit.terminal.KeyMapper; +import com.itmill.toolkit.terminal.PaintException; +import com.itmill.toolkit.terminal.PaintTarget; +import com.itmill.toolkit.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.itmill.toolkit.data.Item}s in a + * {@link com.itmill.toolkit.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 + */ +public class Select + 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.itmill.toolkit.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; + + /** 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 HashMap itemIcons = new HashMap(); + + /** Item captions */ + private 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 LinkedList propertySetEventListeners = null; + + /** List of item set change event listeners */ + private LinkedList itemSetEventListeners = null; + + /** Item id that represents null selection 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> + */ + private Object nullSelectionItemId = null; + + /* Constructors ********************************************************* */ + + /** Creates an empty Select. + * The caption is not used. + */ + public Select() { + setContainerDataSource(new IndexedContainer()); + } + + /** Creates an empty Select with caption. + */ + public Select(String caption) { + setContainerDataSource(new IndexedContainer()); + setCaption(caption); + } + + /** Creates a new select wthat is connected to a data-source. + * @param dataSource Container datasource to be selected from by this select. + * @param caption Caption of the component. + * @param selected Selected item id or null, if none selected. + */ + public Select(String caption, Container dataSource) { + setCaption(caption); + setContainerDataSource(dataSource); + } + + /** Creates a new select that is filled from a collection of option values. + * @param caption Caption of this field. + * @param options Collection containing the options. + * @param selected Selected option or null, if none selected. + */ + public Select(String caption, Collection options) { + + // Create options container and add given options to it + Container c = new IndexedContainer(); + if (options != null) + for (Iterator i = options.iterator(); i.hasNext();) + c.addItem(i.next()); + + setCaption(caption); + setContainerDataSource((Container) c); + } + + /* Component methods **************************************************** */ + + /** Paint the content of this component. + * @param event PaintEvent. + * @throws PaintException The paint operation failed. + */ + public void paintContent(PaintTarget target) throws PaintException { + + // Paint field properties + super.paintContent(target); + + // Paint select attributes + if (isMultiSelect()) + target.addAttribute("selectmode", "multi"); + if (isNewItemsAllowed()) + target.addAttribute("allownewitem", true); + + // Paint options and create array of selected id keys + String[] selectedKeys; + if (isMultiSelect()) + selectedKeys = new String[((Set) getValue()).size()]; + else + selectedKeys = + new String[( + getValue() == null + && getNullSelectionItemId() == null ? 0 : 1)]; + int keyIndex = 0; + target.startTag("options"); + + // Support for external null selection item id + Collection ids = getItemIds(); + if (getNullSelectionItemId() != null + && (!ids.contains(getNullSelectionItemId()))) { + + // Get the option attribute values + Object id = getNullSelectionItemId(); + String key = itemIdMapper.key(id); + String caption = getItemCaption(id); + Resource icon = getItemIcon(id); + + // Paint 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"); + } + + // Paint available selection options from data source + for (Iterator i = getItemIds().iterator(); i.hasNext();) { + + // Get the option attribute values + Object id = i.next(); + String key = itemIdMapper.key(id); + String caption = getItemCaption(id); + Resource icon = getItemIcon(id); + + // Paint 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. + * @param event Variable change event containing the information about + * the changed variable. + */ + public void changeVariables(Object source, Map variables) { + + // Try to set the property value + + // New option entered (and it is allowed) + String newitem = (String) variables.get("newitem"); + if (newitem != null && newitem.length() > 0) { + + // Check for readonly + if (isReadOnly()) + throw new Property.ReadOnlyException(); + + // Add new option + if (addItem(newitem) != null) { + + // Set the caption property, if used + if (getItemCaptionPropertyId() != null) + try { + getContainerProperty( + newitem, + getItemCaptionPropertyId()).setValue( + newitem); + } catch (Property.ConversionException ignored) { + // The conversion exception is safely ignored, the caption is + // just missing + } + } + } + + // Selection change + if (variables.containsKey("selected")) { + String[] ka = (String[]) variables.get("selected"); + + // Multiselect mode + if (isMultiSelect()) { + + // Convert the key-array to id-set + LinkedList s = new LinkedList(); + for (int i = 0; i < ka.length; i++) { + Object id = itemIdMapper.get(ka[i]); + if (id != null && containsId(id)) + s.add(id); + else if ( + itemIdMapper.isNewIdKey(ka[i]) + && newitem != null + && newitem.length() > 0) + s.add(newitem); + } + + // Limit the deselection to the set of visible items + // (non-visible items can not be deselected) + 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); + super.setValue(newsel); + } + } + + // Single select mode + else { + if (ka.length == 0) { + + // Allow deselection only if the deselected item is visible + Object current = getValue(); + Collection visible = getVisibleItemIds(); + if (visible != null && visible.contains(current)) + setValue(null); + } else { + Object id = itemIdMapper.get(ka[0]); + if (id != null && id.equals(getNullSelectionItemId())) + setValue(null); + else if (itemIdMapper.isNewIdKey(ka[0])) + setValue(newitem); + else + setValue(id); + } + } + } + } + + /** Get component UIDL tag. + * @return Component UIDL tag as string. + */ + public String getTag() { + return "select"; + } + + /** Get 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 ***************************************************** */ + + /** Return the type of the property. + * getValue and setValue functions must be compatible with this type: + * one can safely cast getValue() to given type and pass any variable + * assignable to this type as a parameter to setValue(). + * @return type Type of the property. + */ + public Class getType() { + if (isMultiSelect()) + return Set.class; + else + return Object.class; + } + + /** Get the selected item id or in multiselect mode a set of selected ids. + */ + public Object getValue() { + 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 { + Set s = new HashSet(); + if (items.containsId(retValue)) + s.add(retValue); + return s; + } + + } else + return retValue; + } + + /** Set 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 New selected item or collection of selected items. + */ + public void setValue(Object newValue) + throws Property.ReadOnlyException, Property.ConversionException { + + if (isMultiSelect()) { + if (newValue == null) + super.setValue(new HashSet()); + else if (Collection.class.isAssignableFrom(newValue.getClass())) + super.setValue(new HashSet((Collection) newValue)); + } else if (newValue == null || items.containsId(newValue)) + super.setValue(newValue); + } + + /* Container methods **************************************************** */ + + /** Get the item from the container with given id. + * If the container does not contain the requested item, null is returned. + */ + public Item getItem(Object itemId) { + return items.getItem(itemId); + } + + /** Get item Id collection from the container. + * @return Collection of item ids. + */ + public Collection getItemIds() { + return items.getItemIds(); + } + + /** Get property Id collection from the container. + * @return Collection of property ids. + */ + public Collection getContainerPropertyIds() { + return items.getContainerPropertyIds(); + } + + /** Get property type. + * @param id Id identifying the of the property. + */ + public Class getType(Object propertyId) { + return items.getType(propertyId); + } + + /** Get the number of items in the container. + * @return Number of items in the container. + */ + public int size() { + return items.size(); + } + + /** Test, if the collection contains an item with given id. + * @param itemId Id the of item to be tested. + */ + public boolean containsId(Object itemId) { + if (itemId != null) + return items.containsId(itemId); + else + return false; + } + + /** + * @see com.itmill.toolkit.data.Container#getContainerProperty(Object, Object) + */ + public Property getContainerProperty(Object itemId, Object propertyId) { + return items.getContainerProperty(itemId, propertyId); + } + + /* Container.Managed methods ******************************************** */ + + /** Add 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 iff the operation succeeded. + */ + public boolean addContainerProperty( + Object propertyId, + Class type, + Object defaultValue) + throws UnsupportedOperationException { + + boolean retval = + items.addContainerProperty(propertyId, type, defaultValue); + if (retval + && !(items instanceof Container.PropertySetChangeNotifier)) { + firePropertySetChange(); + } + return retval; + } + + /** Remove all items from the container. + * + * This functionality is optional. If the function is unsupported, it always + * returns false. + * + * @return True iff the operation succeeded. + */ + public boolean removeAllItems() throws UnsupportedOperationException { + + boolean retval = items.removeAllItems(); + this.itemIdMapper.removeAll(); + if (retval) { + setValue(null); + if (!(items instanceof Container.ItemSetChangeNotifier)) + fireItemSetChange(); + } + return retval; + } + + /** Create 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 Id of the created item or null in case of failure. + */ + public Object addItem() throws UnsupportedOperationException { + + 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 Identification of the item to be created. + * @return Created item with the given id, or null in case of failure. + */ + public Item addItem(Object itemId) throws UnsupportedOperationException { + + Item retval = items.addItem(itemId); + if (retval != null + && !(items instanceof Container.ItemSetChangeNotifier)) + fireItemSetChange(); + return retval; + } + + /** Remove item identified by Id from the container. + * This functionality is optional. If the function is not implemented, + * the functions allways returns false. + * + * @return True iff the operation succeeded. + */ + public boolean removeItem(Object itemId) + throws UnsupportedOperationException { + + unselect(itemId); + boolean retval = items.removeItem(itemId); + this.itemIdMapper.remove(itemId); + if (retval && !(items instanceof Container.ItemSetChangeNotifier)) + fireItemSetChange(); + return retval; + } + + /** Remove 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 iff the operation succeeded. + */ + public boolean removeContainerProperty(Object propertyId) + throws UnsupportedOperationException { + + boolean retval = items.removeContainerProperty(propertyId); + if (retval && !(items instanceof Container.PropertySetChangeNotifier)) + firePropertySetChange(); + return retval; + } + + /* Container.Viewer methods ********************************************* */ + + /** Set the container as data-source for viewing. */ + public void setContainerDataSource(Container newDataSource) { + if (newDataSource == null) + newDataSource = new IndexedContainer(); + + if (items != newDataSource) { + + // Remove listeners from the old datasource + if (items != null) { + try { + ((Container.ItemSetChangeNotifier) items).removeListener( + (Container.ItemSetChangeListener) this); + } catch (ClassCastException ignored) { + // Ignored + } + try { + ( + ( + Container + .PropertySetChangeNotifier) items) + .removeListener( + (Container.PropertySetChangeListener) this); + } catch (ClassCastException ignored) { + // Ignored + } + } + + // Assign new data source + items = newDataSource; + + // Clear itemIdMapper also + this.itemIdMapper.removeAll(); + + // Add listeners + if (items != null) { + try { + ((Container.ItemSetChangeNotifier) items).addListener( + (Container.ItemSetChangeListener) this); + } catch (ClassCastException ignored) { + // Ignored + } + try { + ((Container.PropertySetChangeNotifier) items).addListener( + (Container.PropertySetChangeListener) this); + } catch (ClassCastException ignored) { + // Ignored + } + } + //TODO: This should be conditional + fireValueChange(); + } + } + + /** Get viewing data-source container. */ + public Container getContainerDataSource() { + return items; + } + + /* Select attributes **************************************************** */ + + /** Is the select in multiselect mode? In multiselect mode + * @return Value of property multiSelect. + */ + public boolean isMultiSelect() { + return this.multiSelect; + } + + /** Set 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 New value of property multiSelect. + */ + public void setMultiSelect(boolean multiSelect) { + + if (multiSelect != this.multiSelect) { + + // Selection before mode change + Object oldValue = getValue(); + + this.multiSelect = multiSelect; + + // Convert the value type + if (multiSelect) { + Set s = new HashSet(); + if (oldValue != null) + s.add(oldValue); + setValue(s); + } else { + 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. No that data-source must allow adding new + * items (it must implement Container.Managed). + * @return True iff additions are allowed. + */ + public boolean isNewItemsAllowed() { + + return this.allowNewOptions; + } + + /** Enable or disable possibility to add new options by the user. + * @param allowNewOptions 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 New caption. + */ + public void setItemCaption(Object itemId, String caption) { + if (itemId != null) { + itemCaptions.put(itemId, caption); + requestRepaint(); + } + } + + /** Get 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 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 : + try { + caption = + String.valueOf( + ((Container.Indexed) items).indexOfId(itemId)); + } catch (ClassCastException ignored) { + } + break; + + case ITEM_CAPTION_MODE_ITEM : + 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 : + Property p = + getContainerProperty(itemId, getItemCaptionPropertyId()); + if (p != null) + caption = p.toString(); + break; + } + + // All items must have some captions + return caption != null ? caption : ""; + } + + /** Set icon for an item. + * + * @param itemId The id of the item to be assigned an icon. + * @param icon New icon. + */ + public void setItemIcon(Object itemId, Resource icon) { + if (itemId != null) { + if (icon == null) + itemIcons.remove(itemId); + else + itemIcons.put(itemId, icon); + requestRepaint(); + } + } + + /** Get the item icon. + * + * @param itemId The id of the item to be assigned an icon. + * @return Icon for the item or null, if not specified. + */ + public Resource getItemIcon(Object itemId) { + Resource explicit = (Resource) itemIcons.get(itemId); + if (explicit != null) + return explicit; + + if (getItemIconPropertyId() == null) + return null; + + Property ip = getContainerProperty(itemId, getItemIconPropertyId()); + if (ip == null) + return null; + Object icon = ip.getValue(); + if (icon instanceof Resource) + return (Resource) icon; + + return null; + } + + /** Set 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 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(); + } + } + + /** Get 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 One of the modes listed above. + */ + public int getItemCaptionMode() { + return itemCaptionMode; + } + + /** Set 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>. + * + */ + 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(); + } + } + + /** Get the item caption property. + * + * @return Id of the property used as item caption source. + */ + public Object getItemCaptionPropertyId() { + return itemCaptionPropertyId; + } + + /** Set 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 that 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 Id of the property that specifies icons for + * items. + */ + public void setItemIconPropertyId(Object propertyId) { + if ((propertyId != null) + && Resource.class.isAssignableFrom(getType(propertyId))) { + itemIconPropertyId = propertyId; + requestRepaint(); + } else + itemIconPropertyId = null; + } + + /** Get 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 that 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 Id of the property containing the item icons. + */ + public Object getItemIconPropertyId() { + return itemIconPropertyId; + } + + /** Test 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> + * + * @see #getNullSelectionItemId() + * @see #setNullSelectionItemId(Object) + * @param itemId Id the of the item to be tested + */ + public boolean isSelected(Object itemId) { + if (itemId == null) + return false; + if (isMultiSelect()) + return ((Set) getValue()).contains(itemId); + else { + Object value = getValue(); + return itemId.equals( + value == null ? getNullSelectionItemId() : value); + } + } + + /** Select an item. + * + * <p>In single select mode selecting item identified + * by {@link #getNullSelectionItemId()} sets the value of the + * property to null.</p> + * + * @see #getNullSelectionItemId() + * @see #setNullSelectionItemId(Object) + * @param itemId Item to be selected. + */ + public void select(Object itemId) { + if (!isSelected(itemId) && items.containsId(itemId)) { + if (isMultiSelect()) { + Set s = new HashSet((Set) getValue()); + s.add(itemId); + setValue(s); + } else if (itemId.equals(getNullSelectionItemId())) + setValue(null); + else + setValue(itemId); + } + } + + /** Unselect an item. + * + * @see #getNullSelectionItemId() + * @see #setNullSelectionItemId(Object) + * @param itemId Item to be unselected. + */ + public void unselect(Object itemId) { + if (isSelected(itemId)) { + if (isMultiSelect()) { + Set s = new HashSet((Set) getValue()); + s.remove(itemId); + setValue(s); + } else + setValue(null); + } + } + + /** + * @see com.itmill.toolkit.data.Container.PropertySetChangeListener#containerPropertySetChange(com.itmill.toolkit.data.Container.PropertySetChangeEvent) + */ + public void containerPropertySetChange( + Container.PropertySetChangeEvent event) { + firePropertySetChange(); + } + + /** + * @see com.itmill.toolkit.data.Container.PropertySetChangeNotifier#addListener(com.itmill.toolkit.data.Container.PropertySetChangeListener) + */ + public void addListener(Container.PropertySetChangeListener listener) { + if (propertySetEventListeners == null) + propertySetEventListeners = new LinkedList(); + propertySetEventListeners.add(listener); + } + + /** + * @see com.itmill.toolkit.data.Container.PropertySetChangeNotifier#removeListener(com.itmill.toolkit.data.Container.PropertySetChangeListener) + */ + public void removeListener(Container.PropertySetChangeListener listener) { + if (propertySetEventListeners != null) { + propertySetEventListeners.remove(listener); + if (propertySetEventListeners.isEmpty()) + propertySetEventListeners = null; + } + } + + /** + * @see com.itmill.toolkit.data.Container.ItemSetChangeNotifier#addListener(com.itmill.toolkit.data.Container.ItemSetChangeListener) + */ + public void addListener(Container.ItemSetChangeListener listener) { + if (itemSetEventListeners == null) + itemSetEventListeners = new LinkedList(); + itemSetEventListeners.add(listener); + } + + /** + * @see com.itmill.toolkit.data.Container.ItemSetChangeNotifier#removeListener(com.itmill.toolkit.data.Container.ItemSetChangeListener) + */ + public void removeListener(Container.ItemSetChangeListener listener) { + if (itemSetEventListeners != null) { + itemSetEventListeners.remove(listener); + if (itemSetEventListeners.isEmpty()) + itemSetEventListeners = null; + } + } + + /** + * @see com.itmill.toolkit.data.Container.ItemSetChangeListener#containerItemSetChange(com.itmill.toolkit.data.Container.ItemSetChangeEvent) + */ + public void containerItemSetChange(Container.ItemSetChangeEvent event) { + // Clear item id mapping table + this.itemIdMapper.removeAll(); + + // Notify all listeners + fireItemSetChange(); + } + + /** Fire property set change event */ + protected void firePropertySetChange() { + if (propertySetEventListeners != null + && !propertySetEventListeners.isEmpty()) { + Container.PropertySetChangeEvent event = + new PropertySetChangeEvent(); + Object[] listeners = propertySetEventListeners.toArray(); + for (int i = 0; i < listeners.length; i++) + ( + ( + Container + .PropertySetChangeListener) listeners[i]) + .containerPropertySetChange( + event); + } + requestRepaint(); + } + + /** Fire item set change event */ + protected void fireItemSetChange() { + if (itemSetEventListeners != null + && !itemSetEventListeners.isEmpty()) { + Container.ItemSetChangeEvent event = new ItemSetChangeEvent(); + 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 Container.ItemSetChangeEvent { + + /** + * @see com.itmill.toolkit.data.Container.ItemSetChangeEvent#getContainer() + */ + public Container getContainer() { + return Select.this; + } + + } + + /** Implementation of property set change event */ + private class PropertySetChangeEvent + implements Container.PropertySetChangeEvent { + + /** + * @see com.itmill.toolkit.data.Container.PropertySetChangeEvent#getContainer() + */ + public Container getContainer() { + return Select.this; + } + + } + /** 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 idetified + * by this id is the same as selecting no items at all. This setting only affects the + * single select mode.</p> + + * @return 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 nullPropertyValueContainerItemId The nullPropertyValueContainerItemId to set + * @see #getNullSelectionItemId() + * @see #isSelected(Object) + * @see #select(Object) + */ + public void setNullSelectionItemId(Object nullSelectionItemId) { + this.nullSelectionItemId = nullSelectionItemId; + } +} diff --git a/src/com/itmill/toolkit/ui/TabSheet.java b/src/com/itmill/toolkit/ui/TabSheet.java new file mode 100644 index 0000000000..adadc476e6 --- /dev/null +++ b/src/com/itmill/toolkit/ui/TabSheet.java @@ -0,0 +1,398 @@ +/* ************************************************************************* + + IT Mill Toolkit + + Development of Browser User Intarfaces Made Easy + + Copyright (C) 2000-2006 IT Mill Ltd + + ************************************************************************* + + This product is distributed under commercial license that can be found + from the product package on license/license.txt. Use of this product might + require purchasing a commercial license from IT Mill Ltd. For guidelines + on usage, see license/licensing-guidelines.html + + ************************************************************************* + + For more information, contact: + + IT Mill Ltd phone: +358 2 4802 7180 + Ruukinkatu 2-4 fax: +358 2 4802 7181 + 20540, Turku email: info@itmill.com + Finland company www: www.itmill.com + + Primary source for information and releases: www.itmill.com + + ********************************************************************** */ + +package com.itmill.toolkit.ui; + +import java.lang.reflect.Method; +import java.util.Hashtable; +import java.util.LinkedList; +import java.util.Iterator; +import java.util.Map; + +import com.itmill.toolkit.terminal.*; + +/** Tabsheet component. + * + * @author IT Mill Ltd. + * @version @VERSION@ + * @since 3.0 + */ +public class TabSheet extends AbstractComponentContainer { + + /** Linked list of component tabs */ + private LinkedList tabs = new LinkedList(); + + /** Tab -> caption mapping */ + private Hashtable tabCaptions = new Hashtable(); + + /** Tab -> icon mapping */ + private Hashtable tabIcons = new Hashtable(); + + /** Selected tab */ + private Component selected = null; + private KeyMapper keyMapper = new KeyMapper(); + + /** Holds value of property tabsHIdden. */ + private boolean tabsHidden; + + /** Construct new Tabsheet. + * Tabsheet is immediate by default. + */ + public TabSheet() { + super(); + setImmediate(true); + } + + /** Get component container iterator for going trough all the components in the container. + * @return Iterator of the components inside the container. + */ + public Iterator getComponentIterator() { + return java.util.Collections.unmodifiableList(tabs).iterator(); + } + + /** Remove a component from this container. + * @param c The component to be removed. + */ + public void removeComponent(Component c) { + if (c != null && tabs.contains(c)) { + super.removeComponent(c); + keyMapper.remove(c); + tabs.remove(c); + tabCaptions.remove(c); + if (c.equals(selected)) { + if (tabs.isEmpty()) + selected = null; + else { + selected = (Component) tabs.getFirst(); + fireSelectedTabChange(); + } + } + requestRepaint(); + } + } + + /** Add a component into this container. + * The component is added as a tab where its default tab-caption is + * the caption of the component. + * @param c The component to be added. + */ + public void addComponent(Component c) { + addTab(c, c.getCaption(), getIcon()); + } + + /** Add a new tab into TabSheet. + * @param c The component to be added onto tab. + * @param caption The caption of the tab. + * @param icon Set the icon of the tab. + */ + public void addTab(Component c, String caption, Resource icon) { + if (c != null) { + tabs.addLast(c); + tabCaptions.put(c, caption != null ? caption : ""); + if (icon != null) + tabIcons.put(c, icon); + if (selected == null) { + selected = c; + fireSelectedTabChange(); + } + super.addComponent(c); + requestRepaint(); + } + } + + /** Get component UIDL tag. + * @return Component UIDL tag as string. + */ + public String getTag() { + return "tabsheet"; + } + + /** Move all components from another container to this container. + * The components are removed from the other container. + * @param source The container components are removed from. + */ + public void moveComponentsFrom(ComponentContainer source) { + for (Iterator i = source.getComponentIterator(); i.hasNext();) { + 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); + + } + } + + /** Paint the content of this component. + * @param event PaintEvent. + * @throws PaintException The paint operation failed. + */ + public void paintContent(PaintTarget target) throws PaintException { + + if (areTabsHidden()) + target.addAttribute("hidetabs", true); + + target.startTag("tabs"); + + for (Iterator i = getComponentIterator(); i.hasNext();) { + Component c = (Component) i.next(); + if (!c.isVisible()) + continue; + target.startTag("tab"); + Resource icon = getTabIcon(c); + if (icon != null) + target.addAttribute("icon", icon); + String caption = getTabCaption(c); + if (!c.isEnabled()) { + target.addAttribute("disabled", true); + } + + if (caption != null && caption.length() > 0) + target.addAttribute("caption", caption); + target.addAttribute("key", keyMapper.key(c)); + if (c.equals(selected)) { + target.addAttribute("selected", true); + c.paint(target); + } + target.endTag("tab"); + } + + target.endTag("tabs"); + + if (selected != null) + target.addVariable(this, "selected", keyMapper.key(selected)); + } + + /** Are tabs hidden. + * @return Property visibility + */ + public boolean areTabsHidden() { + return this.tabsHidden; + } + + /** Setter for property tabsHidden. + * @param tabsHidden True if the tabs should be hidden. + */ + public void hideTabs(boolean tabsHidden) { + this.tabsHidden = tabsHidden; + requestRepaint(); + } + + /** Get the caption for a component */ + public String getTabCaption(Component c) { + String caption = (String) tabCaptions.get(c); + if (caption == null) + caption = ""; + return caption; + } + + /** Set the caption for a component */ + public void setTabCaption(Component c, String caption) { + tabCaptions.put(c, caption); + requestRepaint(); + } + + /** Get the icon for a component */ + public Resource getTabIcon(Component c) { + return (Resource) tabIcons.get(c); + } + + /** Set the icon for a component */ + public void setTabIcon(Component c, Resource icon) { + if (icon == null) + tabIcons.remove(c); + else + tabIcons.put(c, icon); + requestRepaint(); + } + + /** Set the selected tab */ + public void setSelectedTab(Component c) { + if (c != null && tabs.contains(c) && !selected.equals(c)) { + selected = c; + fireSelectedTabChange(); + requestRepaint(); + } + } + + /** Get the selected tab */ + public Component getSelectedTab() { + return selected; + } + + /** Invoked when the value of a variable has changed. + * @param event Variable change event containing the information about + * the changed variable. + */ + 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) { + + // Get the captions + String oldCaption = getTabCaption(oldComponent); + Resource oldIcon = getTabIcon(oldComponent); + String newCaption = getTabCaption(newComponent); + Resource newIcon = getTabIcon(newComponent); + + // Get the locations + int oldLocation = -1; + int newLocation = -1; + int location = 0; + for (Iterator i = tabs.iterator(); i.hasNext();) { + 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); + addComponent(newComponent); + tabs.remove(newComponent); + tabs.add(oldLocation, newComponent); + setTabCaption(newComponent, oldCaption); + setTabIcon(newComponent, oldIcon); + } else { + if (oldLocation > newLocation) { + tabs.remove(oldComponent); + tabs.add(newLocation, oldComponent); + tabs.remove(newComponent); + tabs.add(oldLocation, newComponent); + } else { + tabs.remove(newComponent); + tabs.add(oldLocation, newComponent); + tabs.remove(oldComponent); + tabs.add(newLocation, oldComponent); + } + setTabCaption(newComponent, oldCaption); + setTabIcon(newComponent, oldIcon); + setTabCaption(oldComponent, newCaption); + setTabIcon(oldComponent, 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 (java.lang.NoSuchMethodException e) { + // This should never happen + throw new java.lang.RuntimeException(); + } + } + + /** 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 { + + /** + * Serial generated by eclipse. + */ + private static final long serialVersionUID = 3258129141914940469L; + + /** New instance of selected tab change event + * @param source Source of the event. + */ + public SelectedTabChangeEvent(Component source) { + super(source); + } + + /** Select where the event occurred + * @return Source of the event. + */ + public Select getSelect() { + return (Select) getSource(); + } + } + + /** Selected Tab Change Event listener + * @author IT Mill Ltd. + * @version @VERSION@ + * @since 3.0 + */ + public interface SelectedTabChangeListener { + + /** Visible tab in tab sheet has has been changed. + * @param event Selected tab change event. + */ + public void selectedTabChange(SelectedTabChangeEvent event); + } + + /** Add selected tab change listener + * @param listener Listener to be added. + */ + public void addListener(SelectedTabChangeListener listener) { + addListener( + SelectedTabChangeEvent.class, + listener, + SELECTED_TAB_CHANGE_METHOD); + } + + /** Remove selected tab change listener + * @param listener Listener to be removed. + */ + public void removeListener(SelectedTabChangeListener listener) { + removeListener( + SelectedTabChangeEvent.class, + listener, + SELECTED_TAB_CHANGE_METHOD); + } + + /** Emit options change event. */ + protected void fireSelectedTabChange() { + fireEvent(new SelectedTabChangeEvent(this)); + } +} diff --git a/src/com/itmill/toolkit/ui/Table.java b/src/com/itmill/toolkit/ui/Table.java new file mode 100644 index 0000000000..08f4be3cf1 --- /dev/null +++ b/src/com/itmill/toolkit/ui/Table.java @@ -0,0 +1,2114 @@ +/* ************************************************************************* + + IT Mill Toolkit + + Development of Browser User Intarfaces Made Easy + + Copyright (C) 2000-2006 IT Mill Ltd + + ************************************************************************* + + This product is distributed under commercial license that can be found + from the product package on license/license.txt. Use of this product might + require purchasing a commercial license from IT Mill Ltd. For guidelines + on usage, see license/licensing-guidelines.html + + ************************************************************************* + + For more information, contact: + + IT Mill Ltd phone: +358 2 4802 7180 + Ruukinkatu 2-4 fax: +358 2 4802 7181 + 20540, Turku email: info@itmill.com + Finland company www: www.itmill.com + + Primary source for information and releases: www.itmill.com + + ********************************************************************** */ + +package com.itmill.toolkit.ui; + +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.StringTokenizer; + +import com.itmill.toolkit.data.Container; +import com.itmill.toolkit.data.Item; +import com.itmill.toolkit.data.Property; +import com.itmill.toolkit.data.util.ContainerOrderedWrapper; +import com.itmill.toolkit.data.util.IndexedContainer; +import com.itmill.toolkit.event.Action; +import com.itmill.toolkit.terminal.KeyMapper; +import com.itmill.toolkit.terminal.PaintException; +import com.itmill.toolkit.terminal.PaintTarget; +import com.itmill.toolkit.terminal.Resource; + +/** + * Table component is used for representing data or components in pageable and + * selectable table. + * + * @author IT Mill Ltd. + * @version @VERSION@ @since 3.0 + */ +public class Table extends Select implements Action.Container, + Container.Ordered, Container.Sortable { + + 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 + * behaviour. </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 = Select.ITEM_CAPTION_MODE_ID; + + /** + * Row caption mode: Item-objects toString() is used as row caption. + */ + public static final int ROW_HEADER_MODE_ITEM = Select.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 = Select.ITEM_CAPTION_MODE_INDEX; + + /** + * Row caption mode: Item captions are explicitly specified. + */ + public static final int ROW_HEADER_MODE_EXPLICIT = Select.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 = Select.ITEM_CAPTION_MODE_PROPERTY; + + /** + * Row caption mode: Only icons are shown, the captions are hidden. + */ + public static final int ROW_HEADER_MODE_ICON_ONLY = Select.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 = Select.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 KeyMapper columnIdMap = new KeyMapper(); + + /** Holds visible column propertyIds - in order */ + private LinkedList visibleColumns = new LinkedList(); + + /** Holds propertyIds of currently collapsed columns */ + private HashSet collapsedColumns = new HashSet(); + + /** Holds headers for visible columns (by propertyId) */ + private HashMap columnHeaders = new HashMap(); + + /** Holds icons for visible columns (by propertyId) */ + private HashMap columnIcons = new HashMap(); + + /** Holds alignments for visible columns (by propertyId) */ + private HashMap columnAlignments = new HashMap(); + + /** Holds value of property pageLength. 0 disables paging. */ + private int pageLength = 0; + + /** 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 pageBuffering. */ + private boolean pageBuffering = false; + + /** Holds value of property selectable. */ + private boolean selectable = false; + + /** Holds value of property columnHeaderMode. */ + private int columnHeaderMode = COLUMN_HEADER_MODE_HIDDEN; + + /** True iff the row captions are hidden. */ + private boolean rowCaptionsAreHidden = true; + + /** Page contents buffer used in buffered mode */ + private Object[][] pageBuffer = null; + + /** + * List of properties listened - the list is kept to release the listeners + * later. + */ + private LinkedList listenedProperties = null; + + /** List of visible components - the is used for needsRepaint calculation. */ + private LinkedList visibleComponents = null; + + /** List of action handlers */ + private LinkedList 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; + + /* Table constructors *************************************************** */ + + /** Create new empty table */ + public Table() { + setRowHeaderMode(ROW_HEADER_MODE_HIDDEN); + } + + /** Create new empty table with caption. */ + public Table(String caption) { + this(); + setCaption(caption); + } + + /** Create new table with caption and connect it to a Container. */ + public Table(String caption, Container dataSource) { + this(); + setCaption(caption); + setContainerDataSource(dataSource); + } + + /* Table functionality ************************************************** */ + + /** + * Get the array of visible column property id:s. + * + * <p> + * The columns are show in the order of their appearance in this array + * </p> + * + * @return Value of property availableColumns. + */ + public Object[] getVisibleColumns() { + if (this.visibleColumns == null) { + return null; + } + return this.visibleColumns.toArray(); + } + + /** + * Set the array of visible column property id:s. + * + * <p> + * The columns are show in the order of their appearance in this array + * </p> + * + * @param availableColumns + * 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"); + + // Check that the new visible columns contains no nulls and properties + // exist + Collection properties = getContainerPropertyIds(); + for (int i = 0; i < visibleColumns.length; i++) { + if (visibleColumns[i] == null) + throw new NullPointerException("Properties must be non-nulls"); + else if (!properties.contains(visibleColumns[i])) + throw new IllegalArgumentException( + "Properties must exist in the Container, missing property: " + + visibleColumns[i]); + } + + // If this is called befor the constructor is finished, it might be + // uninitialized + LinkedList newVC = new LinkedList(); + for (int i = 0; i < visibleColumns.length; i++) { + newVC.add(visibleColumns[i]); + } + + // Remove alignments, icons and headers from hidden columns + if (this.visibleColumns != null) + for (Iterator i = this.visibleColumns.iterator(); i.hasNext();) { + Object col = i.next(); + if (!newVC.contains(col)) { + setColumnHeader(col, null); + setColumnAlignment(col, null); + setColumnIcon(col, null); + } + } + + this.visibleColumns = newVC; + + // Assure visual refresh + refreshCurrentPage(); + } + + /** + * Get 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>ROW_HEADER_MODE_EXPLICIT</code> or + * <code>ROW_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 Array of column headers. + */ + public String[] getColumnHeaders() { + if (this.columnHeaders == null) { + return null; + } + String[] headers = new String[this.visibleColumns.size()]; + int i = 0; + for (Iterator it = this.visibleColumns.iterator(); it.hasNext(); i++) { + headers[i] = (String) this.columnHeaders.get(it.next()); + } + return headers; + } + + /** + * Set 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>ROW_HEADER_MODE_EXPLICIT</code> or + * <code>ROW_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 + * Array of column headers that match the + * <code>getVisibleColumns()</code>. + */ + public void setColumnHeaders(String[] columnHeaders) { + + if (columnHeaders.length != this.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 (Iterator it = this.visibleColumns.iterator(); it.hasNext() + && i < columnHeaders.length; i++) { + this.columnHeaders.put(it.next(), columnHeaders[i]); + } + + // Assure visual refresh + refreshCurrentPage(); + } + + /** + * Get 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>ROW_HEADER_MODE_EXPLICIT</code> or + * <code>ROW_HEADER_MODE_EXPLICIT_DEFAULTS_ID</code> mode to show the + * headers with icons. + * </p> + * + * @return Array of icons that match the <code>getVisibleColumns()</code>. + */ + public Resource[] getColumnIcons() { + if (this.columnIcons == null) { + return null; + } + Resource[] icons = new Resource[this.visibleColumns.size()]; + int i = 0; + for (Iterator it = this.visibleColumns.iterator(); it.hasNext(); i++) { + icons[i] = (Resource) this.columnIcons.get(it.next()); + } + + return icons; + } + + /** + * Set 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>ROW_HEADER_MODE_EXPLICIT</code> or + * <code>ROW_HEADER_MODE_EXPLICIT_DEFAULTS_ID</code> mode to show the + * headers with icons. + * </p> + * + * @param columnIcons + * Array of icons that match the <code>getVisibleColumns()</code>. + */ + public void setColumnIcons(Resource[] columnIcons) { + + if (columnIcons.length != this.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 (Iterator it = this.visibleColumns.iterator(); it.hasNext() + && i < columnIcons.length; i++) { + this.columnIcons.put(it.next(), columnIcons[i]); + } + + // Assure visual refresh + refreshCurrentPage(); + } + + /** + * Get 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 Column alignments array. + */ + public String[] getColumnAlignments() { + if (this.columnAlignments == null) { + return null; + } + String[] alignments = new String[this.visibleColumns.size()]; + int i = 0; + for (Iterator it = this.visibleColumns.iterator(); it.hasNext(); i++) { + alignments[i++] = getColumnAlignment(it.next()); + } + + return alignments; + } + + /** + * Set 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 + * Column alignments array. + */ + public void setColumnAlignments(String[] columnAlignments) { + + if (columnAlignments.length != this.visibleColumns.size()) + throw new IllegalArgumentException( + "The length of the alignments array must match the number of visible columns"); + + // Check all alignments + for (int i = 0; i < columnAlignments.length; i++) { + 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"); + } + + // Reset alignments + HashMap newCA = new HashMap(); + int i = 0; + for (Iterator it = this.visibleColumns.iterator(); it.hasNext() + && i < columnAlignments.length; i++) { + newCA.put(it.next(), columnAlignments[i]); + } + this.columnAlignments = newCA; + + // Assure visual refresh + refreshCurrentPage(); + } + + /** + * Get the page length. + * + * <p> + * Setting page length 0 disables paging. + * </p> + * + * @return Lenght of one page. + */ + public int getPageLength() { + return this.pageLength; + } + + /** + * Set the page length. + * + * <p> + * Setting page length 0 disables paging. The page length defaults to 0 (no + * paging). + * </p> + * + * @param Lenght + * of one page. + */ + public void setPageLength(int pageLength) { + if (pageLength >= 0 && this.pageLength != pageLength) { + this.pageLength = pageLength; + + // Assure visual refresh + refreshCurrentPage(); + } + } + + /** + * Getter for property currentPageFirstItem. + * + * @return Value of property currentPageFirstItem. + */ + public Object getCurrentPageFirstItemId() { + + // Priorise index over id if indexes are supported + if (items instanceof Container.Indexed) { + 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 currentPageFirstItem. + * + * @param currentPageFirstItem + * New value of property currentPageFirstItem. + */ + public void setCurrentPageFirstItemId(Object currentPageFirstItemId) { + + // Get the corresponding index + int index = -1; + try { + index = ((Container.Indexed) items) + .indexOfId(currentPageFirstItemId); + } catch (ClassCastException e) { + + // If the table item container does not have index, we have to + // calculate 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 successfull + if (index >= 0) { + this.currentPageFirstItemId = currentPageFirstItemId; + this.currentPageFirstItemIndex = index; + } + + // Assure visual refresh + refreshCurrentPage(); + + } + + /** + * 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 (Resource) this.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) + this.columnIcons.remove(propertyId); + else + this.columnIcons.put(propertyId, icon); + + // Assure visual refresh + refreshCurrentPage(); + } + + /** + * 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 = (String) this.columnHeaders.get(propertyId); + if ((header == null && this.getColumnHeaderMode() == COLUMN_HEADER_MODE_EXPLICIT_DEFAULTS_ID) + || this.getColumnHeaderMode() == COLUMN_HEADER_MODE_ID) { + header = propertyId.toString(); + } + + return header; + } + + /** + * Sets 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) { + this.columnHeaders.remove(propertyId); + return; + } + this.columnHeaders.put(propertyId, header); + + // Assure visual refresh + refreshCurrentPage(); + } + + /** + * 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) { + String a = (String) this.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) { + + // Check 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)) { + this.columnAlignments.remove(propertyId); + return; + } + + this.columnAlignments.put(propertyId, alignment); + + // Assure visual refresh + refreshCurrentPage(); + } + + /** + * 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. + */ + public void setColumnCollapsed(Object propertyId, boolean collapsed) + throws IllegalAccessException { + if (!this.isColumnCollapsingAllowed()) { + throw new IllegalAccessException("Column collapsing not allowed!"); + } + + if (collapsed) + this.collapsedColumns.add(propertyId); + else + this.collapsedColumns.remove(propertyId); + + // Assure visual refresh + refreshCurrentPage(); + } + + /** + * Check if column collapsing is allowed. + * + * @return true if columns can be collapsed; false otherwise. + */ + public boolean isColumnCollapsingAllowed() { + return this.columnCollapsingAllowed; + } + + /** + * Sets whether column collapsing is allowed or not. + * + * @param collapsingAllowed + * specifies whether column collapsing is allowed. + */ + public void setColumnCollapsingAllowed(boolean collapsingAllowed) { + this.columnCollapsingAllowed = collapsingAllowed; + + if (!collapsingAllowed) + collapsedColumns.clear(); + + // Assure visual refresh + refreshCurrentPage(); + } + + /** + * Check if column reordering is allowed. + * + * @return true if columns can be reordered; false otherwise. + */ + public boolean isColumnReorderingAllowed() { + return this.columnReorderingAllowed; + } + + /** + * Sets whether column reordering is allowed or not. + * + * @param reorderingAllowed + * specifies whether column reordering is allowed. + */ + public void setColumnReorderingAllowed(boolean reorderingAllowed) { + this.columnReorderingAllowed = reorderingAllowed; + + // Assure visual refresh + refreshCurrentPage(); + } + + /* + * 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 || !this.isColumnReorderingAllowed()) { + return; + } + LinkedList newOrder = new LinkedList(); + for (int i = 0; i < columnOrder.length; i++) { + if (columnOrder[i] != null + && this.visibleColumns.contains(columnOrder[i])) { + this.visibleColumns.remove(columnOrder[i]); + newOrder.add(columnOrder[i]); + } + } + for (Iterator it = this.visibleColumns.iterator(); it.hasNext();) { + Object columnId = it.next(); + if (!newOrder.contains(columnId)) + newOrder.add(columnId); + } + this.visibleColumns = newOrder; + + // Assure visual refresh + refreshCurrentPage(); + } + + /** + * Getter for property currentPageFirstItem. + * + * @return Value of property currentPageFirstItem. + */ + public int getCurrentPageFirstItemIndex() { + return this.currentPageFirstItemIndex; + } + + /** + * Setter for property currentPageFirstItem. + * + * @param newIndex + * New value of property currentPageFirstItem. + */ + public void setCurrentPageFirstItemIndex(int newIndex) { + + // Ensure that the new value is valid + if (newIndex < 0) + newIndex = 0; + if (newIndex >= size()) + newIndex = size() - 1; + + // Refresh first item id + if (items instanceof Container.Indexed) { + try { + currentPageFirstItemId = ((Container.Indexed) items) + .getIdByIndex(newIndex); + } catch (IndexOutOfBoundsException e) { + currentPageFirstItemId = null; + } + this.currentPageFirstItemIndex = newIndex; + } else { + + // For containers not supporting indexes, we must iterate the + // container forwards / backwards + // next available item forward or backward + + this.currentPageFirstItemId = ((Container.Ordered) items).firstItemId(); + + // Go forwards in the middle of the list (respect borders) + while (this.currentPageFirstItemIndex < newIndex + && !((Container.Ordered) items) + .isLastId(currentPageFirstItemId)) { + this.currentPageFirstItemIndex++; + currentPageFirstItemId = ((Container.Ordered) items) + .nextItemId(currentPageFirstItemId); + } + + // If we did hit the border + if (((Container.Ordered) items).isLastId(currentPageFirstItemId)) { + this.currentPageFirstItemIndex = size() - 1; + } + + // Go backwards in the middle of the list (respect borders) + while (this.currentPageFirstItemIndex > newIndex + && !((Container.Ordered) items) + .isFirstId(currentPageFirstItemId)) { + this.currentPageFirstItemIndex--; + currentPageFirstItemId = ((Container.Ordered) items) + .prevItemId(currentPageFirstItemId); + } + + // If we did hit the border + if (((Container.Ordered) items).isFirstId(currentPageFirstItemId)) { + this.currentPageFirstItemIndex = 0; + } + + // Go forwards once more + while (this.currentPageFirstItemIndex < newIndex + && !((Container.Ordered) items) + .isLastId(currentPageFirstItemId)) { + this.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 = this.currentPageFirstItemIndex = size() - 1; + } + } + + // Assure visual refresh + refreshCurrentPage(); + } + + /** + * Getter for property pageBuffering. + * + * @return Value of property pageBuffering. + */ + public boolean isPageBufferingEnabled() { + return this.pageBuffering; + } + + /** + * Setter for property pageBuffering. + * + * @param pageBuffering + * New value of property pageBuffering. + */ + public void setPageBufferingEnabled(boolean pageBuffering) { + + this.pageBuffering = pageBuffering; + + // If page buffering is disabled, clear the buffer + if (!pageBuffering) + pageBuffer = null; + } + + /** + * Getter for property selectable. + * + * <p> + * The table is not selectable by default. + * </p> + * + * @return Value of property selectable. + */ + public boolean isSelectable() { + return this.selectable; + } + + /** + * Setter for property selectable. + * + * <p> + * The table is not selectable by default. + * </p> + * + * @param selectable + * New value of property selectable. + */ + public void setSelectable(boolean selectable) { + if (this.selectable != selectable) { + this.selectable = selectable; + requestRepaint(); + } + } + + /** + * Getter for property columnHeaderMode. + * + * @return Value of property columnHeaderMode. + */ + public int getColumnHeaderMode() { + return this.columnHeaderMode; + } + + /** + * Setter for property columnHeaderMode. + * + * @param columnHeaderMode + * 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; + + // Assure visual refresh + refreshCurrentPage(); + } + + /** + * Refresh the current page contents. If the page buffering is turned off, + * it is not necessary to call this explicitely. + */ + public void refreshCurrentPage() { + + // Clear page buffer and notify about the change + pageBuffer = null; + requestRepaint(); + } + + /** + * Set 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 + * 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 + refreshCurrentPage(); + } + + /** + * Get the row header mode. + * + * @return Row header mode. + * @see #setRowHeaderMode(int) + */ + public int getRowHeaderMode() { + return rowCaptionsAreHidden ? ROW_HEADER_MODE_HIDDEN + : getItemCaptionMode(); + } + + /** + * Add new row to table and fill the visible cells with given values. + * + * @param cells + * Object array that is used for filling the visible cells new + * row. The types must be settable to visible column property + * types. + * @param itemId + * 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 { + + Object[] cols = getVisibleColumns(); + + // Check that a correct number of cells are given + if (cells.length != cols.length) + return null; + + // Create 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; + + // Fill the item properties + for (int i = 0; i < cols.length; i++) + item.getItemProperty(cols[i]).setValue(cells[i]); + + return itemId; + } + + /* Overriding select behavior******************************************** */ + + /** + * @see com.itmill.toolkit.data.Container.Viewer#setContainerDataSource(Container) + */ + public void setContainerDataSource(Container newDataSource) { + + if (newDataSource == null) + newDataSource = new IndexedContainer(); + + // Assure 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)); + + // Reset page position + currentPageFirstItemId = null; + currentPageFirstItemIndex = 0; + + // Reset column properties + if (this.collapsedColumns != null) + this.collapsedColumns.clear(); + setVisibleColumns(getContainerPropertyIds().toArray()); + + // Assure visual refresh + refreshCurrentPage(); + } + + /* Component basics ***************************************************** */ + + /** + * Invoked when the value of a variable has changed. + * + * @param event + * Variable change event containing the information about the + * changed variable. + */ + public void changeVariables(Object source, Map variables) { + + super.changeVariables(source, variables); + + // Page start index + if (variables.containsKey("firstvisible")) { + Integer value = (Integer) variables.get("firstvisible"); + if (value != null) + setCurrentPageFirstItemIndex(value.intValue() - 1); + } + + // Actions + if (variables.containsKey("action")) { + StringTokenizer st = new StringTokenizer((String) variables + .get("action"), ","); + if (st.countTokens() == 2) { + Object itemId = itemIdMapper.get(st.nextToken()); + Action action = (Action) actionMapper.get(st.nextToken()); + if (action != null && containsId(itemId) + && actionHandlers != null) + for (Iterator i = actionHandlers.iterator(); i.hasNext();) + ((Action.Handler) i.next()).handleAction(action, this, + itemId); + } + } + + // Sorting + boolean doSort = false; + if (variables.containsKey("sortcolumn")) { + String colId = (String) variables.get("sortcolumn"); + if (colId != null && !"".equals(colId) && !"null".equals(colId)) { + Object id = this.columnIdMap.get(colId); + setSortContainerPropertyId(id); + doSort = true; + } + } + if (variables.containsKey("sortascending")) { + boolean state = ((Boolean) variables.get("sortascending")) + .booleanValue(); + if (state != this.sortAscending) { + setSortAscending(state); + doSort = true; + } + } + if (doSort) + this.sort(); + + // Dynamic column hide/show and order + // Update visible columns + if (this.isColumnCollapsingAllowed()) { + if (variables.containsKey("collapsedcolumns")) { + try { + Object[] ids = (Object[]) variables.get("collapsedcolumns"); + for (Iterator it = this.visibleColumns.iterator(); it + .hasNext();) { + this.setColumnCollapsed(it.next(), false); + } + for (int i = 0; i < ids.length; i++) { + this.setColumnCollapsed(columnIdMap.get(ids[i] + .toString()), true); + } + } catch (Exception ignored) { + } + } + } + if (this.isColumnReorderingAllowed()) { + if (variables.containsKey("columnorder")) { + try { + Object[] ids = (Object[]) variables.get("columnorder"); + for (int i = 0; i < ids.length; i++) { + ids[i] = columnIdMap.get(ids[i].toString()); + } + this.setColumnOrder(ids); + } catch (Exception ignored) { + } + } + } + } + + /** + * Paint the content of this component. + * + * @param target + * Paint target. + * @throws PaintException + * The paint operation failed. + */ + public void paintContent(PaintTarget target) throws PaintException { + + // Focus control id + if (this.getFocusableId() > 0) { + target.addAttribute("focusid", this.getFocusableId()); + } + + // The tab ordering number + if (this.getTabIndex() > 0) + target.addAttribute("tabindex", this.getTabIndex()); + + // Initialize temps + Object[] colids = getVisibleColumns(); + int cols = colids.length; + int first = getCurrentPageFirstItemIndex(); + int total = size(); + int pagelen = getPageLength(); + int colHeadMode = getColumnHeaderMode(); + boolean colheads = colHeadMode != COLUMN_HEADER_MODE_HIDDEN; + boolean rowheads = getRowHeaderMode() != ROW_HEADER_MODE_HIDDEN; + Object[][] cells = getVisibleCells(); + boolean iseditable = this.isEditable(); + + // selection support + String[] selectedKeys; + if (isMultiSelect()) + selectedKeys = new String[((Set) getValue()).size()]; + else + selectedKeys = new String[(getValue() == null + && getNullSelectionItemId() == null ? 0 : 1)]; + int keyIndex = 0; + + // Table attributes + if (isSelectable()) + target.addAttribute("selectmode", (isMultiSelect() ? "multi" + : "single")); + else + target.addAttribute("selectmode", "none"); + target.addAttribute("cols", cols); + target.addAttribute("rows", cells[0].length); + target.addAttribute("totalrows", total); + if (pagelen != 0) + target.addAttribute("pagelength", pagelen); + if (colheads) + target.addAttribute("colheaders", true); + if (rowheads) + target.addAttribute("rowheaders", true); + + // Columns + target.startTag("cols"); + Collection sortables = getSortableContainerPropertyIds(); + for (Iterator it = this.visibleColumns.iterator(); it.hasNext();) { + Object columnId = it.next(); + if (!isColumnCollapsed(columnId)) { + target.startTag("ch"); + if (colheads) { + if (this.getColumnIcon(columnId) != null) + target.addAttribute("icon", this + .getColumnIcon(columnId)); + if (sortables.contains(columnId)) + target.addAttribute("sortable", true); + String header = (String) this.getColumnHeader(columnId); + target.addAttribute("caption", (header != null ? header + : "")); + } + target.addAttribute("cid", this.columnIdMap.key(columnId)); + if (!ALIGN_LEFT.equals(this.getColumnAlignment(columnId))) + target.addAttribute("align", this + .getColumnAlignment(columnId)); + target.endTag("ch"); + } + } + target.endTag("cols"); + + // Rows + Set actionSet = new LinkedHashSet(); + boolean selectable = isSelectable(); + boolean[] iscomponent = new boolean[this.visibleColumns.size()]; + int iscomponentIndex = 0; + for (Iterator it = this.visibleColumns.iterator(); it.hasNext() + && iscomponentIndex < iscomponent.length;) { + Object columnId = it.next(); + Class colType = getType(columnId); + iscomponent[iscomponentIndex++] = colType != null + && Component.class.isAssignableFrom(colType); + } + target.startTag("rows"); + for (int i = 0; i < cells[0].length; i++) { + target.startTag("tr"); + Object itemId = cells[CELL_ITEMID][i]; + + // 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]); + } + if (actionHandlers != null || isSelectable()) { + target.addAttribute("key", (String) cells[CELL_KEY][i]); + if (isSelected(itemId) && keyIndex < selectedKeys.length) { + target.addAttribute("selected", true); + selectedKeys[keyIndex++] = (String) cells[CELL_KEY][i]; + } + } + + // Actions + if (actionHandlers != null) { + target.startTag("al"); + for (Iterator ahi = actionHandlers.iterator(); ahi.hasNext();) { + Action[] aa = ((Action.Handler) ahi.next()).getActions( + itemId, this); + if (aa != null) + for (int ai = 0; ai < aa.length; ai++) { + String key = actionMapper.key(aa[ai]); + actionSet.add(aa[ai]); + target.addSection("ak", key); + } + } + target.endTag("al"); + } + + // cells + int currentColumn = 0; + for (Iterator it = this.visibleColumns.iterator(); it.hasNext(); currentColumn++) { + Object columnId = it.next(); + if (columnId == null || this.isColumnCollapsed(columnId)) + continue; + if ((iscomponent[currentColumn] || iseditable) + && Component.class.isInstance(cells[CELL_FIRSTCOL + + currentColumn][i])) { + Component c = (Component) cells[CELL_FIRSTCOL + + currentColumn][i]; + if (c == null) + target.addSection("label", ""); + else + c.paint(target); + } else + target.addSection("label", (String) cells[CELL_FIRSTCOL + + currentColumn][i]); + } + + target.endTag("tr"); + } + target.endTag("rows"); + + // The select variable is only enabled if selectable + if (selectable) + target.addVariable(this, "selected", selectedKeys); + + // The cursors are only shown on pageable table + if (first != 0 || getPageLength() > 0) + target.addVariable(this, "firstvisible", first + 1); + + // Sorting + if (getContainerDataSource() instanceof Container.Sortable) { + target.addVariable(this, "sortcolumn", this.columnIdMap.key(this.sortContainerPropertyId)); + target.addVariable(this, "sortascending", this.sortAscending); + } + + // Actions + if (!actionSet.isEmpty()) { + target.startTag("actions"); + target.addVariable(this, "action", ""); + for (Iterator it = actionSet.iterator(); it.hasNext();) { + Action a = (Action) 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 (this.columnReorderingAllowed) { + String[] colorder = new String[this.visibleColumns.size()]; + int i = 0; + for (Iterator it = this.visibleColumns.iterator(); it.hasNext() + && i < colorder.length;) { + colorder[i++] = this.columnIdMap.key(it.next()); + } + target.addVariable(this, "columnorder", colorder); + } + // Available columns + if (this.columnCollapsingAllowed) { + HashSet ccs = new HashSet(); + for (Iterator i = visibleColumns.iterator(); i.hasNext();) { + Object o = i.next(); + if (isColumnCollapsed(o)) + ccs.add(o); + } + String[] collapsedkeys = new String[ccs.size()]; + int nextColumn = 0; + for (Iterator it = this.visibleColumns.iterator(); it.hasNext() + && nextColumn < collapsedkeys.length;) { + Object columnId = it.next(); + if (this.isColumnCollapsed(columnId)) { + collapsedkeys[nextColumn++] = this.columnIdMap + .key(columnId); + } + } + target.addVariable(this, "collapsedcolumns", collapsedkeys); + target.startTag("visiblecolumns"); + int i = 0; + for (Iterator it = this.visibleColumns.iterator(); it.hasNext(); i++) { + Object columnId = it.next(); + if (columnId != null) { + target.startTag("column"); + target.addAttribute("cid", this.columnIdMap.key(columnId)); + String head = getColumnHeader(columnId); + target.addAttribute("caption", (head != null ? head : "")); + if (this.isColumnCollapsed(columnId)) { + target.addAttribute("collapsed", true); + } + target.endTag("column"); + } + } + target.endTag("visiblecolumns"); + } + } + + /** + * Get UIDL tag corresponding to component. + * + * @return UIDL tag as string. + */ + public String getTag() { + return "table"; + } + + /** Return cached visible table contents */ + private Object[][] getVisibleCells() { + + // Return a buffered value if possible + if (pageBuffer != null && isPageBufferingEnabled()) + return pageBuffer; + + // Stop listening the old properties and initialise the list + if (listenedProperties == null) + listenedProperties = new LinkedList(); + else + for (Iterator i = listenedProperties.iterator(); i.hasNext();) { + ((Property.ValueChangeNotifier) i.next()).removeListener(this); + } + + // Detach old visible component from the table + if (visibleComponents == null) + visibleComponents = new LinkedList(); + else + for (Iterator i = visibleComponents.iterator(); i.hasNext();) { + ((Component) i.next()).setParent(null); + } + + // Collect basic facts about the table page + Object[] colids = getVisibleColumns(); + int cols = colids.length; + int pagelen = getPageLength(); + int firstIndex = getCurrentPageFirstItemIndex(); + int rows = size(); + if (rows > 0 && firstIndex >= 0) + rows -= firstIndex; + + if (pagelen > 0 && pagelen < rows) + rows = pagelen; + Object[][] cells = new Object[cols + CELL_FIRSTCOL][rows]; + if (rows == 0) + return cells; + Object id = getCurrentPageFirstItemId(); + int headmode = getRowHeaderMode(); + boolean[] iscomponent = new boolean[cols]; + for (int i = 0; i < cols; i++) + iscomponent[i] = Component.class + .isAssignableFrom(getType(colids[i])); + + // Create 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++) { + Object value = null; + Property p = getContainerProperty(id, colids[j]); + if (p != null) { + if (p instanceof Property.ValueChangeNotifier) { + ((Property.ValueChangeNotifier) p) + .addListener(this); + listenedProperties.add(p); + } + if (iscomponent[j]) { + value = p.getValue(); + } else if (p != null) { + value = getPropertyValue(id, colids[j], p); + } else { + value = getPropertyValue(id, colids[j], null); + } + } else { + value = ""; + } + + if (value instanceof Component) { + ((Component) value).setParent(this); + visibleComponents.add((Component) value); + } + cells[CELL_FIRSTCOL + j][i] = value; + + } + } + id = ((Container.Ordered) items).nextItemId(id); + + filledRows++; + } + + // Assure that all the rows of the cell-buffer are valid + if (filledRows != cells[0].length) { + 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; + } + + // Save the results to internal buffer iff in buffering mode + // to possible conserve memory from large non-buffered pages + if (isPageBufferingEnabled()) + pageBuffer = cells; + + return cells; + } + + /** + * Get 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. + * + * @see #setFieldFactory(FieldFactory) + * @param rowId + * Id of the row (same as item Id) + * @param colId + * Id of the column + * @param property + * Property to be presented + * @return Object Either formatted value or Component for field. + */ + protected Object getPropertyValue(Object rowId, Object colId, + Property property) { + if (this.isEditable() && this.fieldFactory != null) { + Field f = this.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 itemId + * @param property + * Property to be formatted + * @return 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 *************************************************** */ + + /** + * @see com.itmill.toolkit.event.Action.Container#addActionHandler(Action.Handler) + */ + public void addActionHandler(Action.Handler actionHandler) { + + if (actionHandler != null) { + + if (actionHandlers == null) { + actionHandlers = new LinkedList(); + actionMapper = new KeyMapper(); + } + + if(!actionHandlers.contains(actionHandler)){ + actionHandlers.add(actionHandler); + requestRepaint(); + } + + } + } + + /** + * @see com.itmill.toolkit.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 **************************** */ + + /** + * @see com.itmill.toolkit.data.Property.ValueChangeListener#valueChange(Property.ValueChangeEvent) + */ + public void valueChange(Property.ValueChangeEvent event) { + super.valueChange(event); + requestRepaint(); + } + + /** + * @see com.itmill.toolkit.ui.Component#attach() + */ + public void attach() { + super.attach(); + + if (visibleComponents != null) + for (Iterator i = visibleComponents.iterator(); i.hasNext();) + ((Component) i.next()).attach(); + } + + /** + * @see com.itmill.toolkit.ui.Component#attach() + */ + public void detach() { + super.detach(); + + if (visibleComponents != null) + for (Iterator i = visibleComponents.iterator(); i.hasNext();) + ((Component) i.next()).detach(); + } + + /** + * @see com.itmill.toolkit.data.Container#removeAllItems() + */ + public boolean removeAllItems() { + this.currentPageFirstItemId = null; + this.currentPageFirstItemIndex = 0; + return super.removeAllItems(); + } + + /** + * @see com.itmill.toolkit.data.Container#removeItem(Object) + */ + public boolean removeItem(Object itemId) { + Object nextItemId = ((Container.Ordered) items).nextItemId(itemId); + boolean ret = super.removeItem(itemId); + if (ret && (itemId != null) + && (itemId.equals(this.currentPageFirstItemId))) { + this.currentPageFirstItemId = nextItemId; + } + return ret; + } + + /** + * @see com.itmill.toolkit.data.Container#removeContainerProperty(Object) + */ + public boolean removeContainerProperty(Object propertyId) + throws UnsupportedOperationException { + + // If a visible property is removed, remove the corresponding column + this.visibleColumns.remove(propertyId); + this.columnAlignments.remove(propertyId); + this.columnIcons.remove(propertyId); + this.columnHeaders.remove(propertyId); + + return super.removeContainerProperty(propertyId); + } + + /** + * Adds a new property to the table and show it as a visible column. + * + * @see com.itmill.toolkit.data.Container#addContainerProperty(Object, + * Class, Object) + * + * @param propertyId + * Id of the proprty + * @param type + * The class of the property + * @param defaultValue + * The default value given for all existing items + */ + public boolean addContainerProperty(Object propertyId, Class type, + Object defaultValue) throws UnsupportedOperationException { + if (!super.addContainerProperty(propertyId, type, defaultValue)) + return false; + if (!this.visibleColumns.contains(propertyId)) + this.visibleColumns.add(propertyId); + return true; + } + + /** + * Adds a new property to the table and show it as a visible column. + * + * @see com.itmill.toolkit.data.Container#addContainerProperty(Object, + * Class, Object) + * + * @param propertyId + * Id of the proprty + * @param type + * The class of the property + * @param defaultValue + * The default value given for all existing items + * @param columnHeader + * Explicit header of the column. If explicit header is not + * needed, this should be set null. + * @param columnIcon + * Icon of the column. If icon is not needed, this should be set + * null. + * @param columnAlignment + * Alignment of the column. Null implies align left. + */ + 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; + this.setColumnAlignment(propertyId, columnAlignment); + this.setColumnHeader(propertyId, columnHeader); + this.setColumnIcon(propertyId, columnIcon); + return true; + } + + /** + * Return list of items on the current page + * + * @see com.itmill.toolkit.ui.Select#getVisibleItemIds() + */ + public Collection getVisibleItemIds() { + + LinkedList visible = new LinkedList(); + + 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.itmill.toolkit.data.Container.ItemSetChangeListener#containerItemSetChange(com.itmill.toolkit.data.Container.ItemSetChangeEvent) + */ + public void containerItemSetChange(Container.ItemSetChangeEvent event) { + pageBuffer = null; + super.containerItemSetChange(event); + setCurrentPageFirstItemIndex(this.getCurrentPageFirstItemIndex()); + } + + /** + * Container datasource property set change. Table must flush its buffers on + * change. + * + * @see com.itmill.toolkit.data.Container.PropertySetChangeListener#containerPropertySetChange(com.itmill.toolkit.data.Container.PropertySetChangeEvent) + */ + public void containerPropertySetChange( + Container.PropertySetChangeEvent event) { + pageBuffer = null; + super.containerPropertySetChange(event); + } + + /** + * Adding new items is not supported. + * + * @see com.itmill.toolkit.ui.Select#setNewItemsAllowed(boolean) + * @throws UnsupportedOperationException + * if set to true. + */ + public void setNewItemsAllowed(boolean allowNewOptions) + throws UnsupportedOperationException { + if (allowNewOptions) + throw new UnsupportedOperationException(); + } + + /** + * Focusing to this component is not supported. + * + * @see com.itmill.toolkit.ui.AbstractField#focus() + * @throws UnsupportedOperationException + * if invoked. + */ + public void focus() throws UnsupportedOperationException { + throw new UnsupportedOperationException(); + } + + /** + * @see com.itmill.toolkit.data.Container.Ordered#nextItemId(java.lang.Object) + */ + public Object nextItemId(Object itemId) { + return ((Container.Ordered) items).nextItemId(itemId); + } + + /** + * @see com.itmill.toolkit.data.Container.Ordered#prevItemId(java.lang.Object) + */ + public Object prevItemId(Object itemId) { + return ((Container.Ordered) items).prevItemId(itemId); + } + + /** + * @see com.itmill.toolkit.data.Container.Ordered#firstItemId() + */ + public Object firstItemId() { + return ((Container.Ordered) items).firstItemId(); + } + + /** + * @see com.itmill.toolkit.data.Container.Ordered#lastItemId() + */ + public Object lastItemId() { + return ((Container.Ordered) items).lastItemId(); + } + + /** + * @see com.itmill.toolkit.data.Container.Ordered#isFirstId(java.lang.Object) + */ + public boolean isFirstId(Object itemId) { + return ((Container.Ordered) items).isFirstId(itemId); + } + + /** + * @see com.itmill.toolkit.data.Container.Ordered#isLastId(java.lang.Object) + */ + public boolean isLastId(Object itemId) { + return ((Container.Ordered) items).isLastId(itemId); + } + + /** + * @see com.itmill.toolkit.data.Container.Ordered#addItemAfter(java.lang.Object) + */ + public Object addItemAfter(Object previousItemId) + throws UnsupportedOperationException { + return ((Container.Ordered) items).addItemAfter(previousItemId); + } + + /** + * @see com.itmill.toolkit.data.Container.Ordered#addItemAfter(java.lang.Object, + * java.lang.Object) + */ + public Item addItemAfter(Object previousItemId, Object newItemId) + throws UnsupportedOperationException { + return ((Container.Ordered) items).addItemAfter(previousItemId, + newItemId); + } + + /** + * Get the FieldFactory that is used to create editor for table cells. + * + * The FieldFactory is only used if the Table is editable. + * + * @see #isEditable + * @return FieldFactory used to create the Field instances. + */ + public FieldFactory getFieldFactory() { + return fieldFactory; + } + + /** + * Set 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. + * + * @see #isEditable + * @see BaseFieldFactory + * @param fieldFactory + * The field factory to set + */ + public void setFieldFactory(FieldFactory fieldFactory) { + this.fieldFactory = fieldFactory; + + // Assure visual refresh + refreshCurrentPage(); + } + + /** + * 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. + * + * @see Field + * @see FieldFactory + * @return true if table is editable, false oterwise. + */ + public boolean isEditable() { + return editable; + } + + /** + * Set 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. + * + * @see Field + * @see FieldFactory + * @param editable + * true if table should be editable by user. + */ + public void setEditable(boolean editable) { + this.editable = editable; + + // Assure visual refresh + refreshCurrentPage(); + } + + /** + * Sort table. + * + * @see com.itmill.toolkit.data.Container.Sortable#sort(java.lang.Object[], + * boolean[]) + * + * @throws UnsupportedOperationException + * if the container data source does not implement + * Container.Sortable + */ + public void sort(Object[] propertyId, boolean[] ascending) + throws UnsupportedOperationException { + Container c = getContainerDataSource(); + if (c instanceof Container.Sortable) { + int pageIndex = this.getCurrentPageFirstItemIndex(); + ((Container.Sortable) c).sort(propertyId, ascending); + setCurrentPageFirstItemIndex(pageIndex); + } else if (c != null) { + throw new UnsupportedOperationException( + "Underlying Data does not allow sorting"); + } + } + + /** + * Sort 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[] { this.sortContainerPropertyId }, + new boolean[] { this.sortAscending }); + } + + /* + * (non-Javadoc) + * + * @see com.itmill.toolkit.data.Container.Sortable#getSortableContainerPropertyIds() + */ + public Collection getSortableContainerPropertyIds() { + Container c = getContainerDataSource(); + if (c instanceof Container.Sortable && !isSortDisabled()) { + return ((Container.Sortable) c).getSortableContainerPropertyIds(); + } else { + return new LinkedList(); + } + } + + /** + * Get the currently sorted column property ID. + * + * @return Container property id of the currently sorted column. + */ + public Object getSortContainerPropertyId() { + return this.sortContainerPropertyId; + } + + /** + * Set the currently sorted column property id. + * + * @param propertyId + * Container property id of the currently sorted column. + */ + public void setSortContainerPropertyId(Object propertyId) { + if ((this.sortContainerPropertyId != null && !this.sortContainerPropertyId + .equals(propertyId)) + || (this.sortContainerPropertyId == null && propertyId != null)) { + this.sortContainerPropertyId = propertyId; + sort(); + } + + // Assure visual refresh + refreshCurrentPage(); + } + + /** + * Is the table currently sorted in ascending order. + * + * @return <code>true</code> if ascending, <code>false</code> if + * descending + */ + public boolean isSortAscending() { + return this.sortAscending; + } + + /** + * Set the table in ascending order. + * + * @param ascending + * <code>true</code> if ascending, <code>false</code> if + * descending + */ + public void setSortAscending(boolean ascending) { + if (this.sortAscending != ascending) { + this.sortAscending = ascending; + sort(); + } + + // Assure visual refresh + refreshCurrentPage(); + } + + /** Is sorting disabled alltogether. + * + * True iff no sortable columns are given even in the case where datasource would support this. + * + * @return True iff sorting is disabled. + */ + public boolean isSortDisabled() { + return sortDisabled; + } + + + /** Disable sorting alltogether. + * + * To disable sorting alltogether, 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; + refreshCurrentPage(); + } + } +}
\ No newline at end of file diff --git a/src/com/itmill/toolkit/ui/TextField.java b/src/com/itmill/toolkit/ui/TextField.java new file mode 100644 index 0000000000..86e3f14d50 --- /dev/null +++ b/src/com/itmill/toolkit/ui/TextField.java @@ -0,0 +1,394 @@ +/* ************************************************************************* + + IT Mill Toolkit + + Development of Browser User Intarfaces Made Easy + + Copyright (C) 2000-2006 IT Mill Ltd + + ************************************************************************* + + This product is distributed under commercial license that can be found + from the product package on license/license.txt. Use of this product might + require purchasing a commercial license from IT Mill Ltd. For guidelines + on usage, see license/licensing-guidelines.html + + ************************************************************************* + + For more information, contact: + + IT Mill Ltd phone: +358 2 4802 7180 + Ruukinkatu 2-4 fax: +358 2 4802 7181 + 20540, Turku email: info@itmill.com + Finland company www: www.itmill.com + + Primary source for information and releases: www.itmill.com + + ********************************************************************** */ + +package com.itmill.toolkit.ui; + +import java.text.Format; +import java.util.Map; + +import com.itmill.toolkit.data.Property; +import com.itmill.toolkit.terminal.PaintException; +import com.itmill.toolkit.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.itmill.toolkit.data.Buffered} interface. A + * <code>TextField</code> is in write-through mode by default, so + * {@link com.itmill.toolkit.ui.AbstractField#setWriteThrough(boolean)} + * must be called to enable buffering.</p> + * + * @author IT Mill Ltd. + * @version @VERSION@ + * @since 3.0 + */ +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; + + /* Constructors **************************************************** */ + + /** Constructs an empty <code>TextField</code> with no caption. */ + public TextField() { + setValue(""); + } + + /** Constructs an empty <code>TextField</code> with given caption. */ + 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 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.itmill.toolkit.data.Property.Viewer#setPropertyDataSource(Property)} + * is called to bind it. + * + * @param caption caption <code>String</code> for the editor + * @param text initial text content of the editor + */ + public TextField(String caption, String value) { + setValue(value); + setCaption(caption); + } + + /* Component basic features ********************************************* */ + + /* Paint this component. + * Don't add a JavaDoc comment here, we use the default documentation + * from implemented interface. + */ + public void paintContent(PaintTarget target) throws PaintException { + super.paintContent(target); + + // Set secret attribute + if (this.isSecret()) + target.addAttribute("secret", true); + + // Add the number of column and rows + int c = getColumns(); + 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); + } + + // Add 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); + } + + /** Get the formatted dtring value. + * Sets the field value by using the assigned Format. + * @param value to be formatted + * @return Formatted value + * @see #setFormat(Format) + * @see Format + */ + protected String getFormattedValue() { + Object value = getValue(); + if (this.format != null && value != null) + try { + return this.format.format(value); + } catch (IllegalArgumentException ignored) { + // Ignored exception + } + if (value != null) + return value.toString(); + return null; + } + + /* Gets the components UIDL tag string. + * Don't add a JavaDoc comment here, we use the default documentation + * from implemented interface. + */ + 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. + */ + public void changeVariables(Object source, Map variables) { + + // Set 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"); + String oldValue = getFormattedValue(); + if (newValue != null + && (oldValue == null || isNullSettingAllowed()) + && newValue.equals(getNullRepresentation())) + newValue = null; + if (newValue != oldValue + && (newValue == null || !newValue.equals(oldValue))) + setValue(newValue); + } + + } + + /* 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. + * + * @param the number of columns for this editor + */ + public int getColumns() { + return this.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. + * + * @return number of explicitly set columns + */ + 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 this.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 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 this.wordwrap; + } + + /** Sets the editor's word-wrap mode on or off. + * + * @param wordwrap 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. + */ + public Class getType() { + return String.class; + } + /** Get the secret property on and off. + * If a field is used to enter secretinformation + * the information is not echoed to display. + * @return true if the field is used to enter secret information, false otherwise. + */ + public boolean isSecret() { + return secret; + } + + /** Set the secret property on and off. + * If a field is used to enter secretinformation + * the information is not echoed to display. + * @param secret value specifying if the field is used to enter secret information. + */ + public void setSecret(boolean secret) { + this.secret = secret; + } + + /** Get 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> + * + * @see TextField#isNullSettingAllowed() + * @return String Textual representation for null strings. + */ + 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 allways 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 allways + * 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> + * + * @see TextField#setNullSettingAllowed(boolean) + * @param nullRepresentation Textual representation for null strings. + */ + public void setNullRepresentation(String nullRepresentation) { + this.nullRepresentation = nullRepresentation; + } + + /** Set the null conversion mode. + * + * <p>If this property is true, writing null-representation string to text + * field allways 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 allways + * converted to null-values. + * @see TextField#getNullRepresentation() + */ + public void setNullSettingAllowed(boolean nullSettingAllowed) { + this.nullSettingAllowed = nullSettingAllowed; + } + + /** Get the value formatter of TextField. + * + * + * @return The Format used to format the value. + */ + public Format getFormat() { + return format; + } + + /** Get the value formatter of TextField. + * + * @param The Format used to format the value. Null disables the formatting. + */ + public void setFormat(Format format) { + this.format = format; + } + +} diff --git a/src/com/itmill/toolkit/ui/Tree.java b/src/com/itmill/toolkit/ui/Tree.java new file mode 100644 index 0000000000..b3371b1aa7 --- /dev/null +++ b/src/com/itmill/toolkit/ui/Tree.java @@ -0,0 +1,763 @@ +/* ************************************************************************* + + IT Mill Toolkit + + Development of Browser User Intarfaces Made Easy + + Copyright (C) 2000-2006 IT Mill Ltd + + ************************************************************************* + + This product is distributed under commercial license that can be found + from the product package on license/license.txt. Use of this product might + require purchasing a commercial license from IT Mill Ltd. For guidelines + on usage, see license/licensing-guidelines.html + + ************************************************************************* + + For more information, contact: + + IT Mill Ltd phone: +358 2 4802 7180 + Ruukinkatu 2-4 fax: +358 2 4802 7181 + 20540, Turku email: info@itmill.com + Finland company www: www.itmill.com + + Primary source for information and releases: www.itmill.com + + ********************************************************************** */ + +package com.itmill.toolkit.ui; + +import java.lang.reflect.Method; +import java.util.Collection; +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.itmill.toolkit.data.Container; +import com.itmill.toolkit.data.util.ContainerHierarchicalWrapper; +import com.itmill.toolkit.event.Action; +import com.itmill.toolkit.terminal.KeyMapper; +import com.itmill.toolkit.terminal.PaintException; +import com.itmill.toolkit.terminal.PaintTarget; +import com.itmill.toolkit.terminal.Resource; + +/** MenuTree component. + * MenuTree 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 + */ +public class Tree extends Select implements Container.Hierarchical, Action.Container { + + /* Static members ***************************************************** */ + + 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 (java.lang.NoSuchMethodException e) { + // This should never happen + e.printStackTrace(); + throw new java.lang.RuntimeException( + "Internal error, please report"); + } + } + + /* Private members **************************************************** */ + + /** Set of expanded nodes */ + private HashSet expanded = new HashSet(); + + /** List of action handlers */ + private LinkedList actionHandlers = null; + + /** Action mapper */ + private KeyMapper actionMapper = null; + + /** Is the tree selectable */ + private boolean selectable = true; + + /* Tree constructors ************************************************** */ + + /** Create new empty tree */ + public Tree() { + } + + /** Create new empty tree with caption. */ + public Tree(String caption) { + setCaption(caption); + } + + /** Create new tree with caption and connect it to a Container. */ + public Tree(String caption, Container dataSource) { + setCaption(caption); + setContainerDataSource(dataSource); + } + + /* Expanding and collapsing ******************************************* */ + + /** Check is an item is expanded + * @return true iff the item is expanded + */ + public boolean isExpanded(Object itemId) { + return expanded.contains(itemId); + } + + /** Expand an item. + * + * @return True iff the expand operation succeeded + */ + public boolean expandItem(Object itemId) { + + // 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; + + // Expand + expanded.add(itemId); + requestRepaint(); + fireExpandEvent(itemId); + + return true; + } + + /** Expand items recursively + * + * Expands all the children recursively starting from an item. + * Operation succeeds only if all expandable items are expanded. + * @return True iff the expand operation succeeded + */ + public boolean expandItemsRecursively(Object startItemId) { + + boolean result = true; + + // Initial stack + Stack todo = new Stack(); + todo.add(startItemId); + + // Expand recursively + while (!todo.isEmpty()) { + Object id = todo.pop(); + if (areChildrenAllowed(id) && !expandItem(id)) { + result = false; + } + if (hasChildren(id)) { + todo.addAll(getChildren(id)); + } + } + + return result; + } + + /** Collapse an item. + * + * @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; + } + + /** Collapse items recursively + * + * Collapse all the children recursively starting from an item. + * Operation succeeds only if all expandable items are collapsed. + * @return True iff the collapse operation succeeded + */ + public boolean collapseItemsRecursively(Object startItemId) { + + boolean result = true; + + // Initial stack + Stack todo = new Stack(); + todo.add(startItemId); + + // Collapse recursively + while (!todo.isEmpty()) { + 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 Value of property selectable. + */ + public boolean isSelectable() { + return this.selectable; + } + + /** Setter for property selectable. + * + * <p>The tree is selectable by default.</p> + * + * @param selectable New value of property selectable. + */ + public void setSelectable(boolean selectable) { + if (this.selectable != selectable) { + this.selectable = selectable; + requestRepaint(); + } + } + + /* Component API ****************************************************** */ + + /** + * @see com.itmill.toolkit.ui.AbstractComponent#getTag() + */ + public String getTag() { + return "tree"; + } + + /** + * @see com.itmill.toolkit.terminal.VariableOwner#changeVariables(Object source, Map variables) + */ + public void changeVariables(Object source, Map variables) { + + // Collapse nodes + if (variables.containsKey("collapse")) { + String[] keys = (String[]) variables.get("collapse"); + for (int i = 0; i < keys.length; i++) { + Object id = itemIdMapper.get(keys[i]); + if (id != null) + collapseItem(id); + } + } + + // Expand nodes + if (variables.containsKey("expand")) { + String[] keys = (String[]) variables.get("expand"); + for (int i = 0; i < keys.length; i++) { + Object id = itemIdMapper.get(keys[i]); + if (id != null) + expandItem(id); + } + } + + // Selections are handled by the select component + super.changeVariables(source, variables); + + // Actions + if (variables.containsKey("action")) { + + StringTokenizer st = + new StringTokenizer((String) variables.get("action"), ","); + if (st.countTokens() == 2) { + Object itemId = itemIdMapper.get(st.nextToken()); + Action action = (Action) actionMapper.get(st.nextToken()); + if (action != null + && containsId(itemId) + && actionHandlers != null) + for (Iterator i = actionHandlers.iterator(); + i.hasNext(); + ) + ((Action.Handler) i.next()).handleAction( + action, + this, + itemId); + } + } + } + + /** + * @see com.itmill.toolkit.ui.AbstractComponent#paintContent(PaintTarget) + */ + public void paintContent(PaintTarget target) throws PaintException { + + // Focus control id + if (this.getFocusableId() > 0) { + target.addAttribute("focusid", this.getFocusableId()); + } + + // The tab ordering number + if (this.getTabIndex() > 0) + target.addAttribute("tabindex", this.getTabIndex()); + + + // Paint tree attributes + if (isSelectable()) + target.addAttribute( + "selectmode", + (isMultiSelect() ? "multi" : "single")); + else + target.addAttribute("selectmode", "none"); + if (isNewItemsAllowed()) + target.addAttribute("allownewitem", true); + + // Initialize variables + Set actionSet = new LinkedHashSet(); + String[] selectedKeys; + if (isMultiSelect()) + selectedKeys = new String[((Set) getValue()).size()]; + else + selectedKeys = new String[(getValue() == null ? 0 : 1)]; + int keyIndex = 0; + LinkedList expandedKeys = new LinkedList(); + + // Iterate trough hierarchical tree using a stack of iterators + Stack iteratorStack = new Stack(); + Collection ids = rootItemIds(); + if (ids != null) + iteratorStack.push(ids.iterator()); + while (!iteratorStack.isEmpty()) { + + // Get the iterator for current tree level + Iterator i = (Iterator) iteratorStack.peek(); + + // If the level is finished, back to previous tree level + if (!i.hasNext()) { + + // Remove used iterator from the stack + iteratorStack.pop(); + + // Close node + if (!iteratorStack.isEmpty()) + target.endTag("node"); + } + + // Add the item on current level + else { + Object itemId = i.next(); + + // Start the item / node + boolean isNode = areChildrenAllowed(itemId); + if (isNode) + target.startTag("node"); + else + target.startTag("leaf"); + + // Add attributes + target.addAttribute("caption", getItemCaption(itemId)); + Resource icon = getItemIcon(itemId); + if (icon != null) + target.addAttribute("icon", getItemIcon(itemId)); + String key = itemIdMapper.key(itemId); + target.addAttribute("key", key); + if (isSelected(itemId)) { + target.addAttribute("selected", true); + selectedKeys[keyIndex++] = key; + } + if (areChildrenAllowed(itemId) && isExpanded(itemId)) { + target.addAttribute("expanded", true); + expandedKeys.add(key); + } + + // Actions + if (actionHandlers != null) { + target.startTag("al"); + for (Iterator ahi = actionHandlers.iterator(); + ahi.hasNext(); + ) { + Action[] aa = + ((Action.Handler) ahi.next()).getActions( + itemId, + this); + if (aa != null) + for (int ai = 0; ai < aa.length; ai++) { + String akey = actionMapper.key(aa[ai]); + actionSet.add(aa[ai]); + target.addSection("ak", akey); + } + } + target.endTag("al"); + } + + // Add 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.startTag("actions"); + target.addVariable(this, "action", ""); + for (Iterator i = actionSet.iterator(); i.hasNext();) { + Action a = (Action) 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"); + } + + // 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 ***************************************** */ + + /** + * @see com.itmill.toolkit.data.Container.Hierarchical#areChildrenAllowed(Object) + */ + public boolean areChildrenAllowed(Object itemId) { + return ((Container.Hierarchical) items).areChildrenAllowed(itemId); + } + + /** + * @see com.itmill.toolkit.data.Container.Hierarchical#getChildren(Object) + */ + public Collection getChildren(Object itemId) { + return ((Container.Hierarchical) items).getChildren(itemId); + } + + /** + * @see com.itmill.toolkit.data.Container.Hierarchical#getParent(Object) + */ + public Object getParent(Object itemId) { + return ((Container.Hierarchical) items).getParent(itemId); + } + + /** + * @see com.itmill.toolkit.data.Container.Hierarchical#hasChildren(Object) + */ + public boolean hasChildren(Object itemId) { + return ((Container.Hierarchical) items).hasChildren(itemId); + } + + /** + * @see com.itmill.toolkit.data.Container.Hierarchical#isRoot(Object) + */ + public boolean isRoot(Object itemId) { + return ((Container.Hierarchical) items).isRoot(itemId); + } + + /** + * @see com.itmill.toolkit.data.Container.Hierarchical#rootItemIds() + */ + public Collection rootItemIds() { + return ((Container.Hierarchical) items).rootItemIds(); + } + + /** + * @see com.itmill.toolkit.data.Container.Hierarchical#setChildrenAllowed(Object, boolean) + */ + public boolean setChildrenAllowed( + Object itemId, + boolean areChildrenAllowed) { + boolean success = + ((Container.Hierarchical) items).setChildrenAllowed( + itemId, + areChildrenAllowed); + if (success) + fireValueChange(); + return success; + } + + /** + * @see com.itmill.toolkit.data.Container.Hierarchical#setParent(Object, Object) + */ + public boolean setParent(Object itemId, Object newParentId) { + boolean success = + ((Container.Hierarchical) items).setParent(itemId, newParentId); + if (success) + requestRepaint(); + return success; + } + + /* Overriding select behavior******************************************** */ + + /** + * @see com.itmill.toolkit.data.Container.Viewer#setContainerDataSource(Container) + */ + public void setContainerDataSource(Container newDataSource) { + + // 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 { + + /** + * Serial generated by eclipse. + */ + private static final long serialVersionUID = 3832624001804481075L; + private Object expandedItemId; + /** New instance of options change event + * @param source Source of the event. + */ + public ExpandEvent(Component source, Object expandedItemId) { + super(source); + this.expandedItemId = expandedItemId; + } + + /** Node where the event occurred + * @return Source of the event. + */ + public Object getItemId() { + return this.expandedItemId; + } + } + + /** Expand event listener + * @author IT Mill Ltd. + * @version @VERSION@ + * @since 3.0 + */ + public interface ExpandListener { + + /** A node has been expanded. + * @param event Expand event. + */ + public void nodeExpand(ExpandEvent event); + } + + /** Add expand listener + * @param listener Listener to be added. + */ + public void addListener(ExpandListener listener) { + addListener(ExpandEvent.class, listener, EXPAND_METHOD); + } + + /** Remove expand listener + * @param listener Listener to be removed. + */ + public void removeListener(ExpandListener listener) { + removeListener(ExpandEvent.class, listener, EXPAND_METHOD); + } + + /** Emit expand event. */ + 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 { + + /** + * Serial generated by eclipse. + */ + private static final long serialVersionUID = 3257009834783290160L; + + private Object collapsedItemId; + + /** New instance of options change event + * @param source Source of the event. + */ + public CollapseEvent(Component source, Object collapsedItemId) { + super(source); + this.collapsedItemId = collapsedItemId; + } + + /** Node where the event occurred + * @return Source of the event. + */ + public Object getItemId() { + return collapsedItemId; + } + } + + /** Collapse event listener + * @author IT Mill Ltd. + * @version @VERSION@ + * @since 3.0 + */ + public interface CollapseListener { + + /** A node has been collapsed. + * @param event Collapse event. + */ + public void nodeCollapse(CollapseEvent event); + } + + /** Add collapse listener + * @param listener Listener to be added. + */ + public void addListener(CollapseListener listener) { + addListener(CollapseEvent.class, listener, COLLAPSE_METHOD); + } + + /** Remove collapse listener + * @param listener Listener to be removed. + */ + public void removeListener(CollapseListener listener) { + removeListener(CollapseEvent.class, listener, COLLAPSE_METHOD); + } + + /** Emit collapse event. */ + protected void fireCollapseEvent(Object itemId) { + fireEvent(new CollapseEvent(this, itemId)); + } + + /* Action container *************************************************** */ + + /** Adds an action handler. + * @see com.itmill.toolkit.event.Action.Container#addActionHandler(Action.Handler) + */ + public void addActionHandler(Action.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.itmill.toolkit.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(); + } + } + + /** + * @see com.itmill.toolkit.ui.Select#getVisibleItemIds() + */ + public Collection getVisibleItemIds() { + + LinkedList visible = new LinkedList(); + + // Iterate trough hierarchical tree using a stack of iterators + Stack iteratorStack = new Stack(); + Collection ids = rootItemIds(); + if (ids != null) + iteratorStack.push(ids.iterator()); + while (!iteratorStack.isEmpty()) { + + // Get the iterator for current tree level + Iterator i = (Iterator) iteratorStack.peek(); + + // If the level is finished, back to previous tree level + if (!i.hasNext()) { + + // Remove used iterator from the stack + iteratorStack.pop(); + } + + // Add the item on current level + else { + Object itemId = i.next(); + + visible.add(itemId); + + // Add children if expanded, or close the tag + if (isExpanded(itemId) && hasChildren(itemId)) { + iteratorStack.push(getChildren(itemId).iterator()); + } + } + } + + return visible; + } + + /** Adding new items is not supported. + * @see com.itmill.toolkit.ui.Select#setNewItemsAllowed(boolean) + * @throws UnsupportedOperationException if set to true. + */ + public void setNewItemsAllowed(boolean allowNewOptions) + throws UnsupportedOperationException { + if (allowNewOptions) + throw new UnsupportedOperationException(); + } + + /** Focusing to this component is not supported. + * @see com.itmill.toolkit.ui.AbstractField#focus() + * @throws UnsupportedOperationException if invoked. + */ + public void focus() throws UnsupportedOperationException { + throw new UnsupportedOperationException(); + } + +} diff --git a/src/com/itmill/toolkit/ui/Upload.java b/src/com/itmill/toolkit/ui/Upload.java new file mode 100644 index 0000000000..3bcb998352 --- /dev/null +++ b/src/com/itmill/toolkit/ui/Upload.java @@ -0,0 +1,446 @@ +/* ************************************************************************* + + IT Mill Toolkit + + Development of Browser User Intarfaces Made Easy + + Copyright (C) 2000-2006 IT Mill Ltd + + ************************************************************************* + + This product is distributed under commercial license that can be found + from the product package on license/license.txt. Use of this product might + require purchasing a commercial license from IT Mill Ltd. For guidelines + on usage, see license/licensing-guidelines.html + + ************************************************************************* + + For more information, contact: + + IT Mill Ltd phone: +358 2 4802 7180 + Ruukinkatu 2-4 fax: +358 2 4802 7181 + 20540, Turku email: info@itmill.com + Finland company www: www.itmill.com + + Primary source for information and releases: www.itmill.com + + ********************************************************************** */ + +package com.itmill.toolkit.ui; + +import java.io.InputStream; +import java.lang.reflect.Method; +import java.util.Map; + +import com.itmill.toolkit.terminal.PaintException; +import com.itmill.toolkit.terminal.PaintTarget; +import com.itmill.toolkit.terminal.UploadStream; + +import java.io.IOException; +import java.io.OutputStream; + +/** Component for client file uploading. + * + * @author IT Mill Ltd. + * @version @VERSION@ + * @since 3.0 + */ +public class Upload extends AbstractComponent implements Component.Focusable { + + /** Upload buffer size. */ + private static final int BUFFER_SIZE = 64 * 1024; // 64k + + /** Should the field be focused on next repaint */ + private 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 long focusableId = -1; + + /* TODO: Add a default constructor, receive to temp file. */ + + /** Creates a new instance of Upload that redirects the + * uploaded data to given stream. + * + */ + public Upload(String caption, Receiver uploadReceiver) { + this.focusableId = Window.getNewFocusableId(this); + setCaption(caption); + receiver = uploadReceiver; + } + + /** Get component type. + * @return Component type as string. + */ + public String getTag() { + return "upload"; + } + + /** Invoked when the value of a variable has changed. */ + public void changeVariables(Object source, Map variables) { + + // Check the variable name + if (!variables.containsKey("stream")) + return; + + // Get the upload stream + UploadStream upload = (UploadStream) variables.get("stream"); + + // Get file properties + String filename = upload.getContentName(); + String type = upload.getContentType(); + + // Get the output target stream + OutputStream out = receiver.receiveUpload(filename, type); + if (out == null) + throw new RuntimeException("Error getting outputstream from upload receiver"); + + InputStream in = upload.getStream(); + if (null==in) { + // No file, for instance non-existent filename in html upload + fireUploadInterrupted(filename, type, 0); + return; + } + byte buffer[] = new byte[BUFFER_SIZE]; + int bytesRead = 0; + long totalBytes = 0; + try { + while ((bytesRead = in.read(buffer)) > 0) { + out.write(buffer, 0, bytesRead); + totalBytes += bytesRead; + } + + // Download successfull + out.close(); + fireUploadSuccess(filename, type, totalBytes); + requestRepaint(); + + } catch (IOException e) { + + // Download interrupted + fireUploadInterrupted(filename, type, totalBytes); + } + } + + /** Paint the content of this component. + * @param target Target to paint the content on. + * @throws PaintException The paint operation failed. + */ + public void paintContent(PaintTarget target) throws PaintException { + // The field should be focused + if (focus) + target.addAttribute("focus", true); + + // The tab ordering number + if (this.tabIndex >= 0) + target.addAttribute("tabindex", this.tabIndex); + + target.addUploadStreamVariable(this, "stream"); + } + + /** Notify all upload listeners */ + private void notifyListeners() { + + } + + /** Interface that must be implemented by the upload receivers. + * @author IT Mill Ltd. + * @version @VERSION@ + * @since 3.0 + */ + public interface Receiver { + + /** 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; + + 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_SUCCEEDED_METHOD = + SucceededListener.class.getDeclaredMethod( + "uploadSucceeded", + new Class[] { SucceededEvent.class }); + } catch (java.lang.NoSuchMethodException e) { + // This should never happen + throw new java.lang.RuntimeException("Internal error"); + } + } + + /** Upload.Received event is sent when the upload receives a file, + * regardless if the receival was successfull. + * @author IT Mill Ltd. + * @version @VERSION@ + * @since 3.0 + */ + public class FinishedEvent extends Component.Event { + + /** + * Serial generated by eclipse. + */ + private static final long serialVersionUID = 3257288015385670969L; + + /** Length of the received file. */ + private long length; + + /** MIME type of the received file. */ + private String type; + + /** Received file name */ + private String filename; + + public FinishedEvent( + Upload source, + String filename, + String MIMEType, + long length) { + super(source); + this.type = MIMEType; + this.filename = filename; + this.length = length; + } + + /** Upload where the event occurred + * @return Source of the event. + */ + public Upload getUpload() { + return (Upload) getSource(); + } + /** + * Returns the filename. + */ + public String getFilename() { + return filename; + } + + /** + * Returns the length. + */ + public long getLength() { + return length; + } + + /** + * Returns the type. + */ + public String getMIMEType() { + return type; + } + + } + + /** 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 { + + /** + * Serial generated by eclipse. + */ + private static final long serialVersionUID = 3833746590157386293L; + + public FailedEvent( + 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 { + + /** + * Serial generated by eclipse. + */ + private static final long serialVersionUID = 3256445798169524023L; + + public SucceededEvent( + Upload source, + String filename, + String MIMEType, + long length) { + super(source, filename, MIMEType, length); + } + + } + + /** Receives events when the uploads are ready. + * @author IT Mill Ltd. + * @version @VERSION@ + * @since 3.0 + */ + public interface FinishedListener { + + /** Upload has finished. + * @param event Upload finished event. + */ + public void uploadFinished(FinishedEvent event); + } + + /** Receives events when the uploads are finished, but unsuccessfull. + * @author IT Mill Ltd. + * @version @VERSION@ + * @since 3.0 + */ + public interface FailedListener { + + /** Upload has finished unsuccessfully. + * @param event 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 { + + /** Upload successfull.. + * @param event Upload successfull event. + */ + public void uploadSucceeded(SucceededEvent event); + } + + /** Add upload received event listener + * @param listener Listener to be added. + */ + public void addListener(FinishedListener listener) { + addListener(FinishedEvent.class, listener, UPLOAD_FINISHED_METHOD); + } + + /** Remove upload received event listener + * @param listener Listener to be removed. + */ + public void removeListener(FinishedListener listener) { + removeListener(FinishedEvent.class, listener, UPLOAD_FINISHED_METHOD); + } + + /** Add upload interrupted event listener + * @param listener Listener to be added. + */ + public void addListener(FailedListener listener) { + addListener(FailedEvent.class, listener, UPLOAD_FAILED_METHOD); + } + + /** Remove upload interrupted event listener + * @param listener Listener to be removed. + */ + public void removeListener(FailedListener listener) { + removeListener(FinishedEvent.class, listener, UPLOAD_FAILED_METHOD); + } + + /** Add upload success event listener + * @param listener Listener to be added. + */ + public void addListener(SucceededListener listener) { + addListener(SucceededEvent.class, listener, UPLOAD_SUCCEEDED_METHOD); + } + + /** Remove upload success event listener + * @param listener Listener to be removed. + */ + public void removeListener(SucceededListener listener) { + removeListener(SucceededEvent.class, listener, UPLOAD_SUCCEEDED_METHOD); + } + + /** Emit upload received event. */ + protected void fireUploadReceived( + String filename, + String MIMEType, + long length) { + fireEvent(new Upload.FinishedEvent(this, filename, MIMEType, length)); + } + + /** Emit upload interrupted event. */ + protected void fireUploadInterrupted( + String filename, + String MIMEType, + long length) { + fireEvent(new Upload.FailedEvent(this, filename, MIMEType, length)); + } + + /** Emit upload success event. */ + protected void fireUploadSuccess( + String filename, + String MIMEType, + long length) { + fireEvent(new Upload.SucceededEvent(this, filename, MIMEType, length)); + } + /** Returns the current receiver. + * @return Receiver + */ + public Receiver getReceiver() { + return receiver; + } + + /** Sets the receiver. + * @param receiver The receiver to set + */ + public void setReceiver(Receiver receiver) { + this.receiver = receiver; + } + /** + * @see com.itmill.toolkit.ui.Component.Focusable#focus() + */ + public void focus() { + Window w = getWindow(); + if (w != null) { + w.setFocusedComponent(this); + } + } + + /** + * @see com.itmill.toolkit.ui.Component.Focusable#getTabIndex() + */ + public int getTabIndex() { + return this.tabIndex; + } + + /** + * @see com.itmill.toolkit.ui.Component.Focusable#setTabIndex(int) + */ + public void setTabIndex(int tabIndex) { + this.tabIndex = tabIndex; + } + + /** + * @see com.itmill.toolkit.ui.Component.Focusable#getFocusableId() + */ + public long getFocusableId() { + return this.focusableId; + } + +} diff --git a/src/com/itmill/toolkit/ui/Window.java b/src/com/itmill/toolkit/ui/Window.java new file mode 100644 index 0000000000..f74ff710a8 --- /dev/null +++ b/src/com/itmill/toolkit/ui/Window.java @@ -0,0 +1,695 @@ +/* ************************************************************************* + + IT Mill Toolkit + + Development of Browser User Intarfaces Made Easy + + Copyright (C) 2000-2006 IT Mill Ltd + + ************************************************************************* + + This product is distributed under commercial license that can be found + from the product package on license/license.txt. Use of this product might + require purchasing a commercial license from IT Mill Ltd. For guidelines + on usage, see license/licensing-guidelines.html + + ************************************************************************* + + For more information, contact: + + IT Mill Ltd phone: +358 2 4802 7180 + Ruukinkatu 2-4 fax: +358 2 4802 7181 + 20540, Turku email: info@itmill.com + Finland company www: www.itmill.com + + Primary source for information and releases: www.itmill.com + + ********************************************************************** */ + +package com.itmill.toolkit.ui; + +import com.itmill.toolkit.Application; +import com.itmill.toolkit.terminal.DownloadStream; +import com.itmill.toolkit.terminal.PaintException; +import com.itmill.toolkit.terminal.PaintTarget; +import com.itmill.toolkit.terminal.ParameterHandler; +import com.itmill.toolkit.terminal.Resource; +import com.itmill.toolkit.terminal.Sizeable; +import com.itmill.toolkit.terminal.Terminal; +import com.itmill.toolkit.terminal.URIHandler; + +import java.lang.ref.WeakReference; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.Map; +import java.util.Iterator; + +/** + * Application window component. + * + * @author IT Mill Ltd. + * @version + * @VERSION@ + * @since 3.0 + */ +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 applicaiton 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; + + /** + * 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 LinkedList openList = new LinkedList(); + + /** The name of the window */ + private String name = null; + + /** Window border mode */ + private int border = BORDER_DEFAULT; + + /** Focused component */ + private Focusable focusedComponent; + + /* ********************************************************************* */ + + /** + * Create 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 + * Title of the window + */ + public Window() { + this("", null); + } + + /** + * Create 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 + * Title of the window + */ + public Window(String caption) { + this(caption, null); + } + + /** + * Create 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 + * Title of the window + * @param layout + * Layout of the window + */ + public Window(String caption, Layout layout) { + super(caption, layout); + setScrollable(true); + } + + /** + * Get terminal type. + * + * @return Value of property terminal. + */ + public Terminal getTerminal() { + return this.terminal; + } + + /* ********************************************************************* */ + + /** + * Get 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 parent window of the component. + */ + public final Window getWindow() { + return this; + } + + /** + * Get 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 parent application of the component. + */ + public final Application getApplication() { + return this.application; + } + + /** + * Getter for property parent. Parent is the visual parent of a component. + * Each component can belong to only one ComponentContainer at time. + * + * @return Value of property parent. + */ + public final Component getParent() { + return null; + } + + /** + * Setter for property parent. Parent is the visual parent of a component. + * This is mostly called by containers add method. Setting parent is not + * allowed for the window, and thus this call should newer be called. + * + * @param parent + * New value of property parent. + */ + public void setParent(Component parent) { + throw new RuntimeException("Setting parent for Window is not allowed"); + } + + /** + * Get component UIDL tag. + * + * @return Component UIDL tag as string. + */ + public String getTag() { + return "window"; + } + + /* ********************************************************************* */ + + /** Add new URI handler to this window */ + public void addURIHandler(URIHandler handler) { + if (uriHandlerList == null) + uriHandlerList = new LinkedList(); + synchronized (uriHandlerList) { + uriHandlerList.addLast(handler); + } + } + + /** Remove given URI handler from this window */ + public void removeURIHandler(URIHandler handler) { + if (handler == null || uriHandlerList == null) + return; + synchronized (uriHandlerList) { + uriHandlerList.remove(handler); + if (uriHandlerList.isEmpty()) + uriHandlerList = null; + } + } + + /** + * Handle uri recursively. + */ + 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++) { + 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; + } + + /* ********************************************************************* */ + + /** Add new parameter handler to this window. */ + public void addParameterHandler(ParameterHandler handler) { + if (parameterHandlerList == null) + parameterHandlerList = new LinkedList(); + synchronized (parameterHandlerList) { + parameterHandlerList.addLast(handler); + } + } + + /** Remove given URI handler from this window. */ + public void removeParameterHandler(ParameterHandler handler) { + 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); + } + } + + /* ********************************************************************* */ + + /** + * Get theme for this window. + * + * @return 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 (theme != null) + return theme; + if ((application != null) && (application.getTheme() != null)) + return application.getTheme(); + if (terminal != null) + return terminal.getDefaultTheme(); + return null; + } + + /** + * Set theme for this window. + * + * @param theme + * New theme for this window. Null implies the default theme. + */ + public void setTheme(String theme) { + this.theme = theme; + requestRepaint(); + } + + /** + * Paint the content of this component. + * + * @param event + * PaintEvent. + * @throws PaintException + * The paint operation failed. + */ + public synchronized void paintContent(PaintTarget target) + throws PaintException { + + // Set the window name + target.addAttribute("name", getName()); + + // Mark main window + if (getApplication() != null + && this == getApplication().getMainWindow()) + target.addAttribute("main", true); + + // Open requested resource + synchronized (openList) { + if (!openList.isEmpty()) { + for (Iterator i = openList.iterator(); i.hasNext();) + ((OpenResource) i.next()).paintContent(target); + openList.clear(); + } + } + + // Contents of the window panel is painted + super.paintContent(target); + + // Set focused component + if (this.focusedComponent != null) + target.addVariable(this, "focused", "" + + this.focusedComponent.getFocusableId()); + else + target.addVariable(this, "focused", ""); + + } + + /* ********************************************************************* */ + + /** + * Open the given resource in this window. + */ + public void open(Resource resource) { + synchronized (openList) { + openList.add(new OpenResource(resource, null, -1, -1, + BORDER_DEFAULT)); + } + requestRepaint(); + } + + /* ********************************************************************* */ + + /** + * Open the given resource in named terminal window. Empty or + * <code>null</code> window name results the resource to be opened in this + * window. + */ + public void open(Resource resource, String windowName) { + synchronized (openList) { + openList.add(new OpenResource(resource, windowName, -1, -1, + BORDER_DEFAULT)); + } + requestRepaint(); + } + + /* ********************************************************************* */ + + /** + * Open 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. + */ + public void open(Resource resource, String windowName, int width, + int height, int border) { + synchronized (openList) { + 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 String + */ + public URL getURL() { + + if (application == null) + return null; + + try { + return new URL(application.getURL(), getName() + "/"); + } catch (MalformedURLException e) { + throw new RuntimeException("Internal problem, please report"); + } + } + + /** + * Get the unique name of the window that indentifies it on the terminal. + * + * @return String + */ + public String getName() { + return name; + } + + /** + * Returns the border. + * + * @return int + */ + 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.itmill.toolkit.Application#addWindow(Window)} method should be + * used to add the window to an application and + * {@link com.itmill.toolkit.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; + + // Send detach event if the window is connected to application + if (this.application != null) { + detach(); + } + + // Connect to new parent + this.application = application; + + // Send 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. Also the + * name may only contain the following characters: a-z, A-Z and 0-9. + * </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"); + + // Check the name format + if (name != null) + for (int i = 0; i < name.length(); i++) { + char c = name.charAt(i); + if (!(('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') || ('0' <= c && c <= '9'))) + throw new IllegalArgumentException( + "Window name can contain " + + "only a-z, A-Z and 0-9 characters: '" + + name + "' given."); + } + + this.name = name; + } + + /** + * Set terminal type. The terminal type is set by the the terminal adapter + * and may change from time to time. + * + * @param type + * terminal type to set + */ + public void setTerminal(Terminal type) { + this.terminal = type; + } + + /** + * Window only supports pixels as unit. + * + * @see com.itmill.toolkit.terminal.Sizeable#getHeightUnits() + */ + public void setHeightUnits(int units) { + if (units != Sizeable.UNITS_PIXELS) + throw new IllegalArgumentException("Only pixels are supported"); + } + + /** + * Window only supports pixels as unit. + * + * @see com.itmill.toolkit.terminal.Sizeable#getWidthUnits() + */ + public void setWidthUnits(int units) { + if (units != Sizeable.UNITS_PIXELS) + throw new IllegalArgumentException("Only pixels are supported"); + } + + /** Private data structure for storing opening window properties */ + private class OpenResource { + + private Resource resource; + + private String name; + + private int width; + + private int height; + + private int border; + + /** Create new open resource */ + 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; + } + + /** Paint the open-tag inside the window. */ + 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"); + } + } + + /** + * @see com.itmill.toolkit.terminal.VariableOwner#changeVariables(java.lang.Object, + * java.util.Map) + */ + public void changeVariables(Object source, Map variables) { + super.changeVariables(source, variables); + + // Get focused component + String focusedId = (String) variables.get("focused"); + if (focusedId != null) { + try { + long id = Long.parseLong(focusedId); + this.focusedComponent = Window.getFocusableById(id); + } catch (NumberFormatException ignored) { + // We ignore invalid focusable ids + } + } + + } + + /** + * Get currently focused component in this window. + * + * @return Focused component or null if none is focused. + */ + public Component.Focusable getFocusedComponent() { + return this.focusedComponent; + } + + /** + * Set currently focused component in this window. + * + * @param focusable + * Focused component or null if none is focused. + */ + public void setFocusedComponent(Component.Focusable focusable) { + this.focusedComponent = focusable; + } + + /* Focusable id generator ****************************************** */ + + private static long lastUsedFocusableId = 0; + + private static Map focusableComponents = new HashMap(); + + /** Get an id for focusable component. */ + public static long getNewFocusableId(Component.Focusable focusable) { + long newId = ++lastUsedFocusableId; + WeakReference ref = new WeakReference(focusable); + focusableComponents.put(new Long(newId), ref); + return newId; + } + + /** Map focusable id back to focusable component. */ + public static Component.Focusable getFocusableById(long focusableId) { + WeakReference ref = (WeakReference) focusableComponents.get(new Long( + focusableId)); + if (ref != null) { + Object o = ref.get(); + if (o != null) { + return (Component.Focusable) o; + } + } + return null; + } + + /** Release focusable component id when not used anymore. */ + public static void removeFocusableId(long focusableId) { + Long id = new Long(focusableId); + WeakReference ref = (WeakReference) focusableComponents.get(id); + ref.clear(); + focusableComponents.remove(id); + } +} diff --git a/src/com/itmill/toolkit/ui/doc-files/component_class_hierarchy.gif b/src/com/itmill/toolkit/ui/doc-files/component_class_hierarchy.gif Binary files differnew file mode 100644 index 0000000000..936c220d11 --- /dev/null +++ b/src/com/itmill/toolkit/ui/doc-files/component_class_hierarchy.gif diff --git a/src/com/itmill/toolkit/ui/doc-files/component_interfaces.gif b/src/com/itmill/toolkit/ui/doc-files/component_interfaces.gif Binary files differnew file mode 100644 index 0000000000..44c99826bb --- /dev/null +++ b/src/com/itmill/toolkit/ui/doc-files/component_interfaces.gif diff --git a/src/com/itmill/toolkit/ui/package.html b/src/com/itmill/toolkit/ui/package.html new file mode 100644 index 0000000000..b6b17b43d0 --- /dev/null +++ b/src/com/itmill/toolkit/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 the MillStone UI component library.</p> + +<h2>Package Specification</h2> + +<p><strong>Interface hierarchy</strong></p> + +<p>The general interface hierarchy looks like this:</p> + +<p><center><img src="doc-files/component_interfaces.gif"/> </center></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 org.millstone.base.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 org.millstone.base.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 org.millstone.base.ui.Component}.</p> + +<p>The next level in the component hierarchy are the classes implementing +the {@link org.millstone.base.ui.ComponentContainer} interface. It adds the +capacity to contain other components to +{@link org.millstone.base.ui.Component} with a simple API.</p> + +<p>The third and last level is the {@link org.millstone.base.ui.Layout}, +which adds the concept of location to the components contained in a +{@link org.millstone.base.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> + +<p><center><img src="doc-files/component_class_hierarchy.gif"/></center></p> + +<p><center><i>Underlined classes are abstract.</i></center></p> + +<p>At the top level is {@link org.millstone.base.ui.AbstractComponent} +which implements the {@link org.millstone.base.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 org.millstone.base.ui.AbstractComponentContainer} serves +as the root class for all components (for example, panels and windows) who +can contain other components. {@link org.millstone.base.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> |