]> source.dussan.org Git - vaadin-framework.git/commitdiff
svn changeset:5133/svn branch:trunk
authorMatti Tahvonen <matti.tahvonen@itmill.com>
Tue, 5 Aug 2008 07:47:17 +0000 (07:47 +0000)
committerMatti Tahvonen <matti.tahvonen@itmill.com>
Tue, 5 Aug 2008 07:47:17 +0000 (07:47 +0000)
152 files changed:
src/com/itmill/toolkit/terminal/terminal/ApplicationResource.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/ClassResource.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/CompositeErrorMessage.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/DownloadStream.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/ErrorMessage.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/ExternalResource.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/FileResource.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/KeyMapper.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/PaintException.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/PaintTarget.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/Paintable.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/ParameterHandler.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/Resource.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/Scrollable.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/Sizeable.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/StreamResource.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/SystemError.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/Terminal.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/ThemeResource.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/URIHandler.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/UploadStream.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/UserError.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/VariableOwner.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/DefaultWidgetSet.gwt.xml [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/DefaultWidgetSetNoEntry.gwt.xml [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/ApplicationConfiguration.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/ApplicationConnection.java [new file with mode: 0755]
src/com/itmill/toolkit/terminal/terminal/gwt/client/BrowserInfo.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/Caption.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/CaptionWrapper.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/Console.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/Container.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/ContainerResizedListener.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/DateTimeService.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/DebugConsole.java [new file with mode: 0755]
src/com/itmill/toolkit/terminal/terminal/gwt/client/DefaultWidgetSet.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/ErrorMessage.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/Focusable.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/LocaleNotLoadedException.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/LocaleService.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/NullConsole.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/Paintable.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/StyleConstants.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/Tooltip.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/TooltipInfo.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/UIDL.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/Util.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/WidgetSet.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/Action.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/ActionOwner.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/AlignmentInfo.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/CalendarEntry.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/CalendarPanel.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/ContextMenu.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/Field.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/IAccordion.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/IButton.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/ICheckBox.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/ICustomComponent.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/ICustomLayout.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/IDateField.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/IDateFieldCalendar.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/IEmbedded.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/IExpandLayout.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/IFilterSelect.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/IForm.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/IFormLayout.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/IGridLayout.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/IHorizontalExpandLayout.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/ILabel.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/ILink.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/IListSelect.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/IMenuBar.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/INativeSelect.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/IOptionGroup.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/IOptionGroupBase.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/IOrderedLayout.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/IOrderedLayoutHorizontal.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/IOrderedLayoutVertical.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/IPanel.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/IPasswordField.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/IPopupCalendar.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/IProgressIndicator.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/IScrollTable.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/ISlider.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/ISplitPanel.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/ISplitPanelHorizontal.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/ISplitPanelVertical.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/ITablePaging.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/ITabsheet.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/ITabsheetBase.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/ITabsheetPanel.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/ITextArea.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/ITextField.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/ITextualDate.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/ITree.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/ITwinColSelect.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/IUnknownComponent.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/IUpload.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/IView.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/IWindow.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/Icon.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/MarginInfo.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/MenuBar.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/MenuItem.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/Notification.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/ShortcutActionHandler.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/Table.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/Time.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/ToolkitOverlay.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/TreeAction.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/TreeImages.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/absolutegrid/AbsoluteGrid.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/absolutegrid/ISizeableGridLayout.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/absolutegrid/ISizeableOrderedLayout.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/richtextarea/IRichTextArea.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/richtextarea/RichTextToolbar$Strings.properties [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/richtextarea/RichTextToolbar.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/richtextarea/backColors.gif [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/richtextarea/bold.gif [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/richtextarea/createLink.gif [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/richtextarea/fontSizes.gif [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/richtextarea/fonts.gif [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/richtextarea/foreColors.gif [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/richtextarea/gwtLogo.png [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/richtextarea/hr.gif [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/richtextarea/indent.gif [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/richtextarea/insertImage.gif [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/richtextarea/italic.gif [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/richtextarea/justifyCenter.gif [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/richtextarea/justifyLeft.gif [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/richtextarea/justifyRight.gif [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/richtextarea/ol.gif [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/richtextarea/outdent.gif [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/richtextarea/removeFormat.gif [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/richtextarea/removeLink.gif [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/richtextarea/strikeThrough.gif [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/richtextarea/subscript.gif [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/richtextarea/superscript.gif [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/richtextarea/ul.gif [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/richtextarea/underline.gif [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/server/ApplicationPortlet.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/server/ApplicationServlet.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/server/CommunicationManager.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/server/HttpUploadStream.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/server/JsonPaintTarget.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/server/PortletApplicationContext.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/server/SessionExpired.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/server/WebApplicationContext.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/gwt/server/WebBrowser.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/package.html [new file with mode: 0644]
src/com/itmill/toolkit/terminal/terminal/web/ApplicationServlet.java [new file with mode: 0644]

diff --git a/src/com/itmill/toolkit/terminal/terminal/ApplicationResource.java b/src/com/itmill/toolkit/terminal/terminal/ApplicationResource.java
new file mode 100644 (file)
index 0000000..94f3a21
--- /dev/null
@@ -0,0 +1,73 @@
+/* 
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.itmill.toolkit.terminal;
+
+import com.itmill.toolkit.Application;
+
+/**
+ * This interface must be implemented by classes wishing to provide Application
+ * resources.
+ * <p>
+ * <code>ApplicationResource</code> are a set of named resources (pictures,
+ * sounds, etc) associated with some specific application. Having named
+ * application resources provides a convenient method for having inter-theme
+ * common resources for an application.
+ * </p>
+ * 
+ * @author IT Mill Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+public interface ApplicationResource extends Resource {
+
+    /**
+     * Default cache time.
+     */
+    public static final long DEFAULT_CACHETIME = 1000 * 60 * 60 * 24;
+
+    /**
+     * Gets resource as stream.
+     */
+    public DownloadStream getStream();
+
+    /**
+     * Gets the application of the resource.
+     */
+    public Application getApplication();
+
+    /**
+     * Gets the virtual filename for this resource.
+     * 
+     * @return the file name associated to this resource.
+     */
+    public String getFilename();
+
+    /**
+     * Gets the length of cache expiration time.
+     * 
+     * <p>
+     * This gives the adapter the possibility cache streams sent to the client.
+     * The caching may be made in adapter or at the client if the client
+     * supports caching. Default is <code>DEFAULT_CACHETIME</code>.
+     * </p>
+     * 
+     * @return Cache time in milliseconds
+     */
+    public long getCacheTime();
+
+    /**
+     * Gets the size of the download buffer used for this resource.
+     * 
+     * <p>
+     * If the buffer size is 0, the buffer size is decided by the terminal
+     * adapter. The default value is 0.
+     * </p>
+     * 
+     * @return int the size of the buffer in bytes.
+     */
+    public int getBufferSize();
+
+}
diff --git a/src/com/itmill/toolkit/terminal/terminal/ClassResource.java b/src/com/itmill/toolkit/terminal/terminal/ClassResource.java
new file mode 100644 (file)
index 0000000..cd8498b
--- /dev/null
@@ -0,0 +1,177 @@
+/* 
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.itmill.toolkit.terminal;
+
+import com.itmill.toolkit.Application;
+import com.itmill.toolkit.service.FileTypeResolver;
+
+/**
+ * <code>ClassResource</code> is a named resource accessed with the class
+ * loader.
+ * 
+ * This can be used to access resources such as icons, files, etc.
+ * 
+ * @see java.lang.Class#getResource(java.lang.String)
+ * 
+ * @author IT Mill Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+public class ClassResource implements ApplicationResource {
+
+    /**
+     * Default buffer size for this stream resource.
+     */
+    private int bufferSize = 0;
+
+    /**
+     * Default cache time for this stream resource.
+     */
+    private long cacheTime = DEFAULT_CACHETIME;
+
+    /**
+     * Associated class used for indetifying the source of the resource.
+     */
+    private final Class associatedClass;
+
+    /**
+     * Name of the resource is relative to the associated class.
+     */
+    private final String resourceName;
+
+    /**
+     * Application used for serving the class.
+     */
+    private final Application application;
+
+    /**
+     * Creates a new application resource instance. The resource id is relative
+     * to the location of the application class.
+     * 
+     * @param resourceName
+     *                the Unique identifier of the resource within the
+     *                application.
+     * @param application
+     *                the application this resource will be added to.
+     */
+    public ClassResource(String resourceName, Application application) {
+        associatedClass = application.getClass();
+        this.resourceName = resourceName;
+        this.application = application;
+        if (resourceName == null) {
+            throw new NullPointerException();
+        }
+        application.addResource(this);
+    }
+
+    /**
+     * Creates a new application resource instance.
+     * 
+     * @param associatedClass
+     *                the class of the which the resource is associated.
+     * @param resourceName
+     *                the Unique identifier of the resource within the
+     *                application.
+     * @param application
+     *                the application this resource will be added to.
+     */
+    public ClassResource(Class associatedClass, String resourceName,
+            Application application) {
+        this.associatedClass = associatedClass;
+        this.resourceName = resourceName;
+        this.application = application;
+        if (resourceName == null || associatedClass == null) {
+            throw new NullPointerException();
+        }
+        application.addResource(this);
+    }
+
+    /**
+     * Gets the MIME type of this resource.
+     * 
+     * @see com.itmill.toolkit.terminal.Resource#getMIMEType()
+     */
+    public String getMIMEType() {
+        return FileTypeResolver.getMIMEType(resourceName);
+    }
+
+    /**
+     * Gets the application of this resource.
+     * 
+     * @see com.itmill.toolkit.terminal.ApplicationResource#getApplication()
+     */
+    public Application getApplication() {
+        return application;
+    }
+
+    /**
+     * Gets the virtual filename for this resource.
+     * 
+     * @return the file name associated to this resource.
+     * @see com.itmill.toolkit.terminal.ApplicationResource#getFilename()
+     */
+    public String getFilename() {
+        int index = 0;
+        int next = 0;
+        while ((next = resourceName.indexOf('/', index)) > 0
+                && next + 1 < resourceName.length()) {
+            index = next + 1;
+        }
+        return resourceName.substring(index);
+    }
+
+    /**
+     * Gets resource as stream.
+     * 
+     * @see com.itmill.toolkit.terminal.ApplicationResource#getStream()
+     */
+    public DownloadStream getStream() {
+        final DownloadStream ds = new DownloadStream(associatedClass
+                .getResourceAsStream(resourceName), getMIMEType(),
+                getFilename());
+        ds.setBufferSize(getBufferSize());
+        ds.setCacheTime(cacheTime);
+        return ds;
+    }
+
+    /* documented in superclass */
+    public int getBufferSize() {
+        return bufferSize;
+    }
+
+    /**
+     * Sets the size of the download buffer used for this resource.
+     * 
+     * @param bufferSize
+     *                the size of the buffer in bytes.
+     */
+    public void setBufferSize(int bufferSize) {
+        this.bufferSize = bufferSize;
+    }
+
+    /* documented in superclass */
+    public long getCacheTime() {
+        return cacheTime;
+    }
+
+    /**
+     * Sets the length of cache expiration time.
+     * 
+     * <p>
+     * This gives the adapter the possibility cache streams sent to the client.
+     * The caching may be made in adapter or at the client if the client
+     * supports caching. Zero or negavive value disbales the caching of this
+     * stream.
+     * </p>
+     * 
+     * @param cacheTime
+     *                the cache time in milliseconds.
+     * 
+     */
+    public void setCacheTime(long cacheTime) {
+        this.cacheTime = cacheTime;
+    }
+}
diff --git a/src/com/itmill/toolkit/terminal/terminal/CompositeErrorMessage.java b/src/com/itmill/toolkit/terminal/terminal/CompositeErrorMessage.java
new file mode 100644 (file)
index 0000000..c1caf69
--- /dev/null
@@ -0,0 +1,186 @@
+/* 
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.itmill.toolkit.terminal;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * Class for combining multiple error messages together.
+ * 
+ * @author IT Mill Ltd
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+public class CompositeErrorMessage implements ErrorMessage {
+
+    /**
+     * Array of all the errors.
+     */
+    private final List errors;
+
+    /**
+     * Level of the error.
+     */
+    private int level;
+
+    /**
+     * Constructor for CompositeErrorMessage.
+     * 
+     * @param errorMessages
+     *                the Array of error messages that are listed togeter. Nulls
+     *                are ignored, but at least one message is required.
+     */
+    public CompositeErrorMessage(ErrorMessage[] errorMessages) {
+        errors = new ArrayList(errorMessages.length);
+        level = Integer.MIN_VALUE;
+
+        for (int i = 0; i < errorMessages.length; i++) {
+            addErrorMessage(errorMessages[i]);
+        }
+
+        if (errors.size() == 0) {
+            throw new IllegalArgumentException(
+                    "Composite error message must have at least one error");
+        }
+
+    }
+
+    /**
+     * Constructor for CompositeErrorMessage.
+     * 
+     * @param errorMessages
+     *                the Collection of error messages that are listed togeter.
+     *                At least one message is required.
+     */
+    public CompositeErrorMessage(Collection errorMessages) {
+        errors = new ArrayList(errorMessages.size());
+        level = Integer.MIN_VALUE;
+
+        for (final Iterator i = errorMessages.iterator(); i.hasNext();) {
+            addErrorMessage((ErrorMessage) i.next());
+        }
+
+        if (errors.size() == 0) {
+            throw new IllegalArgumentException(
+                    "Composite error message must have at least one error");
+        }
+    }
+
+    /**
+     * The error level is the largest error level in
+     * 
+     * @see com.itmill.toolkit.terminal.ErrorMessage#getErrorLevel()
+     */
+    public final int getErrorLevel() {
+        return level;
+    }
+
+    /**
+     * Adds a error message into this composite message. Updates the level
+     * field.
+     * 
+     * @param error
+     *                the error message to be added. Duplicate errors are
+     *                ignored.
+     */
+    private void addErrorMessage(ErrorMessage error) {
+        if (error != null && !errors.contains(error)) {
+            errors.add(error);
+            final int l = error.getErrorLevel();
+            if (l > level) {
+                level = l;
+            }
+        }
+    }
+
+    /**
+     * Gets Error Iterator.
+     * 
+     * @return the error iterator.
+     */
+    public Iterator iterator() {
+        return errors.iterator();
+    }
+
+    /**
+     * @see com.itmill.toolkit.terminal.Paintable#paint(com.itmill.toolkit.terminal.PaintTarget)
+     */
+    public void paint(PaintTarget target) throws PaintException {
+
+        if (errors.size() == 1) {
+            ((ErrorMessage) errors.iterator().next()).paint(target);
+        } else {
+            target.startTag("error");
+
+            if (level > 0 && level <= ErrorMessage.INFORMATION) {
+                target.addAttribute("level", "info");
+            } else if (level <= ErrorMessage.WARNING) {
+                target.addAttribute("level", "warning");
+            } else if (level <= ErrorMessage.ERROR) {
+                target.addAttribute("level", "error");
+            } else if (level <= ErrorMessage.CRITICAL) {
+                target.addAttribute("level", "critical");
+            } else {
+                target.addAttribute("level", "system");
+            }
+
+            // Paint all the exceptions
+            for (final Iterator i = errors.iterator(); i.hasNext();) {
+                ((ErrorMessage) i.next()).paint(target);
+            }
+
+            target.endTag("error");
+        }
+    }
+
+    /* Documented in super interface */
+    public void addListener(RepaintRequestListener listener) {
+    }
+
+    /* Documented in super interface */
+    public void removeListener(RepaintRequestListener listener) {
+    }
+
+    /* Documented in super interface */
+    public void requestRepaint() {
+    }
+
+    /* Documented in super interface */
+    public void requestRepaintRequests() {
+    }
+
+    /**
+     * Returns a comma separated list of the error messages.
+     * 
+     * @return String, comma separated list of error messages.
+     */
+    public String toString() {
+        String retval = "[";
+        int pos = 0;
+        for (final Iterator i = errors.iterator(); i.hasNext();) {
+            if (pos > 0) {
+                retval += ",";
+            }
+            pos++;
+            retval += i.next().toString();
+        }
+        retval += "]";
+
+        return retval;
+    }
+
+    public String getDebugId() {
+        return null;
+    }
+
+    public void setDebugId(String id) {
+        throw new UnsupportedOperationException(
+                "Setting testing id for this Paintable is not implemented");
+    }
+}
diff --git a/src/com/itmill/toolkit/terminal/terminal/DownloadStream.java b/src/com/itmill/toolkit/terminal/terminal/DownloadStream.java
new file mode 100644 (file)
index 0000000..e8eaef8
--- /dev/null
@@ -0,0 +1,204 @@
+/* 
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.itmill.toolkit.terminal;
+
+import java.io.InputStream;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+/**
+ * Downloadable stream.
+ * 
+ * @author IT Mill Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+public class DownloadStream {
+
+    /**
+     * Maximum cache time.
+     */
+    public static final long MAX_CACHETIME = Long.MAX_VALUE;
+
+    /**
+     * Default cache time.
+     */
+    public static final long DEFAULT_CACHETIME = 1000 * 60 * 60 * 24;
+
+    private InputStream stream;
+
+    private String contentType;
+
+    private String fileName;
+
+    private Map params;
+
+    private long cacheTime = DEFAULT_CACHETIME;
+
+    private int bufferSize = 0;
+
+    /**
+     * Creates a new instance of DownloadStream.
+     */
+    public DownloadStream(InputStream stream, String contentType,
+            String fileName) {
+        setStream(stream);
+        setContentType(contentType);
+        setFileName(fileName);
+    }
+
+    /**
+     * Gets downloadable stream.
+     * 
+     * @return output stream.
+     */
+    public InputStream getStream() {
+        return stream;
+    }
+
+    /**
+     * Sets the stream.
+     * 
+     * @param stream
+     *                The stream to set
+     */
+    public void setStream(InputStream stream) {
+        this.stream = stream;
+    }
+
+    /**
+     * Gets stream content type.
+     * 
+     * @return type of the stream content.
+     */
+    public String getContentType() {
+        return contentType;
+    }
+
+    /**
+     * Sets stream content type.
+     * 
+     * @param contentType
+     *                the contentType to set
+     */
+    public void setContentType(String contentType) {
+        this.contentType = contentType;
+    }
+
+    /**
+     * Returns the file name.
+     * 
+     * @return the name of the file.
+     */
+    public String getFileName() {
+        return fileName;
+    }
+
+    /**
+     * Sets the file name.
+     * 
+     * @param fileName
+     *                the file name to set.
+     */
+    public void setFileName(String fileName) {
+        this.fileName = fileName;
+    }
+
+    /**
+     * Sets a paramater for download stream. Parameters are optional information
+     * about the downloadable stream and their meaning depends on the used
+     * adapter. For example in WebAdapter they are interpreted as HTTP response
+     * headers.
+     * 
+     * If the parameters by this name exists, the old value is replaced.
+     * 
+     * @param name
+     *                the Name of the parameter to set.
+     * @param value
+     *                the Value of the parameter to set.
+     */
+    public void setParameter(String name, String value) {
+        if (params == null) {
+            params = new HashMap();
+        }
+        params.put(name, value);
+    }
+
+    /**
+     * Gets a paramater for download stream. Parameters are optional information
+     * about the downloadable stream and their meaning depends on the used
+     * adapter. For example in WebAdapter they are interpreted as HTTP response
+     * headers.
+     * 
+     * @param name
+     *                the Name of the parameter to set.
+     * @return Value of the parameter or null if the parameter does not exist.
+     */
+    public String getParameter(String name) {
+        if (params != null) {
+            return (String) params.get(name);
+        }
+        return null;
+    }
+
+    /**
+     * Gets the names of the parameters.
+     * 
+     * @return Iterator of names or null if no parameters are set.
+     */
+    public Iterator getParameterNames() {
+        if (params != null) {
+            return params.keySet().iterator();
+        }
+        return null;
+    }
+
+    /**
+     * Gets length of cache expiration time. This gives the adapter the
+     * possibility cache streams sent to the client. The caching may be made in
+     * adapter or at the client if the client supports caching. Default is
+     * <code>DEFAULT_CACHETIME</code>.
+     * 
+     * @return Cache time in milliseconds
+     */
+    public long getCacheTime() {
+        return cacheTime;
+    }
+
+    /**
+     * Sets length of cache expiration time. This gives the adapter the
+     * possibility cache streams sent to the client. The caching may be made in
+     * adapter or at the client if the client supports caching. Zero or negavive
+     * value disbales the caching of this stream.
+     * 
+     * @param cacheTime
+     *                the cache time in milliseconds.
+     */
+    public void setCacheTime(long cacheTime) {
+        this.cacheTime = cacheTime;
+    }
+
+    /**
+     * Gets the size of the download buffer.
+     * 
+     * @return int The size of the buffer in bytes.
+     */
+    public int getBufferSize() {
+        return bufferSize;
+    }
+
+    /**
+     * Sets the size of the download buffer.
+     * 
+     * @param bufferSize
+     *                the size of the buffer in bytes.
+     */
+    public void setBufferSize(int bufferSize) {
+        this.bufferSize = bufferSize;
+    }
+
+}
diff --git a/src/com/itmill/toolkit/terminal/terminal/ErrorMessage.java b/src/com/itmill/toolkit/terminal/terminal/ErrorMessage.java
new file mode 100644 (file)
index 0000000..8c3a256
--- /dev/null
@@ -0,0 +1,78 @@
+/* 
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.itmill.toolkit.terminal;
+
+/**
+ * Interface for rendering error messages to terminal. All the visible errors
+ * shown to user must implement this interface.
+ * 
+ * @author IT Mill Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+public interface ErrorMessage extends Paintable {
+
+    /**
+     * Error code for system errors and bugs.
+     */
+    public static final int SYSTEMERROR = 5000;
+
+    /**
+     * Error code for critical error messages.
+     */
+    public static final int CRITICAL = 4000;
+
+    /**
+     * Error code for regular error messages.
+     */
+    public static final int ERROR = 3000;
+
+    /**
+     * Error code for warning messages.
+     */
+    public static final int WARNING = 2000;
+
+    /**
+     * Error code for informational messages.
+     */
+    public static final int INFORMATION = 1000;
+
+    /**
+     * Gets the errors level.
+     * 
+     * @return the level of error as an integer.
+     */
+    public int getErrorLevel();
+
+    /**
+     * Error messages are inmodifiable and thus listeners are not needed. This
+     * method should be implemented as empty.
+     * 
+     * @param listener
+     *                the listener to be added.
+     * @see com.itmill.toolkit.terminal.Paintable#addListener(Paintable.RepaintRequestListener)
+     */
+    public void addListener(RepaintRequestListener listener);
+
+    /**
+     * Error messages are inmodifiable and thus listeners are not needed. This
+     * method should be implemented as empty.
+     * 
+     * @param listener
+     *                the listener to be removed.
+     * @see com.itmill.toolkit.terminal.Paintable#removeListener(Paintable.RepaintRequestListener)
+     */
+    public void removeListener(RepaintRequestListener listener);
+
+    /**
+     * Error messages are inmodifiable and thus listeners are not needed. This
+     * method should be implemented as empty.
+     * 
+     * @see com.itmill.toolkit.terminal.Paintable#requestRepaint()
+     */
+    public void requestRepaint();
+
+}
diff --git a/src/com/itmill/toolkit/terminal/terminal/ExternalResource.java b/src/com/itmill/toolkit/terminal/terminal/ExternalResource.java
new file mode 100644 (file)
index 0000000..356d3f1
--- /dev/null
@@ -0,0 +1,74 @@
+/* 
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.itmill.toolkit.terminal;
+
+import java.net.URL;
+
+import com.itmill.toolkit.service.FileTypeResolver;
+
+/**
+ * <code>ExternalResource</code> implements source for resources fetched from
+ * location specified by URL:s. The resources are fetched directly by the client
+ * terminal and are not fetched trough the terminal adapter.
+ * 
+ * @author IT Mill Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+public class ExternalResource implements Resource {
+
+    /**
+     * Url of the download.
+     */
+    private String sourceURL = null;
+
+    /**
+     * Creates a new download component for downloading directly from given URL.
+     * 
+     * @param sourceURL
+     *                the source URL.
+     */
+    public ExternalResource(URL sourceURL) {
+        if (sourceURL == null) {
+            throw new RuntimeException("Source must be non-null");
+        }
+
+        this.sourceURL = sourceURL.toString();
+    }
+
+    /**
+     * Creates a new download component for downloading directly from given URL.
+     * 
+     * @param sourceURL
+     *                the source URL.
+     */
+    public ExternalResource(String sourceURL) {
+        if (sourceURL == null) {
+            throw new RuntimeException("Source must be non-null");
+        }
+
+        this.sourceURL = sourceURL.toString();
+    }
+
+    /**
+     * Gets the URL of the external resource.
+     * 
+     * @return the URL of the external resource.
+     */
+    public String getURL() {
+        return sourceURL;
+    }
+
+    /**
+     * Gets the MIME type of the resource.
+     * 
+     * @see com.itmill.toolkit.terminal.Resource#getMIMEType()
+     */
+    public String getMIMEType() {
+        return FileTypeResolver.getMIMEType(getURL().toString());
+    }
+
+}
diff --git a/src/com/itmill/toolkit/terminal/terminal/FileResource.java b/src/com/itmill/toolkit/terminal/terminal/FileResource.java
new file mode 100644 (file)
index 0000000..eaa7061
--- /dev/null
@@ -0,0 +1,154 @@
+/* 
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.itmill.toolkit.terminal;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+
+import com.itmill.toolkit.Application;
+import com.itmill.toolkit.service.FileTypeResolver;
+
+/**
+ * <code>FileResources</code> are files or directories on local filesystem.
+ * The files and directories are served through URI:s to the client terminal and
+ * thus must be registered to an URI context before they can be used. The
+ * resource is automatically registered to the application when it is created.
+ * 
+ * @author IT Mill Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+public class FileResource implements ApplicationResource {
+
+    /**
+     * Default buffer size for this stream resource.
+     */
+    private int bufferSize = 0;
+
+    /**
+     * File where the downloaded content is fetched from.
+     */
+    private File sourceFile;
+
+    /**
+     * Application.
+     */
+    private final Application application;
+
+    /**
+     * Default cache time for this stream resource.
+     */
+    private long cacheTime = DownloadStream.DEFAULT_CACHETIME;
+
+    /**
+     * Creates a new file resource for providing given file for client
+     * terminals.
+     */
+    public FileResource(File sourceFile, Application application) {
+        this.application = application;
+        setSourceFile(sourceFile);
+        application.addResource(this);
+    }
+
+    /**
+     * Gets the resource as stream.
+     * 
+     * @see com.itmill.toolkit.terminal.ApplicationResource#getStream()
+     */
+    public DownloadStream getStream() {
+        try {
+            final DownloadStream ds = new DownloadStream(new FileInputStream(
+                    sourceFile), getMIMEType(), getFilename());
+            ds.setCacheTime(cacheTime);
+            return ds;
+        } catch (final FileNotFoundException e) {
+            // No logging for non-existing files at this level.
+            return null;
+        }
+    }
+
+    /**
+     * Gets the source file.
+     * 
+     * @return the source File.
+     */
+    public File getSourceFile() {
+        return sourceFile;
+    }
+
+    /**
+     * Sets the source file.
+     * 
+     * @param sourceFile
+     *                the source file to set.
+     */
+    public void setSourceFile(File sourceFile) {
+        this.sourceFile = sourceFile;
+    }
+
+    /**
+     * @see com.itmill.toolkit.terminal.ApplicationResource#getApplication()
+     */
+    public Application getApplication() {
+        return application;
+    }
+
+    /**
+     * @see com.itmill.toolkit.terminal.ApplicationResource#getFilename()
+     */
+    public String getFilename() {
+        return sourceFile.getName();
+    }
+
+    /**
+     * @see com.itmill.toolkit.terminal.Resource#getMIMEType()
+     */
+    public String getMIMEType() {
+        return FileTypeResolver.getMIMEType(sourceFile);
+    }
+
+    /**
+     * Gets the length of cache expiration time. This gives the adapter the
+     * possibility cache streams sent to the client. The caching may be made in
+     * adapter or at the client if the client supports caching. Default is
+     * <code>DownloadStream.DEFAULT_CACHETIME</code>.
+     * 
+     * @return Cache time in milliseconds.
+     */
+    public long getCacheTime() {
+        return cacheTime;
+    }
+
+    /**
+     * Sets the length of cache expiration time. This gives the adapter the
+     * possibility cache streams sent to the client. The caching may be made in
+     * adapter or at the client if the client supports caching. Zero or negavive
+     * value disbales the caching of this stream.
+     * 
+     * @param cacheTime
+     *                the cache time in milliseconds.
+     */
+    public void setCacheTime(long cacheTime) {
+        this.cacheTime = cacheTime;
+    }
+
+    /* documented in superclass */
+    public int getBufferSize() {
+        return bufferSize;
+    }
+
+    /**
+     * Sets the size of the download buffer used for this resource.
+     * 
+     * @param bufferSize
+     *                the size of the buffer in bytes.
+     */
+    public void setBufferSize(int bufferSize) {
+        this.bufferSize = bufferSize;
+    }
+
+}
diff --git a/src/com/itmill/toolkit/terminal/terminal/KeyMapper.java b/src/com/itmill/toolkit/terminal/terminal/KeyMapper.java
new file mode 100644 (file)
index 0000000..f516ac9
--- /dev/null
@@ -0,0 +1,86 @@
+/* 
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.itmill.toolkit.terminal;
+
+import java.util.Hashtable;
+
+/**
+ * <code>KeyMapper</code> is the simple two-way map for generating textual
+ * keys for objects and retrieving the objects later with the key.
+ * 
+ * @author IT Mill Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+public class KeyMapper {
+
+    private int lastKey = 0;
+
+    private final Hashtable objectKeyMap = new Hashtable();
+
+    private final Hashtable keyObjectMap = new Hashtable();
+
+    /**
+     * Gets key for an object.
+     * 
+     * @param o
+     *                the object.
+     */
+    public String key(Object o) {
+
+        if (o == null) {
+            return "null";
+        }
+
+        // If the object is already mapped, use existing key
+        String key = (String) objectKeyMap.get(o);
+        if (key != null) {
+            return key;
+        }
+
+        // If the object is not yet mapped, map it
+        key = String.valueOf(++lastKey);
+        objectKeyMap.put(o, key);
+        keyObjectMap.put(key, o);
+
+        return key;
+    }
+
+    /**
+     * Retrieves object with the key.
+     * 
+     * @param key
+     *                the name with the desired value.
+     * @return the object with the key.
+     */
+    public Object get(String key) {
+
+        return keyObjectMap.get(key);
+    }
+
+    /**
+     * Removes object from the mapper.
+     * 
+     * @param removeobj
+     *                the object to be removed.
+     */
+    public void remove(Object removeobj) {
+        final String key = (String) objectKeyMap.get(removeobj);
+
+        if (key != null) {
+            objectKeyMap.remove(key);
+            keyObjectMap.remove(removeobj);
+        }
+    }
+
+    /**
+     * Removes all objects from the mapper.
+     */
+    public void removeAll() {
+        objectKeyMap.clear();
+        keyObjectMap.clear();
+    }
+}
diff --git a/src/com/itmill/toolkit/terminal/terminal/PaintException.java b/src/com/itmill/toolkit/terminal/terminal/PaintException.java
new file mode 100644 (file)
index 0000000..1c0287e
--- /dev/null
@@ -0,0 +1,45 @@
+/* 
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.itmill.toolkit.terminal;
+
+import java.io.IOException;
+
+/**
+ * <code>PaintExcepection</code> is thrown if painting of a component fails.
+ * 
+ * @author IT Mill Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+public class PaintException extends IOException {
+
+    /**
+     * Serial generated by eclipse.
+     */
+    private static final long serialVersionUID = 3762535607221891897L;
+
+    /**
+     * Constructs an instance of <code>PaintExeception</code> with the
+     * specified detail message.
+     * 
+     * @param msg
+     *                the detail message.
+     */
+    public PaintException(String msg) {
+        super(msg);
+    }
+
+    /**
+     * Constructs an instance of <code>PaintExeception</code> from
+     * IOException.
+     * 
+     * @param exception
+     *                the original exception.
+     */
+    public PaintException(IOException exception) {
+        super(exception.getMessage());
+    }
+}
diff --git a/src/com/itmill/toolkit/terminal/terminal/PaintTarget.java b/src/com/itmill/toolkit/terminal/terminal/PaintTarget.java
new file mode 100644 (file)
index 0000000..a624074
--- /dev/null
@@ -0,0 +1,380 @@
+/* 
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.itmill.toolkit.terminal;
+
+/**
+ * This interface defines the methods for painting XML to the UIDL stream.
+ * 
+ * @author IT Mill Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+public interface PaintTarget {
+
+    /**
+     * Prints single XMLsection.
+     * 
+     * Prints full XML section. The section data is escaped from XML tags and
+     * surrounded by XML start and end-tags.
+     * 
+     * @param sectionTagName
+     *                the name of the tag.
+     * @param sectionData
+     *                the scetion data.
+     * @throws PaintException
+     *                 if the paint operation failed.
+     */
+    public void addSection(String sectionTagName, String sectionData)
+            throws PaintException;
+
+    /**
+     * Prints element start tag of a paintable section. Starts a paintable
+     * section using the given tag. The PaintTarget may implement a caching
+     * scheme, that checks the paintable has actually changed or can a cached
+     * version be used instead. This method should call the startTag method.
+     * <p>
+     * If the Paintable is found in cache and this function returns true it may
+     * omit the content and close the tag, in which case cached content should
+     * be used.
+     * </p>
+     * 
+     * @param paintable
+     *                the paintable to start.
+     * @param tag
+     *                the name of the start tag.
+     * @return <code>true</code> if paintable found in cache,
+     *         <code>false</code> otherwise.
+     * @throws PaintException
+     *                 if the paint operation failed.
+     * @see #startTag(String)
+     * @since 3.1
+     */
+    public boolean startTag(Paintable paintable, String tag)
+            throws PaintException;
+
+    /**
+     * Paints a component reference as an attribute to current tag. This method
+     * is meant to enable component interactions on client side. With reference
+     * the client side component can communicate directly to other component.
+     * 
+     * Note! This is still an experimental feature and API is likely to change
+     * in future.
+     * 
+     * @param paintable
+     *                the Paintable to reference
+     * @param referenceName
+     * @throws PaintException
+     * 
+     * @since 5.2
+     */
+    public void paintReference(Paintable paintable, String referenceName)
+            throws PaintException;
+
+    /**
+     * Prints element start tag.
+     * 
+     * <pre>
+     * Todo:
+     * Checking of input values
+     * </pre>
+     * 
+     * @param tagName
+     *                the name of the start tag.
+     * @throws PaintException
+     *                 if the paint operation failed.
+     */
+    public void startTag(String tagName) throws PaintException;
+
+    /**
+     * Prints element end tag.
+     * 
+     * If the parent tag is closed before every child tag is closed an
+     * PaintException is raised.
+     * 
+     * @param tagName
+     *                the name of the end tag.
+     * @throws PaintException
+     *                 if the paint operation failed.
+     */
+    public void endTag(String tagName) throws PaintException;
+
+    /**
+     * Adds a boolean attribute to component. Atributes must be added before any
+     * content is written.
+     * 
+     * @param name
+     *                the Attribute name.
+     * @param value
+     *                the Attribute value.
+     * 
+     * @throws PaintException
+     *                 if the paint operation failed.
+     */
+    public void addAttribute(String name, boolean value) throws PaintException;
+
+    /**
+     * Adds a integer attribute to component. Atributes must be added before any
+     * content is written.
+     * 
+     * @param name
+     *                the Attribute name.
+     * @param value
+     *                the Attribute value.
+     * 
+     * @throws PaintException
+     *                 if the paint operation failed.
+     */
+    public void addAttribute(String name, int value) throws PaintException;
+
+    /**
+     * Adds a resource attribute to component. Atributes must be added before
+     * any content is written.
+     * 
+     * @param name
+     *                the Attribute name
+     * @param value
+     *                the Attribute value
+     * 
+     * @throws PaintException
+     *                 if the paint operation failed.
+     */
+    public void addAttribute(String name, Resource value) throws PaintException;
+
+    /**
+     * Adds a long attribute to component. Atributes must be added before any
+     * content is written.
+     * 
+     * @param name
+     *                the Attribute name.
+     * @param value
+     *                the Attribute value.
+     * 
+     * @throws PaintException
+     *                 if the paint operation failed.
+     */
+    public void addAttribute(String name, long value) throws PaintException;
+
+    /**
+     * Adds a float attribute to component. Atributes must be added before any
+     * content is written.
+     * 
+     * @param name
+     *                the Attribute name.
+     * @param value
+     *                the Attribute value.
+     * 
+     * @throws PaintException
+     *                 if the paint operation failed.
+     */
+    public void addAttribute(String name, float value) throws PaintException;
+
+    /**
+     * Adds a double attribute to component. Atributes must be added before any
+     * content is written.
+     * 
+     * @param name
+     *                the Attribute name.
+     * @param value
+     *                the Attribute value.
+     * 
+     * @throws PaintException
+     *                 if the paint operation failed.
+     */
+    public void addAttribute(String name, double value) throws PaintException;
+
+    /**
+     * Adds a string attribute to component. Atributes must be added before any
+     * content is written.
+     * 
+     * @param name
+     *                the Boolean attribute name.
+     * @param value
+     *                the Boolean attribute value.
+     * 
+     * @throws PaintException
+     *                 if the paint operation failed.
+     */
+    public void addAttribute(String name, String value) throws PaintException;
+
+    /**
+     * Adds a string type variable.
+     * 
+     * @param owner
+     *                the Listener for variable changes.
+     * @param name
+     *                the Variable name.
+     * @param value
+     *                the Variable initial value.
+     * 
+     * @throws PaintException
+     *                 if the paint operation failed.
+     */
+    public void addVariable(VariableOwner owner, String name, String value)
+            throws PaintException;
+
+    /**
+     * Adds a int type variable.
+     * 
+     * @param owner
+     *                the Listener for variable changes.
+     * @param name
+     *                the Variable name.
+     * @param value
+     *                the Variable initial value.
+     * 
+     * @throws PaintException
+     *                 if the paint operation failed.
+     */
+    public void addVariable(VariableOwner owner, String name, int value)
+            throws PaintException;
+
+    /**
+     * Adds a long type variable.
+     * 
+     * @param owner
+     *                the Listener for variable changes.
+     * @param name
+     *                the Variable name.
+     * @param value
+     *                the Variable initial value.
+     * 
+     * @throws PaintException
+     *                 if the paint operation failed.
+     */
+    public void addVariable(VariableOwner owner, String name, long value)
+            throws PaintException;
+
+    /**
+     * Adds a float type variable.
+     * 
+     * @param owner
+     *                the Listener for variable changes.
+     * @param name
+     *                the Variable name.
+     * @param value
+     *                the Variable initial value.
+     * 
+     * @throws PaintException
+     *                 if the paint operation failed.
+     */
+    public void addVariable(VariableOwner owner, String name, float value)
+            throws PaintException;
+
+    /**
+     * Adds a double type variable.
+     * 
+     * @param owner
+     *                the Listener for variable changes.
+     * @param name
+     *                the Variable name.
+     * @param value
+     *                the Variable initial value.
+     * 
+     * @throws PaintException
+     *                 if the paint operation failed.
+     */
+    public void addVariable(VariableOwner owner, String name, double value)
+            throws PaintException;
+
+    /**
+     * Adds a boolean type variable.
+     * 
+     * @param owner
+     *                the Listener for variable changes.
+     * @param name
+     *                the Variable name.
+     * @param value
+     *                the Variable initial value.
+     * 
+     * @throws PaintException
+     *                 if the paint operation failed.
+     */
+    public void addVariable(VariableOwner owner, String name, boolean value)
+            throws PaintException;
+
+    /**
+     * Adds a string array type variable.
+     * 
+     * @param owner
+     *                the Listener for variable changes.
+     * @param name
+     *                the Variable name.
+     * @param value
+     *                the Variable initial value.
+     * 
+     * @throws PaintException
+     *                 if the paint operation failed.
+     */
+    public void addVariable(VariableOwner owner, String name, String[] value)
+            throws PaintException;
+
+    /**
+     * Adds a upload stream type variable.
+     * 
+     * @param owner
+     *                the Listener for variable changes.
+     * @param name
+     *                the Variable name.
+     * 
+     * @throws PaintException
+     *                 if the paint operation failed.
+     */
+    public void addUploadStreamVariable(VariableOwner owner, String name)
+            throws PaintException;
+
+    /**
+     * Prints single XML section.
+     * <p>
+     * Prints full XML section. The section data must be XML and it is
+     * surrounded by XML start and end-tags.
+     * </p>
+     * 
+     * @param sectionTagName
+     *                the tag name.
+     * @param sectionData
+     *                the section data to be printed.
+     * @param namespace
+     *                the namespace.
+     * @throws PaintException
+     *                 if the paint operation failed.
+     */
+    public void addXMLSection(String sectionTagName, String sectionData,
+            String namespace) throws PaintException;
+
+    /**
+     * Adds UIDL directly. The UIDL must be valid in accordance with the
+     * UIDL.dtd
+     * 
+     * @param uidl
+     *                the UIDL to be added.
+     * @throws PaintException
+     *                 if the paint operation failed.
+     */
+    public void addUIDL(java.lang.String uidl) throws PaintException;
+
+    /**
+     * Adds text node. All the contents of the text are XML-escaped.
+     * 
+     * @param text
+     *                the Text to add
+     * @throws PaintException
+     *                 if the paint operation failed.
+     */
+    void addText(String text) throws PaintException;
+
+    /**
+     * Adds CDATA node to target UIDL-tree.
+     * 
+     * @param text
+     *                the Character data to add
+     * @throws PaintException
+     *                 if the paint operation failed.
+     * @since 3.1
+     */
+    void addCharacterData(String text) throws PaintException;
+
+    public void addAttribute(String string, Object[] keys);
+}
diff --git a/src/com/itmill/toolkit/terminal/terminal/Paintable.java b/src/com/itmill/toolkit/terminal/terminal/Paintable.java
new file mode 100644 (file)
index 0000000..687199c
--- /dev/null
@@ -0,0 +1,146 @@
+/* 
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.itmill.toolkit.terminal;
+
+import java.util.EventObject;
+
+/**
+ * Interface implemented by all classes that can be painted. Classes
+ * implementing this interface know how to output themselves to a UIDL stream
+ * and that way describing to the terminal how it should be displayed in the UI.
+ * 
+ * @author IT Mill Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+public interface Paintable extends java.util.EventListener {
+
+    /**
+     * <p>
+     * Paints the Paintable into a UIDL stream. This method creates the UIDL
+     * sequence describing it and outputs it to the given UIDL stream.
+     * </p>
+     * 
+     * <p>
+     * It is called when the contents of the component should be painted in
+     * response to the component first being shown or having been altered so
+     * that its visual representation is changed.
+     * </p>
+     * 
+     * @param target
+     *                the target UIDL stream where the component should paint
+     *                itself to.
+     * @throws PaintException
+     *                 if the paint operation failed.
+     */
+    public void paint(PaintTarget target) throws PaintException;
+
+    /**
+     * Requests that the paintable should be repainted as soon as possible.
+     */
+    public void requestRepaint();
+
+    /**
+     * Adds an unique id for component that get's transferred to terminal for
+     * testing purposes. Keeping identifiers unique throughout the Application
+     * instance is on programmers responsibility.
+     * 
+     * @param id
+     *                A short (< 20 chars) alphanumeric id
+     */
+    public void setDebugId(String id);
+
+    /**
+     * Get's currently set debug identifier
+     * 
+     * @return current debug id, null if not set
+     */
+    public String getDebugId();
+
+    /**
+     * Repaint request event is thrown when the paintable needs to be repainted.
+     * This is typically done when the <code>paint</code> method would return
+     * dissimilar UIDL from the previous call of the method.
+     */
+    public class RepaintRequestEvent extends EventObject {
+
+        /**
+         * Serial generated by eclipse.
+         */
+        private static final long serialVersionUID = 3256725095530442805L;
+
+        /**
+         * Constructs a new event.
+         * 
+         * @param source
+         *                the paintable needing repaint.
+         */
+        public RepaintRequestEvent(Paintable source) {
+            super(source);
+        }
+
+        /**
+         * Gets the paintable needing repainting.
+         * 
+         * @return Paintable for which the <code>paint</code> method will
+         *         return dissimilar UIDL from the previous call of the method.
+         */
+        public Paintable getPaintable() {
+            return (Paintable) getSource();
+        }
+    }
+
+    /**
+     * Listens repaint requests. The <code>repaintRequested</code> method is
+     * called when the paintable needs to be repainted. This is typically done
+     * when the <code>paint</code> method would return dissimilar UIDL from
+     * the previous call of the method.
+     */
+    public interface RepaintRequestListener {
+
+        /**
+         * Receives repaint request events.
+         * 
+         * @param event
+         *                the repaint request event specifying the paintable
+         *                source.
+         */
+        public void repaintRequested(RepaintRequestEvent event);
+    }
+
+    /**
+     * Adds repaint request listener. In order to assure that no repaint
+     * requests are missed, the new repaint listener should paint the paintable
+     * right after adding itself as listener.
+     * 
+     * @param listener
+     *                the listener to be added.
+     */
+    public void addListener(RepaintRequestListener listener);
+
+    /**
+     * Removes repaint request listener.
+     * 
+     * @param listener
+     *                the listener to be removed.
+     */
+    public void removeListener(RepaintRequestListener listener);
+
+    /**
+     * Request sending of repaint events on any further visible changes.
+     * Normally the paintable only send up to one repaint request for listeners
+     * after paint as the paintable as the paintable assumes that the listeners
+     * already know about the repaint need. This method resets the assumtion.
+     * Paint implicitly does the assumtion reset functionality implemented by
+     * this method.
+     * <p>
+     * This method is normally used only by the terminals to note paintables
+     * about implicit repaints (painting the component without actually invoking
+     * paint method).
+     * </p>
+     */
+    public void requestRepaintRequests();
+}
diff --git a/src/com/itmill/toolkit/terminal/terminal/ParameterHandler.java b/src/com/itmill/toolkit/terminal/terminal/ParameterHandler.java
new file mode 100644 (file)
index 0000000..fb8c86e
--- /dev/null
@@ -0,0 +1,57 @@
+/* 
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.itmill.toolkit.terminal;
+
+import java.util.Map;
+
+/**
+ * Interface implemented by all the classes capable of handling external
+ * parameters.
+ * 
+ * <p>
+ * Some terminals can provide external parameters for application. For example
+ * GET and POST parameters are passed to application as external parameters on
+ * Web Adapter. The parameters can be received at any time during the
+ * application lifecycle. All the parameter handlers implementing this interface
+ * and registered to {@link com.itmill.toolkit.ui.Window} receive all the
+ * parameters got from the terminal in the given window.
+ * </p>
+ * 
+ * @author IT Mill Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+public interface ParameterHandler {
+
+    /**
+     * <p>
+     * Handles the given parameters. The parameters are given as inmodifieable
+     * name to value map. All parameters names are of type:
+     * {@link java.lang.String}. All the parameter values are arrays of
+     * strings.
+     * </p>
+     * 
+     * @param parameters
+     *                the Inmodifiable name to value[] mapping.
+     * 
+     */
+    public void handleParameters(Map parameters);
+
+    /**
+     * ParameterHandler error event.
+     */
+    public interface ErrorEvent extends Terminal.ErrorEvent {
+
+        /**
+         * Gets the source ParameterHandler.
+         * 
+         * @return the source Parameter Handler.
+         */
+        public ParameterHandler getParameterHandler();
+
+    }
+
+}
diff --git a/src/com/itmill/toolkit/terminal/terminal/Resource.java b/src/com/itmill/toolkit/terminal/terminal/Resource.java
new file mode 100644 (file)
index 0000000..5cf0772
--- /dev/null
@@ -0,0 +1,24 @@
+/* 
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.itmill.toolkit.terminal;
+
+/**
+ * <code>Resource</code> provided to the client terminal. Support for actually
+ * displaying the resource type is left to the terminal.
+ * 
+ * @author IT Mill Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+public interface Resource {
+
+    /**
+     * Gets the MIME type of the resource.
+     * 
+     * @return the MIME type of the resource.
+     */
+    public String getMIMEType();
+}
diff --git a/src/com/itmill/toolkit/terminal/terminal/Scrollable.java b/src/com/itmill/toolkit/terminal/terminal/Scrollable.java
new file mode 100644 (file)
index 0000000..e1301d7
--- /dev/null
@@ -0,0 +1,96 @@
+/* 
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.itmill.toolkit.terminal;
+
+/**
+ * <p>
+ * This interface is implemented by all visual objects that can be scrolled. The
+ * unit of scrolling is pixel.
+ * </p>
+ * 
+ * @author IT Mill Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+public interface Scrollable {
+
+    /**
+     * Gets scroll left offset.
+     * 
+     * <p>
+     * Scrolling offset is the number of pixels this scrollable has been
+     * scrolled right.
+     * </p>
+     * 
+     * @return Horizontal scrolling position in pixels.
+     */
+    public int getScrollLeft();
+
+    /**
+     * Sets scroll left offset.
+     * 
+     * <p>
+     * Scrolling offset is the number of pixels this scrollable has been
+     * scrolled right.
+     * </p>
+     * 
+     * @param pixelsScrolled
+     *                the xOffset.
+     */
+    public void setScrollLeft(int pixelsScrolled);
+
+    /**
+     * Gets scroll top offset.
+     * 
+     * <p>
+     * Scrolling offset is the number of pixels this scrollable has been
+     * scrolled down.
+     * </p>
+     * 
+     * @return Vertical scrolling position in pixels.
+     */
+    public int getScrollTop();
+
+    /**
+     * Sets scroll top offset.
+     * 
+     * <p>
+     * Scrolling offset is the number of pixels this scrollable has been
+     * scrolled down.
+     * </p>
+     * 
+     * @param pixelsScrolled
+     *                the yOffset.
+     */
+    public void setScrollTop(int pixelsScrolled);
+
+    /**
+     * Is the scrolling enabled.
+     * 
+     * <p>
+     * Enabling scrolling allows the user to scroll the scrollable view
+     * interactively
+     * </p>
+     * 
+     * @return <code>true</code> if the scrolling is allowed, otherwise
+     *         <code>false</code>.
+     */
+    public boolean isScrollable();
+
+    /**
+     * Enables or disables scrolling..
+     * 
+     * <p>
+     * Enabling scrolling allows the user to scroll the scrollable view
+     * interactively
+     * </p>
+     * 
+     * @param isScrollingEnabled
+     *                true if the scrolling is allowed.
+     */
+    public void setScrollable(boolean isScrollingEnabled);
+
+}
diff --git a/src/com/itmill/toolkit/terminal/terminal/Sizeable.java b/src/com/itmill/toolkit/terminal/terminal/Sizeable.java
new file mode 100644 (file)
index 0000000..4d467d7
--- /dev/null
@@ -0,0 +1,199 @@
+/* 
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.itmill.toolkit.terminal;
+
+/**
+ * Interface to be implemented by components wishing to display some object that
+ * may be dynamically resized during runtime.
+ * 
+ * @author IT Mill Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+public interface Sizeable {
+
+    /**
+     * Unit code representing pixels.
+     */
+    public static final int UNITS_PIXELS = 0;
+
+    /**
+     * Unit code representing points (1/72nd of an inch).
+     */
+    public static final int UNITS_POINTS = 1;
+
+    /**
+     * Unit code representing picas (12 points).
+     */
+    public static final int UNITS_PICAS = 2;
+
+    /**
+     * Unit code representing the font-size of the relevant font.
+     */
+    public static final int UNITS_EM = 3;
+
+    /**
+     * Unit code representing the x-height of the relevant font.
+     */
+    public static final int UNITS_EX = 4;
+
+    /**
+     * Unit code representing millimeters.
+     */
+    public static final int UNITS_MM = 5;
+
+    /**
+     * Unit code representing centimeters.
+     */
+    public static final int UNITS_CM = 6;
+
+    /**
+     * Unit code representing inches.
+     */
+    public static final int UNITS_INCH = 7;
+
+    /**
+     * Unit code representing in percentage of the containing element defined by
+     * terminal.
+     */
+    public static final int UNITS_PERCENTAGE = 8;
+
+    /**
+     * Unit code representing in rows of text. This unit is only applicaple to
+     * some components can it's meaning is specified by component
+     * implementation.
+     */
+    public static final int UNITS_ROWS = 9;
+
+    public static final int SIZE_UNDEFINED = -1;
+
+    /**
+     * Textual representations of units symbols. Supported units and their
+     * symbols are:
+     * <ul>
+     * <li><code>UNITS_PIXELS</code>: "px"</li>
+     * <li><code>UNITS_POINTS</code>: "pt"</li>
+     * <li><code>UNITS_PICAS</code>: "pc"</li>
+     * <li><code>UNITS_EM</code>: "em"</li>
+     * <li><code>UNITS_EX</code>: "ex"</li>
+     * <li><code>UNITS_MM</code>: "mm"</li>
+     * <li><code>UNITS_CM</code>. "cm"</li>
+     * <li><code>UNITS_INCH</code>: "in"</li>
+     * <li><code>UNITS_PERCENTAGE</code>: "%"</li>
+     * <li><code>UNITS_ROWS</code>: "rows"</li>
+     * </ul>
+     * These can be used like <code>Sizeable.UNIT_SYMBOLS[UNITS_PIXELS]</code>.
+     */
+    public static final String[] UNIT_SYMBOLS = { "px", "pt", "pc", "em", "ex",
+            "mm", "cm", "in", "%", "rows" };
+
+    /**
+     * Gets the width of the object. Negative number implies unspecified size
+     * (terminal is free to set the size).
+     * 
+     * @return width of the object in units specified by widthUnits property.
+     */
+    public int getWidth();
+
+    /**
+     * Sets the width of the object. Negative number implies unspecified size
+     * (terminal is free to set the size).
+     * 
+     * @param width
+     *                the width of the object in units specified by widthUnits
+     *                property.
+     */
+    public void setWidth(int width);
+
+    /**
+     * Gets the height of the object. Negative number implies unspecified size
+     * (terminal is free to set the size).
+     * 
+     * @return height of the object in units specified by heightUnits property.
+     */
+    public int getHeight();
+
+    /**
+     * Sets the height of the object. Negative number implies unspecified size
+     * (terminal is free to set the size).
+     * 
+     * @param height
+     *                the height of the object in units specified by heightUnits
+     *                property.
+     */
+    public void setHeight(int height);
+
+    /**
+     * Gets the width property units.
+     * 
+     * @return units used in width property.
+     */
+    public int getWidthUnits();
+
+    /**
+     * Sets the width property units.
+     * 
+     * @param units
+     *                the units used in width property.
+     */
+    public void setWidthUnits(int units);
+
+    /**
+     * Gets the height property units.
+     * 
+     * @return units used in height property.
+     */
+    public int getHeightUnits();
+
+    /**
+     * Sets the height property units.
+     * 
+     * @param units
+     *                the units used in height property.
+     */
+    public void setHeightUnits(int units);
+
+    /**
+     * Sets the height of the component using String presentation.
+     * 
+     * String presentation is similar to what is used in Cascading Style Sheets.
+     * Size can be length or percentage of available size.
+     * 
+     * See <a
+     * href="http://www.w3.org/TR/REC-CSS2/syndata.html#value-def-length">CSS
+     * spesification</a> for more details.
+     * 
+     * @param height
+     *                in CSS style string representation
+     */
+    public void setHeight(String height);
+
+    /**
+     * Sets the width of the component using String presentation.
+     * 
+     * String presentation is similar to what is used in Cascading Style Sheets.
+     * Size can be length or percentage of available size.
+     * 
+     * See <a
+     * href="http://www.w3.org/TR/REC-CSS2/syndata.html#value-def-length">CSS
+     * spesification</a> for more details.
+     * 
+     * @param width
+     *                in CSS style string representation
+     */
+    public void setWidth(String width);
+
+    /**
+     * Sets the size to 100% x 100%.
+     */
+    public void setSizeFull();
+
+    /**
+     * Clears any size settings.
+     */
+    public void setSizeUndefined();
+
+}
diff --git a/src/com/itmill/toolkit/terminal/terminal/StreamResource.java b/src/com/itmill/toolkit/terminal/terminal/StreamResource.java
new file mode 100644 (file)
index 0000000..08a39f4
--- /dev/null
@@ -0,0 +1,214 @@
+/* 
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.itmill.toolkit.terminal;
+
+import java.io.InputStream;
+
+import com.itmill.toolkit.Application;
+import com.itmill.toolkit.service.FileTypeResolver;
+
+/**
+ * <code>StreamResource</code> is a resource provided to the client directly
+ * by the application. The strean resource is fetched from URI that is most
+ * often in the context of the application or window. The resource is
+ * automatically registered to window in creation.
+ * 
+ * @author IT Mill Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+public class StreamResource implements ApplicationResource {
+
+    /**
+     * Source stream the downloaded content is fetched from.
+     */
+    private StreamSource streamSource = null;
+
+    /**
+     * Explicit mime-type.
+     */
+    private String MIMEType = null;
+
+    /**
+     * Filename.
+     */
+    private String filename;
+
+    /**
+     * Application.
+     */
+    private final Application application;
+
+    /**
+     * Default buffer size for this stream resource.
+     */
+    private int bufferSize = 0;
+
+    /**
+     * Default cache time for this stream resource.
+     */
+    private long cacheTime = DEFAULT_CACHETIME;
+
+    /**
+     * Creates a new stream resource for downloading from stream.
+     * 
+     * @param streamSource
+     *                the source Stream.
+     * @param filename
+     *                the name of the file.
+     * @param application
+     *                the Application object.
+     */
+    public StreamResource(StreamSource streamSource, String filename,
+            Application application) {
+
+        this.application = application;
+        setFilename(filename);
+        setStreamSource(streamSource);
+
+        // Register to application
+        application.addResource(this);
+
+    }
+
+    /**
+     * @see com.itmill.toolkit.terminal.Resource#getMIMEType()
+     */
+    public String getMIMEType() {
+        if (MIMEType != null) {
+            return MIMEType;
+        }
+        return FileTypeResolver.getMIMEType(filename);
+    }
+
+    /**
+     * Sets the mime type of the resource.
+     * 
+     * @param MIMEType
+     *                the MIME type to be set.
+     */
+    public void setMIMEType(String MIMEType) {
+        this.MIMEType = MIMEType;
+    }
+
+    /**
+     * Returns the source for this <code>StreamResource</code>. StreamSource
+     * is queried when the resource is about to be streamed to the client.
+     * 
+     * @return Source of the StreamResource.
+     */
+    public StreamSource getStreamSource() {
+        return streamSource;
+    }
+
+    /**
+     * Sets the source for this <code>StreamResource</code>.
+     * <code>StreamSource</code> is queried when the resource is about to be
+     * streamed to the client.
+     * 
+     * @param streamSource
+     *                the source to set.
+     */
+    public void setStreamSource(StreamSource streamSource) {
+        this.streamSource = streamSource;
+    }
+
+    /**
+     * Gets the filename.
+     * 
+     * @return the filename.
+     */
+    public String getFilename() {
+        return filename;
+    }
+
+    /**
+     * Sets the filename.
+     * 
+     * @param filename
+     *                the filename to set.
+     */
+    public void setFilename(String filename) {
+        this.filename = filename;
+    }
+
+    /**
+     * @see com.itmill.toolkit.terminal.ApplicationResource#getApplication()
+     */
+    public Application getApplication() {
+        return application;
+    }
+
+    /**
+     * @see com.itmill.toolkit.terminal.ApplicationResource#getStream()
+     */
+    public DownloadStream getStream() {
+        final StreamSource ss = getStreamSource();
+        if (ss == null) {
+            return null;
+        }
+        final DownloadStream ds = new DownloadStream(ss.getStream(),
+                getMIMEType(), getFilename());
+        ds.setBufferSize(getBufferSize());
+        ds.setCacheTime(cacheTime);
+        return ds;
+    }
+
+    /**
+     * Interface implemented by the source of a StreamResource.
+     * 
+     * @author IT Mill Ltd.
+     * @version
+     * @VERSION@
+     * @since 3.0
+     */
+    public interface StreamSource {
+
+        /**
+         * Returns new input stream that is used for reading the resource.
+         */
+        public InputStream getStream();
+    }
+
+    /* documented in superclass */
+    public int getBufferSize() {
+        return bufferSize;
+    }
+
+    /**
+     * Sets the size of the download buffer used for this resource.
+     * 
+     * @param bufferSize
+     *                the size of the buffer in bytes.
+     */
+    public void setBufferSize(int bufferSize) {
+        this.bufferSize = bufferSize;
+    }
+
+    /* documented in superclass */
+    public long getCacheTime() {
+        return cacheTime;
+    }
+
+    /**
+     * Sets the length of cache expiration time.
+     * 
+     * <p>
+     * This gives the adapter the possibility cache streams sent to the client.
+     * The caching may be made in adapter or at the client if the client
+     * supports caching. Zero or negavive value disbales the caching of this
+     * stream.
+     * </p>
+     * 
+     * @param cacheTime
+     *                the cache time in milliseconds.
+     * 
+     */
+    public void setCacheTime(long cacheTime) {
+        this.cacheTime = cacheTime;
+    }
+
+}
diff --git a/src/com/itmill/toolkit/terminal/terminal/SystemError.java b/src/com/itmill/toolkit/terminal/terminal/SystemError.java
new file mode 100644 (file)
index 0000000..d82aa4d
--- /dev/null
@@ -0,0 +1,135 @@
+/* 
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.itmill.toolkit.terminal;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+
+/**
+ * <code>SystemError</code> is a runtime exception caused by error in system.
+ * The system error can be shown to the user as it implements
+ * <code>ErrorMessage</code> interface, but contains technical information
+ * such as stack trace and exception.
+ * 
+ * @author IT Mill Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+public class SystemError extends RuntimeException implements ErrorMessage {
+
+    /**
+     * Serial generated by eclipse.
+     */
+    private static final long serialVersionUID = 3256445789512675891L;
+
+    /**
+     * The cause of the system error. The cause is stored separately as JDK 1.3
+     * does not support causes natively.
+     */
+    private Throwable cause = null;
+
+    /**
+     * Constructor for SystemError with error message specified.
+     * 
+     * @param message
+     *                the Textual error description.
+     */
+    public SystemError(String message) {
+        super(message);
+    }
+
+    /**
+     * Constructor for SystemError with causing exception and error message.
+     * 
+     * @param message
+     *                the Textual error description.
+     * @param cause
+     *                the throwable causing the system error.
+     */
+    public SystemError(String message, Throwable cause) {
+        super(message);
+        this.cause = cause;
+    }
+
+    /**
+     * Constructor for SystemError with cause.
+     * 
+     * @param cause
+     *                the throwable causing the system error.
+     */
+    public SystemError(Throwable cause) {
+        this.cause = cause;
+    }
+
+    /**
+     * @see com.itmill.toolkit.terminal.ErrorMessage#getErrorLevel()
+     */
+    public final int getErrorLevel() {
+        return ErrorMessage.SYSTEMERROR;
+    }
+
+    /**
+     * @see com.itmill.toolkit.terminal.Paintable#paint(com.itmill.toolkit.terminal.PaintTarget)
+     */
+    public void paint(PaintTarget target) throws PaintException {
+
+        target.startTag("error");
+        target.addAttribute("level", "system");
+
+        // Paint the error message
+        final String message = getLocalizedMessage();
+        if (message != null) {
+            target.addSection("h2", message);
+        }
+
+        // Paint the exception
+        if (cause != null) {
+            target.addSection("h3", "Exception");
+            final StringWriter buffer = new StringWriter();
+            cause.printStackTrace(new PrintWriter(buffer));
+            target.addSection("pre", buffer.toString());
+        }
+
+        target.endTag("error");
+
+    }
+
+    /**
+     * Gets cause for the error.
+     * 
+     * @return the cause.
+     * @see java.lang.Throwable#getCause()
+     */
+    public Throwable getCause() {
+        return cause;
+    }
+
+    /* Documented in super interface */
+    public void addListener(RepaintRequestListener listener) {
+    }
+
+    /* Documented in super interface */
+    public void removeListener(RepaintRequestListener listener) {
+    }
+
+    /* Documented in super interface */
+    public void requestRepaint() {
+    }
+
+    /* Documented in super interface */
+    public void requestRepaintRequests() {
+    }
+
+    public String getDebugId() {
+        return null;
+    }
+
+    public void setDebugId(String id) {
+        throw new UnsupportedOperationException(
+                "Setting testing id for this Paintable is not implemented");
+    }
+
+}
diff --git a/src/com/itmill/toolkit/terminal/terminal/Terminal.java b/src/com/itmill/toolkit/terminal/terminal/Terminal.java
new file mode 100644 (file)
index 0000000..573130d
--- /dev/null
@@ -0,0 +1,63 @@
+/* 
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.itmill.toolkit.terminal;
+
+/**
+ * Interface for different terminal types.
+ * 
+ * @author IT Mill Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+public interface Terminal {
+
+    /**
+     * Gets the name of the default theme.
+     * 
+     * @return the Name of the terminal window.
+     */
+    public String getDefaultTheme();
+
+    /**
+     * Gets the width of the terminal window in pixels.
+     * 
+     * @return the Width of the terminal window.
+     */
+    public int getScreenWidth();
+
+    /**
+     * Gets the height of the terminal window in pixels.
+     * 
+     * @return the Height of the terminal window.
+     */
+    public int getScreenHeight();
+
+    /**
+     * Terminal error event.
+     */
+    public interface ErrorEvent {
+
+        /**
+         * Gets the contained throwable.
+         */
+        public Throwable getThrowable();
+
+    }
+
+    /**
+     * Terminal error listener interface.
+     */
+    public interface ErrorListener {
+
+        /**
+         * Invoked when terminal error occurs.
+         * 
+         * @param event
+         *                the fired event.
+         */
+        public void terminalError(Terminal.ErrorEvent event);
+    }
+}
diff --git a/src/com/itmill/toolkit/terminal/terminal/ThemeResource.java b/src/com/itmill/toolkit/terminal/terminal/ThemeResource.java
new file mode 100644 (file)
index 0000000..1b9995d
--- /dev/null
@@ -0,0 +1,91 @@
+/* 
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.itmill.toolkit.terminal;
+
+import com.itmill.toolkit.service.FileTypeResolver;
+
+/**
+ * <code>ThemeResource</code> is a named theme dependant resource provided and
+ * managed by a theme. The actual resource contents are dynamically resolved to
+ * comply with the used theme by the terminal adapter. This is commonly used to
+ * provide static images, flash, java-applets, etc for the terminals.
+ * 
+ * @author IT Mill Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+public class ThemeResource implements Resource {
+
+    /**
+     * Id of the terminal managed resource.
+     */
+    private String resourceID = null;
+
+    /**
+     * Creates a resource.
+     * 
+     * @param resourceId
+     *                the Id of the resource.
+     */
+    public ThemeResource(String resourceId) {
+        if (resourceId == null) {
+            throw new NullPointerException("Resource ID must not be null");
+        }
+        if (resourceId.length() == 0) {
+            throw new IllegalArgumentException("Resource ID can not be empty");
+        }
+        if (resourceId.charAt(0) == '/') {
+            throw new IllegalArgumentException(
+                    "Resource ID must be relative (can not begin with /)");
+        }
+
+        resourceID = resourceId;
+    }
+
+    /**
+     * Tests if the given object equals this Resource.
+     * 
+     * @param obj
+     *                the object to be tested for equality.
+     * @return <code>true</code> if the given object equals this Icon,
+     *         <code>false</code> if not.
+     * @see java.lang.Object#equals(Object)
+     */
+    public boolean equals(Object obj) {
+        return obj instanceof ThemeResource
+                && resourceID.equals(((ThemeResource) obj).resourceID);
+    }
+
+    /**
+     * @see java.lang.Object#hashCode()
+     */
+    public int hashCode() {
+        return resourceID.hashCode();
+    }
+
+    /**
+     * @see java.lang.Object#toString()
+     */
+    public String toString() {
+        return resourceID.toString();
+    }
+
+    /**
+     * Gets the resource id.
+     * 
+     * @return the resource id.
+     */
+    public String getResourceId() {
+        return resourceID;
+    }
+
+    /**
+     * @see com.itmill.toolkit.terminal.Resource#getMIMEType()
+     */
+    public String getMIMEType() {
+        return FileTypeResolver.getMIMEType(getResourceId());
+    }
+}
diff --git a/src/com/itmill/toolkit/terminal/terminal/URIHandler.java b/src/com/itmill/toolkit/terminal/terminal/URIHandler.java
new file mode 100644 (file)
index 0000000..ef1fc76
--- /dev/null
@@ -0,0 +1,50 @@
+/* 
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.itmill.toolkit.terminal;
+
+import java.net.URL;
+
+/**
+ * Interface implemented by all the classes capable of handling URI:s.
+ * 
+ * <p>
+ * <code>URIHandler</code> can provide <code>DownloadStream</code> for
+ * transferring data for client.
+ * </p>
+ * 
+ * @author IT Mill Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+public interface URIHandler {
+
+    /**
+     * Handles a given relative URI. If the URI handling wants to emit a
+     * downloadable stream it can return download stream object. If no emitting
+     * stream is necessary, null should be returned instead.
+     * 
+     * @param context
+     *                the URl.
+     * @param relativeUri
+     *                the relative uri.
+     * @return the download stream object.
+     */
+    public DownloadStream handleURI(URL context, String relativeUri);
+
+    /**
+     * URIHandler error event.
+     */
+    public interface ErrorEvent extends Terminal.ErrorEvent {
+
+        /**
+         * Gets the source URIHandler.
+         * 
+         * @return the URIHandler.
+         */
+        public URIHandler getURIHandler();
+
+    }
+}
diff --git a/src/com/itmill/toolkit/terminal/terminal/UploadStream.java b/src/com/itmill/toolkit/terminal/terminal/UploadStream.java
new file mode 100644 (file)
index 0000000..b15bb1d
--- /dev/null
@@ -0,0 +1,49 @@
+/* 
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.itmill.toolkit.terminal;
+
+import java.io.InputStream;
+
+/**
+ * Defines a variable type, that is used for passing uploaded files from
+ * terminal. Most often, file upload is implented using the
+ * {@link com.itmill.toolkit.ui.Upload Upload} component.
+ * 
+ * @author IT Mill Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+public interface UploadStream {
+
+    /**
+     * Gets the name of the stream.
+     * 
+     * @return the name of the stream.
+     */
+    public String getStreamName();
+
+    /**
+     * Gets the input stream.
+     * 
+     * @return the Input stream.
+     */
+    public InputStream getStream();
+
+    /**
+     * Gets the input stream content type.
+     * 
+     * @return the content type of the input stream.
+     */
+    public String getContentType();
+
+    /**
+     * Gets stream content name. Stream content name usually differs from the
+     * actual stream name. It is used to identify the content of the stream.
+     * 
+     * @return the Name of the stream content.
+     */
+    public String getContentName();
+}
diff --git a/src/com/itmill/toolkit/terminal/terminal/UserError.java b/src/com/itmill/toolkit/terminal/terminal/UserError.java
new file mode 100644 (file)
index 0000000..182867b
--- /dev/null
@@ -0,0 +1,152 @@
+/* 
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.itmill.toolkit.terminal;
+
+/**
+ * <code>UserError</code> is a controlled error occurred in application. User
+ * errors are occur in normal usage of the application and guide the user.
+ * 
+ * @author IT Mill Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+public class UserError implements ErrorMessage {
+
+    /**
+     * Content mode, where the error contains only plain text.
+     */
+    public static final int CONTENT_TEXT = 0;
+
+    /**
+     * Content mode, where the error 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.
+     */
+    private int mode = CONTENT_TEXT;
+
+    /**
+     * Message in content mode.
+     */
+    private final String msg;
+
+    /**
+     * Error level.
+     */
+    private int level = ErrorMessage.ERROR;
+
+    /**
+     * Creates a textual error message of level ERROR.
+     * 
+     * @param textErrorMessage
+     *                the text of the error message.
+     */
+    public UserError(String textErrorMessage) {
+        msg = textErrorMessage;
+    }
+
+    /**
+     * Creates a error message with level and content mode.
+     * 
+     * @param message
+     *                the error message.
+     * @param contentMode
+     *                the content Mode.
+     * @param errorLevel
+     *                the level of error.
+     */
+    public UserError(String message, int contentMode, int errorLevel) {
+
+        // Check the parameters
+        if (contentMode < 0 || contentMode > 2) {
+            throw new java.lang.IllegalArgumentException(
+                    "Unsupported content mode: " + contentMode);
+        }
+
+        msg = message;
+        mode = contentMode;
+        level = errorLevel;
+    }
+
+    /* Documenten in interface */
+    public int getErrorLevel() {
+        return level;
+    }
+
+    /* Documenten in interface */
+    public void addListener(RepaintRequestListener listener) {
+    }
+
+    /* Documenten in interface */
+    public void removeListener(RepaintRequestListener listener) {
+    }
+
+    /* Documenten in interface */
+    public void requestRepaint() {
+    }
+
+    /* Documenten in interface */
+    public void paint(PaintTarget target) throws PaintException {
+
+        target.startTag("error");
+
+        // Error level
+        if (level >= ErrorMessage.SYSTEMERROR) {
+            target.addAttribute("level", "system");
+        } else if (level >= ErrorMessage.CRITICAL) {
+            target.addAttribute("level", "critical");
+        } else if (level >= ErrorMessage.ERROR) {
+            target.addAttribute("level", "error");
+        } else if (level >= ErrorMessage.WARNING) {
+            target.addAttribute("level", "warning");
+        } else {
+            target.addAttribute("level", "info");
+        }
+
+        // Paint the message
+        switch (mode) {
+        case CONTENT_TEXT:
+            target.addText(msg);
+            break;
+        case CONTENT_UIDL:
+            target.addUIDL(msg);
+            break;
+        case CONTENT_PREFORMATTED:
+            target.startTag("pre");
+            target.addText(msg);
+            target.endTag("pre");
+        }
+
+        target.endTag("error");
+    }
+
+    /* Documenten in interface */
+    public void requestRepaintRequests() {
+    }
+
+    /* Documented in superclass */
+    public String toString() {
+        return msg;
+    }
+
+    public String getDebugId() {
+        return null;
+    }
+
+    public void setDebugId(String id) {
+        throw new UnsupportedOperationException(
+                "Setting testing id for this Paintable is not implemented");
+    }
+
+}
diff --git a/src/com/itmill/toolkit/terminal/terminal/VariableOwner.java b/src/com/itmill/toolkit/terminal/terminal/VariableOwner.java
new file mode 100644 (file)
index 0000000..cdb3321
--- /dev/null
@@ -0,0 +1,81 @@
+/* 
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.itmill.toolkit.terminal;
+
+import java.util.Map;
+
+/**
+ * <p>
+ * Listener interface for UI variable changes. The user communicates with the
+ * application using the so-called <i>variables</i>. When the user makes a
+ * change using the UI the terminal trasmits the changed variables to the
+ * application, and the components owning those variables may then process those
+ * changes.
+ * </p>
+ * 
+ * @author IT Mill Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.0
+ */
+public interface VariableOwner {
+
+    /**
+     * Called when one or more variables handled by the implementing class are
+     * changed.
+     * 
+     * @param source
+     *                the Source of the variable change. This is the origin of
+     *                the event. For example in Web Adapter this is the request.
+     * @param variables
+     *                the Mapping from variable names to new variable values.
+     */
+    public void changeVariables(Object source, Map variables);
+
+    /**
+     * <p>
+     * Tests if the variable owner is enabled or not. The terminal should not
+     * send any variable changes to disabled variable owners.
+     * </p>
+     * 
+     * @return <code>true</code> if the variable owner is enabled,
+     *         <code>false</code> if not
+     */
+    public boolean isEnabled();
+
+    /**
+     * <p>
+     * Tests if the variable owner is in immediate mode or not. Being in
+     * immediate mode means that all variable changes are required to be sent
+     * back from the terminal immediately when they occur.
+     * </p>
+     * 
+     * <p>
+     * <strong>Note:</strong> <code>VariableOwner</code> does not include a
+     * set- method for the immediateness property. This is because not all
+     * VariableOwners wish to offer the functionality. Such VariableOwners are
+     * never in the immediate mode, thus they always return <code>false</code>
+     * in {@link #isImmediate()}.
+     * </p>
+     * 
+     * @return <code>true</code> if the component is in immediate mode,
+     *         <code>false</code> if not.
+     */
+    public boolean isImmediate();
+
+    /**
+     * VariableOwner error event.
+     */
+    public interface ErrorEvent extends Terminal.ErrorEvent {
+
+        /**
+         * Gets the source VariableOwner.
+         * 
+         * @return the variable owner.
+         */
+        public VariableOwner getVariableOwner();
+
+    }
+}
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/DefaultWidgetSet.gwt.xml b/src/com/itmill/toolkit/terminal/terminal/gwt/DefaultWidgetSet.gwt.xml
new file mode 100644 (file)
index 0000000..a226292
--- /dev/null
@@ -0,0 +1,7 @@
+<module>
+       <source path="client"/>\r
+\r
+       <!-- This module just defines a entrypoint for the DefaultWidgetSet -->
+       <inherits name="com.itmill.toolkit.terminal.gwt.DefaultWidgetSetNoEntry"/>
+       <entry-point class="com.itmill.toolkit.terminal.gwt.client.DefaultWidgetSet"/>
+</module>
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/DefaultWidgetSetNoEntry.gwt.xml b/src/com/itmill/toolkit/terminal/terminal/gwt/DefaultWidgetSetNoEntry.gwt.xml
new file mode 100644 (file)
index 0000000..7520019
--- /dev/null
@@ -0,0 +1,46 @@
+<module>
+       <!-- \r
+               This is the NoEntry version of DefaultWidgetSet.\r
+               This is the module you want to extend when creating an extended\r
+               widget set.\r
+       -->\r
+
+       <inherits name="com.google.gwt.user.User"/>
+
+       <inherits name="com.google.gwt.http.HTTP"/>
+
+       <inherits name="com.google.gwt.xml.XML"/>
+
+       <inherits name="com.google.gwt.json.JSON"/>\r
+
+       <source path="client"/>
+       
+       <source path="gwtwidgets"/>
+       
+       <!-- \r
+               Default theme for this widget set. \r
+               Please name the sub directory differently when creating a extended widget set\r
+               (e.g. src="mywidgets/styles.css") to avoid naming conflicts.\r
+       
+       <stylesheet src="default/common/common.css"/>
+       <stylesheet src="default/button/button.css"/>
+       <stylesheet src="default/textfield/textfield.css"/>
+       <stylesheet src="default/select/select.css"/>
+       <stylesheet src="default/panel/panel.css"/>
+       <stylesheet src="default/tabsheet/tabsheet.css"/>
+       <stylesheet src="default/datefield/datefield.css"/>
+       <stylesheet src="default/table/table.css"/>
+       <stylesheet src="default/slider/slider.css"/>
+       <stylesheet src="default/window/window.css"/>
+       <stylesheet src="default/window/notification.css"/>
+       <stylesheet src="default/caption/caption.css"/>
+       <stylesheet src="default/tree/tree.css"/>
+       <stylesheet src="default/splitpanel/splitpanel.css"/>
+       <stylesheet src="default/select/filterselect.css"/>
+       <stylesheet src="default/progressindicator/progressindicator.css"/>
+       <stylesheet src="default/expandlayout/expandlayout.css"/>
+       <stylesheet src="default/orderedlayout/orderedlayout.css"/>
+       <stylesheet src="default/accordion/accordion.css"/>
+        -->
+
+</module>
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/ApplicationConfiguration.java b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ApplicationConfiguration.java
new file mode 100644 (file)
index 0000000..aefc931
--- /dev/null
@@ -0,0 +1,78 @@
+package com.itmill.toolkit.terminal.gwt.client;
+
+import java.util.ArrayList;
+
+import com.google.gwt.core.client.JavaScriptObject;
+
+public class ApplicationConfiguration {
+
+    private String id;
+    private String themeUri;
+    private String pathInfo;
+    private String appUri;
+    private JavaScriptObject versionInfo;
+
+    public String getRootPanelId() {
+        return id;
+    }
+
+    public String getApplicationUri() {
+        return appUri;
+    }
+
+    public String getPathInfo() {
+        return pathInfo;
+    }
+
+    public String getThemeUri() {
+        return themeUri;
+    }
+
+    public void setAppId(String appId) {
+        id = appId;
+    }
+
+    public JavaScriptObject getVersionInfoJSObject() {
+        return versionInfo;
+    }
+
+    private native void loadFromDOM()
+    /*-{
+
+        var id = this.@com.itmill.toolkit.terminal.gwt.client.ApplicationConfiguration::id;
+        if($wnd.itmill.toolkitConfigurations && $wnd.itmill.toolkitConfigurations[id]) {
+            var jsobj = $wnd.itmill.toolkitConfigurations[id];
+            var uri = jsobj.appUri;
+            if(uri[uri.length -1] != "/") {
+                uri = uri + "/";
+            }
+            this.@com.itmill.toolkit.terminal.gwt.client.ApplicationConfiguration::appUri = uri;
+            this.@com.itmill.toolkit.terminal.gwt.client.ApplicationConfiguration::pathInfo = jsobj.pathInfo;
+            this.@com.itmill.toolkit.terminal.gwt.client.ApplicationConfiguration::themeUri = jsobj.themeUri;
+            if(jsobj.versionInfo) {
+                this.@com.itmill.toolkit.terminal.gwt.client.ApplicationConfiguration::versionInfo = jsobj.versionInfo;
+            }
+        
+        } else {
+            $wnd.alert("Toolkit app failed to initialize: " + this.id);
+        }
+     
+     }-*/;
+
+    public native static void loadAppIdListFromDOM(ArrayList list)
+    /*-{
+     
+         var j;
+         for(j in $wnd.itmill.toolkitConfigurations) {
+             list.@java.util.Collection::add(Ljava/lang/Object;)(j);
+         }
+     }-*/;
+
+    public static ApplicationConfiguration getConfigFromDOM(String appId) {
+        ApplicationConfiguration conf = new ApplicationConfiguration();
+        conf.setAppId(appId);
+        conf.loadFromDOM();
+        return conf;
+    }
+
+}
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/ApplicationConnection.java b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ApplicationConnection.java
new file mode 100755 (executable)
index 0000000..a1afdf7
--- /dev/null
@@ -0,0 +1,984 @@
+/* 
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.itmill.toolkit.terminal.gwt.client;
+
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Vector;
+
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.http.client.Request;
+import com.google.gwt.http.client.RequestBuilder;
+import com.google.gwt.http.client.RequestCallback;
+import com.google.gwt.http.client.RequestException;
+import com.google.gwt.http.client.Response;
+import com.google.gwt.json.client.JSONArray;
+import com.google.gwt.json.client.JSONObject;
+import com.google.gwt.json.client.JSONParser;
+import com.google.gwt.json.client.JSONString;
+import com.google.gwt.json.client.JSONValue;
+import com.google.gwt.user.client.Command;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.DeferredCommand;
+import com.google.gwt.user.client.Element;
+import com.google.gwt.user.client.Event;
+import com.google.gwt.user.client.Timer;
+import com.google.gwt.user.client.Window;
+import com.google.gwt.user.client.WindowCloseListener;
+import com.google.gwt.user.client.ui.FocusWidget;
+import com.google.gwt.user.client.ui.HasFocus;
+import com.google.gwt.user.client.ui.HasWidgets;
+import com.google.gwt.user.client.ui.Widget;
+import com.itmill.toolkit.terminal.gwt.client.ui.ContextMenu;
+import com.itmill.toolkit.terminal.gwt.client.ui.Field;
+import com.itmill.toolkit.terminal.gwt.client.ui.IView;
+import com.itmill.toolkit.terminal.gwt.client.ui.Notification;
+import com.itmill.toolkit.terminal.gwt.client.ui.Notification.HideEvent;
+
+/**
+ * Entry point classes define <code>onModuleLoad()</code>.
+ */
+public class ApplicationConnection {
+    private static final String MODIFIED_CLASSNAME = "i-modified";
+
+    private static final String REQUIRED_CLASSNAME_EXT = "-required";
+
+    private static final String ERROR_CLASSNAME_EXT = "-error";
+
+    public static final String VAR_RECORD_SEPARATOR = "\u001e";
+
+    public static final String VAR_FIELD_SEPARATOR = "\u001f";
+
+    private final HashMap resourcesMap = new HashMap();
+
+    private static Console console;
+
+    private static boolean testingMode;
+
+    private final Vector pendingVariables = new Vector();
+
+    private final HashMap idToPaintable = new HashMap();
+
+    private final HashMap paintableToId = new HashMap();
+
+    /** Contains ExtendedTitleInfo by paintable id */
+    private final HashMap paintableToTitle = new HashMap();
+
+    private final WidgetSet widgetSet;
+
+    private ContextMenu contextMenu = null;
+
+    private Timer loadTimer;
+    private Timer loadTimer2;
+    private Timer loadTimer3;
+    private Element loadElement;
+
+    private final IView view;
+
+    private boolean applicationRunning = false;
+
+    /**
+     * True if each Paintable objects id is injected to DOM. Used for Testing
+     * Tools.
+     */
+    private boolean usePaintableIdsInDOM = false;
+
+    /**
+     * Contains reference for client wrapper given to Testing Tools.
+     * 
+     * Used in JSNI functions
+     * 
+     * @SuppressWarnings
+     */
+    private final JavaScriptObject ttClientWrapper = null;
+
+    private int activeRequests = 0;
+
+    private final ApplicationConfiguration configuration;
+
+    private final Vector pendingVariableBursts = new Vector();
+
+    public ApplicationConnection(WidgetSet widgetSet,
+            ApplicationConfiguration cnf) {
+        this.widgetSet = widgetSet;
+        configuration = cnf;
+
+        if (isDebugMode()) {
+            console = new DebugConsole(this);
+        } else {
+            console = new NullConsole();
+        }
+
+        if (checkTestingMode()) {
+            usePaintableIdsInDOM = true;
+            initializeTestingTools();
+            Window.addWindowCloseListener(new WindowCloseListener() {
+                public void onWindowClosed() {
+                    uninitializeTestingTools();
+                }
+
+                public String onWindowClosing() {
+                    return null;
+                }
+            });
+        }
+
+        initializeClientHooks();
+
+        // TODO remove hard coded id name
+        view = new IView(cnf.getRootPanelId());
+
+        makeUidlRequest("", true);
+        applicationRunning = true;
+    }
+
+    /**
+     * Method to check if application is in testing mode. Can be used after
+     * application init.
+     * 
+     * @return true if in testing mode
+     */
+    public static boolean isTestingMode() {
+        return testingMode;
+    }
+
+    /**
+     * Check is application is run in testing mode.
+     * 
+     * @return true if in testing mode
+     */
+    private native static boolean checkTestingMode()
+    /*-{
+        @com.itmill.toolkit.terminal.gwt.client.ApplicationConnection::testingMode = $wnd.top.itmill && $wnd.top.itmill.registerToTT ? true : false;
+        return @com.itmill.toolkit.terminal.gwt.client.ApplicationConnection::testingMode;
+    }-*/;
+
+    private native void initializeTestingTools()
+    /*-{
+         var ap = this;
+         var client = {};
+         client.isActive = function() {
+             return ap.@com.itmill.toolkit.terminal.gwt.client.ApplicationConnection::hasActiveRequest()();
+         }
+         var vi = ap.@com.itmill.toolkit.terminal.gwt.client.ApplicationConnection::getVersionInfo()();
+         if (vi) {
+             client.getVersionInfo = function() {
+                 return vi;
+             }
+         }
+         $wnd.top.itmill.registerToTT(client);
+         this.@com.itmill.toolkit.terminal.gwt.client.ApplicationConnection::ttClientWrapper = client;
+    }-*/;
+
+    /**
+     * Helper for tt initialization
+     */
+    private JavaScriptObject getVersionInfo() {
+        return configuration.getVersionInfoJSObject();
+    }
+
+    private native void uninitializeTestingTools()
+    /*-{
+         $wnd.top.itmill.unregisterFromTT(this.@com.itmill.toolkit.terminal.gwt.client.ApplicationConnection::ttClientWrapper);
+    }-*/;
+
+    /**
+     * Publishes a JavaScript API for mash-up applications.
+     * <ul>
+     * <li><code>itmill.forceSync()</code> sends pending variable changes, in
+     * effect synchronizing the server and client state. This is done for all
+     * applications on host page.</li>
+     * </ul>
+     * 
+     * TODO make this multi-app aware
+     */
+    private native void initializeClientHooks()
+    /*-{
+        var app = this;
+        var oldSync;
+        if($wnd.itmill.forceSync) {
+            oldSync = $wnd.itmill.forceSync;
+        }
+        $wnd.itmill.forceSync = function() {
+            if(oldSync) {
+                oldSync();
+            }
+            app.@com.itmill.toolkit.terminal.gwt.client.ApplicationConnection::sendPendingVariableChanges()();
+        }
+    }-*/;
+
+    public static Console getConsole() {
+        return console;
+    }
+
+    private native static boolean isDebugMode()
+    /*-{
+     var uri = $wnd.location;
+     var re = /debug[^\/]*$/;
+     return re.test(uri);
+     }-*/;
+
+    public String getAppUri() {
+        return configuration.getApplicationUri();
+    };
+
+    public boolean hasActiveRequest() {
+        return (activeRequests > 0);
+    }
+
+    private void makeUidlRequest(String requestData, boolean repaintAll) {
+        startRequest();
+
+        console.log("Making UIDL Request with params: " + requestData);
+        String uri = getAppUri() + "UIDL" + configuration.getPathInfo();
+        if (repaintAll) {
+            uri += "?repaintAll=1";
+        }
+        final RequestBuilder rb = new RequestBuilder(RequestBuilder.POST, uri);
+        // rb.setHeader("Content-Type",
+        // "application/x-www-form-urlencoded; charset=utf-8");
+        rb.setHeader("Content-Type", "text/plain;charset=utf-8");
+        try {
+            rb.sendRequest(requestData, new RequestCallback() {
+                public void onError(Request request, Throwable exception) {
+                    // TODO Better reporting to user
+                    console.error("Got error");
+                    endRequest();
+                }
+
+                public void onResponseReceived(Request request,
+                        Response response) {
+                    handleReceivedJSONMessage(response);
+                }
+
+            });
+
+        } catch (final RequestException e) {
+            // TODO Better reporting to user
+            console.error(e.getMessage());
+            endRequest();
+        }
+    }
+
+    private void startRequest() {
+        activeRequests++;
+        showLoadingIndicator();
+    }
+
+    private void endRequest() {
+
+        checkForPendingVariableBursts();
+
+        activeRequests--;
+        // deferring to avoid flickering
+        DeferredCommand.addCommand(new Command() {
+            public void execute() {
+                if (activeRequests == 0) {
+                    hideLoadingIndicator();
+                }
+            }
+        });
+    }
+
+    /**
+     * This method is called after applying uidl change set to application.
+     * 
+     * It will clean current and queued variable change sets. And send next
+     * change set if it exists.
+     */
+    private void checkForPendingVariableBursts() {
+        cleanVariableBurst(pendingVariables);
+        if (pendingVariableBursts.size() > 0) {
+            for (Iterator iterator = pendingVariableBursts.iterator(); iterator
+                    .hasNext();) {
+                cleanVariableBurst((Vector) iterator.next());
+            }
+            Vector nextBurst = (Vector) pendingVariableBursts.firstElement();
+            pendingVariableBursts.remove(0);
+            buildAndSendVariableBurst(nextBurst);
+        }
+    }
+
+    /**
+     * Cleans given queue of variable changes of such changes that came from
+     * components that do not exist anymore.
+     * 
+     * @param variableBurst
+     */
+    private void cleanVariableBurst(Vector variableBurst) {
+        for (int i = 1; i < variableBurst.size(); i += 2) {
+            String id = (String) variableBurst.get(i);
+            id = id.substring(0, id.indexOf(VAR_FIELD_SEPARATOR));
+            if (!idToPaintable.containsKey(id)) {
+                // variable owner does not exist anymore
+                variableBurst.remove(i - 1);
+                variableBurst.remove(i - 1);
+                i -= 2;
+                ApplicationConnection.getConsole().log(
+                        "Removed variable from removed component: " + id);
+            }
+        }
+    }
+
+    private void showLoadingIndicator() {
+        // show initial throbber
+        if (loadTimer == null) {
+            loadTimer = new Timer() {
+                public void run() {
+                    // show initial throbber
+                    if (loadElement == null) {
+                        loadElement = DOM.createDiv();
+                        DOM.setStyleAttribute(loadElement, "position",
+                                "absolute");
+                        DOM.appendChild(view.getElement(), loadElement);
+                        ApplicationConnection.getConsole().log(
+                                "inserting load indicator");
+                        // Position
+                        DOM.setStyleAttribute(loadElement, "top", (view
+                                .getAbsoluteTop() + 6)
+                                + "px");
+                    }
+                    DOM.setElementProperty(loadElement, "className",
+                            "i-loading-indicator");
+                    DOM.setStyleAttribute(loadElement, "display", "block");
+                    DOM.setStyleAttribute(loadElement, "left", (view
+                            .getAbsoluteLeft()
+                            + view.getOffsetWidth()
+                            - DOM.getElementPropertyInt(loadElement,
+                                    "offsetWidth") - 5)
+                            + "px");
+
+                    // Initialize other timers
+                    loadTimer2 = new Timer() {
+                        public void run() {
+                            DOM.setElementProperty(loadElement, "className",
+                                    "i-loading-indicator-delay");
+                        }
+                    };
+                    // Second one kicks in at 1500ms
+                    loadTimer2.schedule(1200);
+
+                    loadTimer3 = new Timer() {
+                        public void run() {
+                            DOM.setElementProperty(loadElement, "className",
+                                    "i-loading-indicator-wait");
+                        }
+                    };
+                    // Third one kicks in at 5000ms
+                    loadTimer3.schedule(4700);
+                }
+            };
+            // First one kicks in at 300ms
+            loadTimer.schedule(300);
+        }
+    }
+
+    private void hideLoadingIndicator() {
+        if (loadTimer != null) {
+            loadTimer.cancel();
+            if (loadTimer2 != null) {
+                loadTimer2.cancel();
+                loadTimer3.cancel();
+            }
+            loadTimer = null;
+        }
+        if (loadElement != null) {
+            DOM.removeChild(view.getElement(), loadElement);
+            loadElement = null;
+        }
+    }
+
+    private void handleReceivedJSONMessage(Response response) {
+        final Date start = new Date();
+        String jsonText = response.getText();
+        // for(;;);[realjson]
+        jsonText = jsonText.substring(9, jsonText.length() - 1);
+        JSONValue json;
+        try {
+            json = JSONParser.parse(jsonText);
+        } catch (final com.google.gwt.json.client.JSONException e) {
+            endRequest();
+            console.log(e.getMessage() + " - Original JSON-text:");
+            console.log(jsonText);
+            return;
+        }
+        // Handle redirect
+        final JSONObject redirect = (JSONObject) ((JSONObject) json)
+                .get("redirect");
+        if (redirect != null) {
+            final JSONString url = (JSONString) redirect.get("url");
+            if (url != null) {
+                console.log("redirecting to " + url.stringValue());
+                redirect(url.stringValue());
+                return;
+            }
+        }
+
+        // Store resources
+        final JSONObject resources = (JSONObject) ((JSONObject) json)
+                .get("resources");
+        for (final Iterator i = resources.keySet().iterator(); i.hasNext();) {
+            final String key = (String) i.next();
+            resourcesMap.put(key, ((JSONString) resources.get(key))
+                    .stringValue());
+        }
+
+        // Store locale data
+        if (((JSONObject) json).containsKey("locales")) {
+            final JSONArray l = (JSONArray) ((JSONObject) json).get("locales");
+            for (int i = 0; i < l.size(); i++) {
+                LocaleService.addLocale((JSONObject) l.get(i));
+            }
+        }
+
+        JSONObject meta = null;
+        if (((JSONObject) json).containsKey("meta")) {
+            meta = ((JSONObject) json).get("meta").isObject();
+            if (meta.containsKey("repaintAll")) {
+                view.clear();
+                idToPaintable.clear();
+                paintableToId.clear();
+            }
+        }
+
+        // Process changes
+        final JSONArray changes = (JSONArray) ((JSONObject) json)
+                .get("changes");
+        for (int i = 0; i < changes.size(); i++) {
+            try {
+                final UIDL change = new UIDL((JSONArray) changes.get(i));
+                try {
+                    console.dirUIDL(change);
+                } catch (final Exception e) {
+                    console.log(e.getMessage());
+                    // TODO: dir doesn't work in any browser although it should
+                    // work (works in hosted mode)
+                    // it partially did at some part but now broken.
+                }
+                final UIDL uidl = change.getChildUIDL(0);
+                final Paintable paintable = getPaintable(uidl.getId());
+                if (paintable != null) {
+                    paintable.updateFromUIDL(uidl, this);
+                } else {
+                    if (!uidl.getTag().equals("window")) {
+                        System.out.println("Received update for "
+                                + uidl.getTag()
+                                + ", but there is no such paintable ("
+                                + uidl.getId() + ") rendered.");
+                    } else {
+                        view.updateFromUIDL(uidl, this);
+                    }
+                }
+            } catch (final Throwable e) {
+                e.printStackTrace();
+            }
+        }
+
+        if (meta != null) {
+            if (meta.containsKey("focus")) {
+                final String focusPid = meta.get("focus").isString()
+                        .stringValue();
+                final Paintable toBeFocused = getPaintable(focusPid);
+                if (toBeFocused instanceof HasFocus) {
+                    final HasFocus toBeFocusedWidget = (HasFocus) toBeFocused;
+                    toBeFocusedWidget.setFocus(true);
+                } else if (toBeFocused instanceof Focusable) {
+                    ((Focusable) toBeFocused).focus();
+                } else {
+                    getConsole().log("Could not focus component");
+                }
+
+            }
+            if (meta.containsKey("appError")) {
+                JSONObject error = meta.get("appError").isObject();
+                JSONValue val = error.get("caption");
+                String html = "";
+                if (val.isString() != null) {
+                    html += "<h1>" + val.isString().stringValue() + "</h1>";
+                }
+                val = error.get("message");
+                if (val.isString() != null) {
+                    html += "<p>" + val.isString().stringValue() + "</p>";
+                }
+                val = error.get("url");
+                String url = null;
+                if (val.isString() != null) {
+                    url = val.isString().stringValue();
+                }
+
+                if (html.length() != 0) {
+                    Notification n = new Notification(1000 * 60 * 45); // 45min
+                    n.addEventListener(new NotificationRedirect(url));
+                    n.show(html, Notification.CENTERED_TOP, "system");
+                } else {
+                    redirect(url);
+                }
+                applicationRunning = false;
+            }
+        }
+
+        final long prosessingTime = (new Date().getTime()) - start.getTime();
+        console.log(" Processing time was " + String.valueOf(prosessingTime)
+                + "ms for " + jsonText.length() + " characters of JSON");
+        console.log("Referenced paintables: " + idToPaintable.size());
+
+        endRequest();
+    }
+
+    // Redirect browser, null reloads current page
+    private static native void redirect(String url)
+    /*-{
+      if (url) {
+         $wnd.location = url;
+      } else {
+          $wnd.location = $wnd.location;
+      }
+     }-*/;
+
+    public void registerPaintable(String id, Paintable paintable) {
+        idToPaintable.put(id, paintable);
+        paintableToId.put(paintable, id);
+    }
+
+    public void unregisterPaintable(Paintable p) {
+        Object id = paintableToId.get(p);
+        idToPaintable.remove(id);
+        paintableToTitle.remove(id);
+        paintableToId.remove(p);
+
+        if (p instanceof HasWidgets) {
+            unregisterChildPaintables((HasWidgets) p);
+        }
+    }
+
+    public void unregisterChildPaintables(HasWidgets container) {
+        final Iterator it = container.iterator();
+        while (it.hasNext()) {
+            final Widget w = (Widget) it.next();
+            if (w instanceof Paintable) {
+                unregisterPaintable((Paintable) w);
+            } else if (w instanceof HasWidgets) {
+                unregisterChildPaintables((HasWidgets) w);
+            }
+        }
+    }
+
+    /**
+     * Returns Paintable element by its id
+     * 
+     * @param id
+     *                Paintable ID
+     */
+    public Paintable getPaintable(String id) {
+        return (Paintable) idToPaintable.get(id);
+    }
+
+    private void addVariableToQueue(String paintableId, String variableName,
+            String encodedValue, boolean immediate, char type) {
+        final String id = paintableId + VAR_FIELD_SEPARATOR + variableName
+                + VAR_FIELD_SEPARATOR + type;
+        for (int i = 1; i < pendingVariables.size(); i += 2) {
+            if ((pendingVariables.get(i)).equals(id)) {
+                pendingVariables.remove(i - 1);
+                pendingVariables.remove(i - 1);
+                break;
+            }
+        }
+        pendingVariables.add(encodedValue);
+        pendingVariables.add(id);
+        if (immediate) {
+            sendPendingVariableChanges();
+        }
+    }
+
+    /**
+     * This method sends currently queued variable changes to server. It is
+     * called when immediate variable update must happen.
+     * 
+     * To ensure correct order for variable changes (due servers multithreading
+     * or network), we always wait for active request to be handler before
+     * sending a new one. If there is an active request, we will put varible
+     * "burst" to queue that will be purged after current request is handled.
+     * 
+     */
+    public void sendPendingVariableChanges() {
+        if (applicationRunning) {
+            if (hasActiveRequest()) {
+                // skip empty queues if there are pending bursts to be sent
+                if (pendingVariables.size() > 0
+                        || pendingVariableBursts.size() == 0) {
+                    Vector burst = (Vector) pendingVariables.clone();
+                    pendingVariableBursts.add(burst);
+                }
+            } else {
+                buildAndSendVariableBurst(pendingVariables);
+            }
+        }
+    }
+
+    private void buildAndSendVariableBurst(Vector pendingVariables) {
+        final StringBuffer req = new StringBuffer();
+
+        for (int i = 0; i < pendingVariables.size(); i++) {
+            if (i > 0) {
+                if (i % 2 == 0) {
+                    req.append(VAR_RECORD_SEPARATOR);
+                } else {
+                    req.append(VAR_FIELD_SEPARATOR);
+                }
+            }
+            req.append(pendingVariables.get(i));
+        }
+
+        pendingVariables.clear();
+        makeUidlRequest(req.toString(), false);
+    }
+
+    public void updateVariable(String paintableId, String variableName,
+            String newValue, boolean immediate) {
+        addVariableToQueue(paintableId, variableName, newValue, immediate, 's');
+    }
+
+    public void updateVariable(String paintableId, String variableName,
+            int newValue, boolean immediate) {
+        addVariableToQueue(paintableId, variableName, "" + newValue, immediate,
+                'i');
+    }
+
+    public void updateVariable(String paintableId, String variableName,
+            long newValue, boolean immediate) {
+        addVariableToQueue(paintableId, variableName, "" + newValue, immediate,
+                'l');
+    }
+
+    public void updateVariable(String paintableId, String variableName,
+            float newValue, boolean immediate) {
+        addVariableToQueue(paintableId, variableName, "" + newValue, immediate,
+                'f');
+    }
+
+    public void updateVariable(String paintableId, String variableName,
+            double newValue, boolean immediate) {
+        addVariableToQueue(paintableId, variableName, "" + newValue, immediate,
+                'd');
+    }
+
+    public void updateVariable(String paintableId, String variableName,
+            boolean newValue, boolean immediate) {
+        addVariableToQueue(paintableId, variableName, newValue ? "true"
+                : "false", immediate, 'b');
+    }
+
+    public void updateVariable(String paintableId, String variableName,
+            Object[] values, boolean immediate) {
+        final StringBuffer buf = new StringBuffer();
+        for (int i = 0; i < values.length; i++) {
+            if (i > 0) {
+                buf.append(",");
+            }
+            buf.append(values[i].toString());
+        }
+        addVariableToQueue(paintableId, variableName, buf.toString(),
+                immediate, 'a');
+    }
+
+    /**
+     * Update generic component features.
+     * 
+     * <h2>Selecting correct implementation</h2>
+     * 
+     * <p>
+     * The implementation of a component depends on many properties, including
+     * styles, component features, etc. Sometimes the user changes those
+     * properties after the component has been created. Calling this method in
+     * the beginning of your updateFromUIDL -method automatically replaces your
+     * component with more appropriate if the requested implementation changes.
+     * </p>
+     * 
+     * <h2>Caption, icon, error messages and description</h2>
+     * 
+     * <p>
+     * Component can delegate management of caption, icon, error messages and
+     * description to parent layout. This is optional an should be decided by
+     * component author
+     * </p>
+     * 
+     * <h2>Component visibility and disabling</h2>
+     * 
+     * This method will manage component visibility automatically and if
+     * component is an instanceof FocusWidget, also handle component disabling
+     * when needed.
+     * 
+     * @param component
+     *                Widget to be updated, expected to implement an instance of
+     *                Paintable
+     * @param uidl
+     *                UIDL to be painted
+     * @param manageCaption
+     *                True if you want to delegate caption, icon, description
+     *                and error message management to parent.
+     * 
+     * @return Returns true iff no further painting is needed by caller
+     */
+    public boolean updateComponent(Widget component, UIDL uidl,
+            boolean manageCaption) {
+
+        // If the server request that a cached instance should be used, do
+        // nothing
+        if (uidl.getBooleanAttribute("cached")) {
+            return true;
+        }
+
+        // Visibility
+        boolean visible = !uidl.getBooleanAttribute("invisible");
+        component.setVisible(visible);
+        // Set captions
+        if (manageCaption) {
+            final Container parent = Util.getParentLayout(component);
+            if (parent != null) {
+                parent.updateCaption((Paintable) component, uidl);
+            }
+        }
+
+        if (!visible) {
+            return true;
+        }
+
+        // Switch to correct implementation if needed
+        if (!widgetSet.isCorrectImplementation(component, uidl)) {
+            final Container parent = Util.getParentLayout(component);
+            if (parent != null) {
+                final Widget w = widgetSet.createWidget(uidl);
+                parent.replaceChildComponent(component, w);
+                registerPaintable(uidl.getId(), (Paintable) w);
+                ((Paintable) w).updateFromUIDL(uidl, this);
+                return true;
+            }
+        }
+
+        updateComponentSize(component, uidl);
+
+        boolean enabled = !uidl.getBooleanAttribute("disabled");
+        if (component instanceof FocusWidget) {
+            ((FocusWidget) component).setEnabled(enabled);
+        }
+
+        StringBuffer styleBuf = new StringBuffer();
+        final String primaryName = component.getStylePrimaryName();
+        styleBuf.append(primaryName);
+
+        // first disabling and read-only status
+        if (!enabled) {
+            styleBuf.append(" ");
+            styleBuf.append("i-disabled");
+        }
+        if (uidl.getBooleanAttribute("readonly")) {
+            styleBuf.append(" ");
+            styleBuf.append("i-readonly");
+        }
+
+        // add additional styles as css classes, prefixed with component default
+        // stylename
+        if (uidl.hasAttribute("style")) {
+            final String[] styles = uidl.getStringAttribute("style").split(" ");
+            for (int i = 0; i < styles.length; i++) {
+                styleBuf.append(" ");
+                styleBuf.append(primaryName);
+                styleBuf.append("-");
+                styleBuf.append(styles[i]);
+            }
+        }
+
+        // add modified classname to Fields
+        if (uidl.hasAttribute("modified") && component instanceof Field) {
+            styleBuf.append(" ");
+            styleBuf.append(MODIFIED_CLASSNAME);
+        }
+
+        TooltipInfo tooltipInfo = getTitleInfo((Paintable) component);
+        if (uidl.hasAttribute("description")) {
+            tooltipInfo.setTitle(uidl.getStringAttribute("description"));
+        } else {
+            tooltipInfo.setTitle(null);
+        }
+
+        // add error classname to components w/ error
+        if (uidl.hasAttribute("error")) {
+            styleBuf.append(" ");
+            styleBuf.append(primaryName);
+            styleBuf.append(ERROR_CLASSNAME_EXT);
+
+            tooltipInfo.setErrorUidl(uidl.getErrors());
+        } else {
+            tooltipInfo.setErrorUidl(null);
+        }
+
+        // add required style to required components
+        if (uidl.hasAttribute("required")) {
+            styleBuf.append(" ");
+            styleBuf.append(primaryName);
+            styleBuf.append(REQUIRED_CLASSNAME_EXT);
+        }
+
+        // Styles + disabled & readonly
+        component.setStyleName(styleBuf.toString());
+
+        if (usePaintableIdsInDOM) {
+            DOM.setElementProperty(component.getElement(), "id", uidl.getId());
+        }
+
+        return false;
+    }
+
+    private void updateComponentSize(Widget component, UIDL uidl) {
+        String w = uidl.hasAttribute("width") ? uidl
+                .getStringAttribute("width") : "";
+        component.setWidth(w);
+        String h = uidl.hasAttribute("height") ? uidl
+                .getStringAttribute("height") : "";
+        component.setHeight(h);
+    }
+
+    /**
+     * Get either existing or new Paintable for given UIDL.
+     * 
+     * If corresponding Paintable has been previously painted, return it.
+     * Otherwise create and register a new Paintable from UIDL. Caller must
+     * update the returned Paintable from UIDL after it has been connected to
+     * parent.
+     * 
+     * @param uidl
+     *                UIDL to create Paintable from.
+     * @return Either existing or new Paintable corresponding to UIDL.
+     */
+    public Paintable getPaintable(UIDL uidl) {
+        final String id = uidl.getId();
+        Paintable w = getPaintable(id);
+        if (w != null) {
+            return w;
+        }
+        w = (Paintable) widgetSet.createWidget(uidl);
+        registerPaintable(id, w);
+        return w;
+    }
+
+    public String getResource(String name) {
+        return (String) resourcesMap.get(name);
+    }
+
+    /**
+     * Singleton method to get instance of app's context menu.
+     * 
+     * @return IContextMenu object
+     */
+    public ContextMenu getContextMenu() {
+        if (contextMenu == null) {
+            contextMenu = new ContextMenu();
+            if (usePaintableIdsInDOM) {
+                DOM.setElementProperty(contextMenu.getElement(), "id",
+                        "PID_TOOLKIT_CM");
+            }
+        }
+        return contextMenu;
+    }
+
+    /**
+     * Translates custom protocols in UIRL URI's to be recognizable by browser.
+     * All uri's from UIDL should be routed via this method before giving them
+     * to browser due URI's in UIDL may contain custom protocols like theme://.
+     * 
+     * @param toolkitUri
+     *                toolkit URI from uidl
+     * @return translated URI ready for browser
+     */
+    public String translateToolkitUri(String toolkitUri) {
+        if (toolkitUri == null) {
+            return null;
+        }
+        if (toolkitUri.startsWith("theme://")) {
+            final String themeUri = configuration.getThemeUri();
+            if (themeUri == null) {
+                console
+                        .error("Theme not set: ThemeResource will not be found. ("
+                                + toolkitUri + ")");
+            }
+            toolkitUri = themeUri + toolkitUri.substring(7);
+        }
+        return toolkitUri;
+    }
+
+    public String getThemeUri() {
+        return configuration.getThemeUri();
+    }
+
+    /**
+     * Listens for Notification hide event, and redirects. Used for system
+     * messages, such as session expired.
+     * 
+     */
+    private class NotificationRedirect implements Notification.EventListener {
+        String url;
+
+        NotificationRedirect(String url) {
+            this.url = url;
+        }
+
+        public void notificationHidden(HideEvent event) {
+            redirect(url);
+        }
+
+    }
+
+    /* Extended title handling */
+
+    /**
+     * Data showed in tooltips are stored centrilized as it may be needed in
+     * varios place: caption, layouts, and in owner components themselves.
+     * 
+     * Updating TooltipInfo is done in updateComponent method.
+     * 
+     */
+    public TooltipInfo getTitleInfo(Paintable titleOwner) {
+        TooltipInfo info = (TooltipInfo) paintableToTitle.get(titleOwner);
+        if (info == null) {
+            info = new TooltipInfo();
+            paintableToTitle.put(titleOwner, info);
+        }
+        return info;
+    }
+
+    private final Tooltip tooltip = new Tooltip(this);
+
+    /**
+     * Component may want to delegate Tooltip handling to client. Layouts add
+     * Tooltip (description, errors) to caption, but some components may want
+     * them to appear one other elements too.
+     * 
+     * Events wanted by this handler are same as in Tooltip.TOOLTIP_EVENTS
+     * 
+     * @param event
+     * @param owner
+     */
+    public void handleTooltipEvent(Event event, Paintable owner) {
+        tooltip.handleTooltipEvent(event, owner);
+
+    }
+
+    /**
+     * Adds PNG-fix conditionally (only for IE6) to the specified IMG -element.
+     * 
+     * @param el
+     *                the IMG element to fix
+     */
+    public void addPngFix(Element el) {
+        BrowserInfo b = BrowserInfo.get();
+        if (b.isIE6()) {
+            Util.addPngFix(el, getThemeUri()
+                    + "/../default/common/img/blank.gif");
+        }
+    }
+
+}
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/BrowserInfo.java b/src/com/itmill/toolkit/terminal/terminal/gwt/client/BrowserInfo.java
new file mode 100644 (file)
index 0000000..63805fd
--- /dev/null
@@ -0,0 +1,141 @@
+/* 
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.itmill.toolkit.terminal.gwt.client;
+
+/**
+ * Class used to query information about web browser.
+ * 
+ * Browser details are detected only once and those are stored in this singleton
+ * class.
+ * 
+ */
+public class BrowserInfo {
+
+    private static BrowserInfo instance;
+
+    /**
+     * Singleton method to get BrowserInfo object.
+     * 
+     * @return instance of BrowserInfo object
+     */
+    public static BrowserInfo get() {
+        if (instance == null) {
+            instance = new BrowserInfo();
+        }
+        return instance;
+    }
+
+    private boolean isGecko;
+    private boolean isAppleWebKit;
+    private boolean isSafari;
+    private boolean isOpera;
+    private boolean isIE;
+    private float ieVersion = -1;
+    private float geckoVersion = -1;
+    private float appleWebKitVersion = -1;
+
+    private BrowserInfo() {
+        try {
+            String ua = getBrowserString().toLowerCase();
+            // browser engine name
+            isGecko = ua.indexOf("gecko") != -1 && ua.indexOf("safari") == -1;
+            isAppleWebKit = ua.indexOf("applewebkit") != -1;
+
+            // browser name
+            isSafari = ua.indexOf("safari") != -1;
+            isOpera = ua.indexOf("opera") != -1;
+            isIE = ua.indexOf("msie") != -1 && !isOpera
+                    && (ua.indexOf("webtv") == -1);
+
+            if (isGecko) {
+                String tmp = ua.substring(ua.indexOf("rv:") + 3);
+                tmp = tmp.replaceFirst("(\\.[0-9]+).+", "$1");
+                geckoVersion = Float.parseFloat(tmp);
+            }
+            if (isAppleWebKit) {
+                String tmp = ua.substring(ua.indexOf("webkit/") + 7);
+                tmp = tmp.replaceFirst("([0-9]+)[^0-9].+", "$1");
+                appleWebKitVersion = Float.parseFloat(tmp);
+
+            }
+
+            if (isIE) {
+                String ieVersionString = ua.substring(ua.indexOf("msie ") + 5);
+                ieVersionString = ieVersionString.substring(0, ieVersionString
+                        .indexOf(";"));
+                ieVersion = Float.parseFloat(ieVersionString);
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+            ApplicationConnection.getConsole().error(e.getMessage());
+        }
+    }
+
+    public boolean isIE() {
+        return isIE;
+    }
+
+    public boolean isSafari() {
+        return isSafari;
+    }
+
+    public boolean isIE6() {
+        return isIE && ieVersion == 6;
+    }
+
+    public boolean isIE7() {
+        return isIE && ieVersion == 7;
+    }
+
+    public boolean isGecko() {
+        return isGecko;
+    }
+
+    public boolean isFF2() {
+        return isGecko && geckoVersion == 1.8;
+    }
+
+    public boolean isFF3() {
+        return isGecko && geckoVersion == 1.9;
+    }
+
+    public float getGeckoVersion() {
+        return geckoVersion;
+    }
+
+    public float getWebkitVersion() {
+        return appleWebKitVersion;
+    }
+
+    public float getIEVersion() {
+        return ieVersion;
+    }
+
+    public boolean isOpera() {
+        return isOpera;
+    }
+
+    public native static String getBrowserString()
+    /*-{
+        return $wnd.navigator.userAgent;
+    }-*/;
+
+    public static void test() {
+        Console c = ApplicationConnection.getConsole();
+
+        c.log("getBrowserString() " + getBrowserString());
+        c.log("isIE() " + get().isIE());
+        c.log("isIE6() " + get().isIE6());
+        c.log("isIE7() " + get().isIE7());
+        c.log("isFF2() " + get().isFF2());
+        c.log("isSafari() " + get().isSafari());
+        c.log("getGeckoVersion() " + get().getGeckoVersion());
+        c.log("getWebkitVersion() " + get().getWebkitVersion());
+        c.log("getIEVersion() " + get().getIEVersion());
+        c.log("isIE() " + get().isIE());
+        c.log("isIE() " + get().isIE());
+    }
+
+}
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/Caption.java b/src/com/itmill/toolkit/terminal/terminal/gwt/client/Caption.java
new file mode 100644 (file)
index 0000000..bab2b6e
--- /dev/null
@@ -0,0 +1,172 @@
+/* 
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.itmill.toolkit.terminal.gwt.client;
+
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.Element;
+import com.google.gwt.user.client.Event;
+import com.google.gwt.user.client.ui.HTML;
+import com.itmill.toolkit.terminal.gwt.client.ui.Icon;
+
+public class Caption extends HTML {
+
+    public static final String CLASSNAME = "i-caption";
+
+    private final Paintable owner;
+
+    private Element errorIndicatorElement;
+
+    private Element requiredFieldIndicator;
+
+    private Icon icon;
+
+    private Element captionText;
+
+    private final ApplicationConnection client;
+
+    /**
+     * 
+     * @param component
+     *                optional owner of caption. If not set, getOwner will
+     *                return null
+     * @param client
+     */
+    public Caption(Paintable component, ApplicationConnection client) {
+        super();
+        this.client = client;
+        owner = component;
+        setStyleName(CLASSNAME);
+    }
+
+    public void updateCaption(UIDL uidl) {
+        setVisible(!uidl.getBooleanAttribute("invisible"));
+
+        setStyleName(getElement(), "i-disabled", uidl.hasAttribute("disabled"));
+
+        boolean isEmpty = true;
+
+        if (uidl.hasAttribute("icon")) {
+            if (icon == null) {
+                icon = new Icon(client);
+
+                DOM.insertChild(getElement(), icon.getElement(), 0);
+            }
+            icon.setUri(uidl.getStringAttribute("icon"));
+            isEmpty = false;
+        } else {
+            if (icon != null) {
+                DOM.removeChild(getElement(), icon.getElement());
+                icon = null;
+            }
+        }
+
+        if (uidl.hasAttribute("caption")) {
+            if (captionText == null) {
+                captionText = DOM.createSpan();
+                DOM
+                        .insertChild(getElement(), captionText,
+                                icon == null ? 0 : 1);
+            }
+            String c = uidl.getStringAttribute("caption");
+            if (c == null) {
+                c = "";
+            } else {
+                isEmpty = false;
+            }
+            DOM.setInnerText(captionText, c);
+        } else {
+            // TODO should span also be removed
+        }
+
+        if (uidl.hasAttribute("description")) {
+            if (captionText != null) {
+                addStyleDependentName("hasdescription");
+            } else {
+                removeStyleDependentName("hasdescription");
+            }
+        }
+
+        if (uidl.getBooleanAttribute("required")) {
+            isEmpty = false;
+            if (requiredFieldIndicator == null) {
+                requiredFieldIndicator = DOM.createSpan();
+                DOM.setInnerText(requiredFieldIndicator, "*");
+                DOM.setElementProperty(requiredFieldIndicator, "className",
+                        "i-required-field-indicator");
+                DOM.appendChild(getElement(), requiredFieldIndicator);
+            }
+        } else {
+            if (requiredFieldIndicator != null) {
+                DOM.removeChild(getElement(), requiredFieldIndicator);
+                requiredFieldIndicator = null;
+            }
+        }
+
+        if (uidl.hasAttribute("error")) {
+            isEmpty = false;
+            if (errorIndicatorElement == null) {
+                errorIndicatorElement = DOM.createDiv();
+                if (Util.isIE()) {
+                    DOM.setInnerHTML(errorIndicatorElement, "&nbsp;");
+                }
+                DOM.setElementProperty(errorIndicatorElement, "className",
+                        "i-errorindicator");
+                DOM.appendChild(getElement(), errorIndicatorElement);
+            }
+        } else if (errorIndicatorElement != null) {
+            DOM.removeChild(getElement(), errorIndicatorElement);
+            errorIndicatorElement = null;
+        }
+
+        // Workaround for IE weirdness, sometimes returns bad height in some
+        // circumstances when Caption is empty. See #1444
+        // IE7 bugs more often. I wonder what happens when IE8 arrives...
+        if (Util.isIE()) {
+            if (isEmpty) {
+                setHeight("0px");
+                DOM.setStyleAttribute(getElement(), "overflow", "hidden");
+            } else {
+                setHeight("");
+                DOM.setStyleAttribute(getElement(), "overflow", "");
+            }
+
+        }
+
+    }
+
+    public void onBrowserEvent(Event event) {
+        super.onBrowserEvent(event);
+        final Element target = DOM.eventGetTarget(event);
+        if (client != null && !DOM.compare(target, getElement())) {
+            client.handleTooltipEvent(event, owner);
+        }
+    }
+
+    public static boolean isNeeded(UIDL uidl) {
+        if (uidl.getStringAttribute("caption") != null) {
+            return true;
+        }
+        if (uidl.hasAttribute("error")) {
+            return true;
+        }
+        if (uidl.hasAttribute("icon")) {
+            return true;
+        }
+        if (uidl.hasAttribute("required")) {
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * Returns Paintable for which this Caption belongs to.
+     * 
+     * @return owner Widget
+     */
+    public Paintable getOwner() {
+        return owner;
+    }
+}
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/CaptionWrapper.java b/src/com/itmill/toolkit/terminal/terminal/gwt/client/CaptionWrapper.java
new file mode 100644 (file)
index 0000000..ec5a585
--- /dev/null
@@ -0,0 +1,32 @@
+/* 
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.itmill.toolkit.terminal.gwt.client;
+
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.Widget;
+
+public class CaptionWrapper extends FlowPanel {
+
+    public static final String CLASSNAME = "i-captionwrapper";
+    Caption caption;
+    Paintable widget;
+
+    public CaptionWrapper(Paintable toBeWrapped, ApplicationConnection client) {
+        caption = new Caption(toBeWrapped, client);
+        add(caption);
+        widget = toBeWrapped;
+        add((Widget) widget);
+        setStyleName(CLASSNAME);
+    }
+
+    public void updateCaption(UIDL uidl) {
+        caption.updateCaption(uidl);
+        setVisible(!uidl.getBooleanAttribute("invisible"));
+    }
+
+    public Paintable getPaintable() {
+        return widget;
+    }
+}
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/Console.java b/src/com/itmill/toolkit/terminal/terminal/gwt/client/Console.java
new file mode 100644 (file)
index 0000000..51cc931
--- /dev/null
@@ -0,0 +1,17 @@
+/* 
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.itmill.toolkit.terminal.gwt.client;
+
+public interface Console {
+
+    public abstract void log(String msg);
+
+    public abstract void error(String msg);
+
+    public abstract void printObject(Object msg);
+
+    public abstract void dirUIDL(UIDL u);
+
+}
\ No newline at end of file
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/Container.java b/src/com/itmill/toolkit/terminal/terminal/gwt/client/Container.java
new file mode 100644 (file)
index 0000000..b73ad05
--- /dev/null
@@ -0,0 +1,52 @@
+/* 
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.itmill.toolkit.terminal.gwt.client;
+
+import com.google.gwt.user.client.ui.Widget;
+
+public interface Container extends Paintable {
+
+    /**
+     * Replace child of this layout with another component.
+     * 
+     * Each layout must be able to switch children. To to this, one must just
+     * give references to a current and new child. Note that the Layout is not
+     * responsible for registering Paintable into ApplicationConnection, but it
+     * is responsible is for unregistering it.
+     * 
+     * @param oldComponent
+     *                Child to be replaced
+     * @param newComponent
+     *                Child that replaces the oldComponent
+     */
+    void replaceChildComponent(Widget oldComponent, Widget newComponent);
+
+    /**
+     * Is a given component child of this layout.
+     * 
+     * @param component
+     *                Component to test.
+     * @return true iff component is a child of this layout.
+     */
+    boolean hasChildComponent(Widget component);
+
+    /**
+     * Update child components caption, description and error message.
+     * 
+     * <p>
+     * Each component is responsible for maintaining its caption, description
+     * and error message. In most cases components doesn't want to do that and
+     * those elements reside outside of the component. Because of this layouts
+     * must provide service for it's childen to show those elements for them.
+     * </p>
+     * 
+     * @param component
+     *                Child component for which service is requested.
+     * @param uidl
+     *                UIDL of the child component.
+     */
+    void updateCaption(Paintable component, UIDL uidl);
+
+}
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/ContainerResizedListener.java b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ContainerResizedListener.java
new file mode 100644 (file)
index 0000000..f299c98
--- /dev/null
@@ -0,0 +1,20 @@
+/* 
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.itmill.toolkit.terminal.gwt.client;
+
+/**
+ * ContainerResizedListener interface is useful for Widgets that support
+ * relative sizes and who need some additional sizing logic.
+ */
+public interface ContainerResizedListener {
+    /**
+     * This function is run when container box has been resized. Object
+     * implementing ContainerResizedListener is responsible to call the same
+     * function on its ancestors that implement NeedsLayout in case their
+     * container has resized. runAnchestorsLayout(HasWidgets parent) function
+     * from Util class may be a good helper for this.
+     */
+    public void iLayout();
+}
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/DateTimeService.java b/src/com/itmill/toolkit/terminal/terminal/gwt/client/DateTimeService.java
new file mode 100644 (file)
index 0000000..aaa3ebc
--- /dev/null
@@ -0,0 +1,245 @@
+/* \r
+@ITMillApache2LicenseForJavaFiles@\r
+ */\r
+\r
+package com.itmill.toolkit.terminal.gwt.client;\r
+\r
+import java.util.Date;\r
+\r
+/**\r
+ * This class provides date/time parsing services to all components on the\r
+ * client side.\r
+ * \r
+ * @author IT Mill Ltd.\r
+ * \r
+ */\r
+public class DateTimeService {\r
+    public static int RESOLUTION_YEAR = 0;\r
+    public static int RESOLUTION_MONTH = 1;\r
+    public static int RESOLUTION_DAY = 2;\r
+    public static int RESOLUTION_HOUR = 3;\r
+    public static int RESOLUTION_MIN = 4;\r
+    public static int RESOLUTION_SEC = 5;\r
+    public static int RESOLUTION_MSEC = 6;\r
+\r
+    private String currentLocale;\r
+\r
+    private static int[] maxDaysInMonth = { 31, 28, 31, 30, 31, 30, 31, 31, 30,\r
+            31, 30, 31 };\r
+\r
+    /**\r
+     * Creates a new date time service with the application default locale.\r
+     */\r
+    public DateTimeService() {\r
+        currentLocale = LocaleService.getDefaultLocale();\r
+    }\r
+\r
+    /**\r
+     * Creates a new date time service with a given locale.\r
+     * \r
+     * @param locale\r
+     *                e.g. fi, en etc.\r
+     * @throws LocaleNotLoadedException\r
+     */\r
+    public DateTimeService(String locale) throws LocaleNotLoadedException {\r
+        setLocale(locale);\r
+    }\r
+\r
+    public void setLocale(String locale) throws LocaleNotLoadedException {\r
+        if (LocaleService.getAvailableLocales().contains(locale)) {\r
+            currentLocale = locale;\r
+        } else {\r
+            throw new LocaleNotLoadedException(locale);\r
+        }\r
+    }\r
+\r
+    public String getLocale() {\r
+        return currentLocale;\r
+    }\r
+\r
+    public String getMonth(int month) {\r
+        try {\r
+            return LocaleService.getMonthNames(currentLocale)[month];\r
+        } catch (final LocaleNotLoadedException e) {\r
+            // TODO redirect to console\r
+            System.out.println(e + ":" + e.getMessage());\r
+        }\r
+        return null;\r
+    }\r
+\r
+    public String getShortMonth(int month) {\r
+        try {\r
+            return LocaleService.getShortMonthNames(currentLocale)[month];\r
+        } catch (final LocaleNotLoadedException e) {\r
+            // TODO redirect to console\r
+            System.out.println(e + ":" + e.getMessage());\r
+        }\r
+        return null;\r
+    }\r
+\r
+    public String getDay(int day) {\r
+        try {\r
+            return LocaleService.getDayNames(currentLocale)[day];\r
+        } catch (final LocaleNotLoadedException e) {\r
+            // TODO redirect to console\r
+            System.out.println(e + ":" + e.getMessage());\r
+        }\r
+        return null;\r
+    }\r
+\r
+    public String getShortDay(int day) {\r
+        try {\r
+            return LocaleService.getShortDayNames(currentLocale)[day];\r
+        } catch (final LocaleNotLoadedException e) {\r
+            // TODO redirect to console\r
+            System.out.println(e + ":" + e.getMessage());\r
+        }\r
+        return null;\r
+    }\r
+\r
+    public int getFirstDayOfWeek() {\r
+        try {\r
+            return LocaleService.getFirstDayOfWeek(currentLocale);\r
+        } catch (final LocaleNotLoadedException e) {\r
+            // TODO redirect to console\r
+            System.out.println(e + ":" + e.getMessage());\r
+        }\r
+        return 0;\r
+    }\r
+\r
+    public boolean isTwelveHourClock() {\r
+        try {\r
+            return LocaleService.isTwelveHourClock(currentLocale);\r
+        } catch (final LocaleNotLoadedException e) {\r
+            // TODO redirect to console\r
+            System.out.println(e + ":" + e.getMessage());\r
+        }\r
+        return false;\r
+    }\r
+\r
+    public String getClockDelimeter() {\r
+        try {\r
+            return LocaleService.getClockDelimiter(currentLocale);\r
+        } catch (final LocaleNotLoadedException e) {\r
+            // TODO redirect to console\r
+            System.out.println(e + ":" + e.getMessage());\r
+        }\r
+        return ":";\r
+    }\r
+\r
+    public String[] getAmPmStrings() {\r
+        try {\r
+            return LocaleService.getAmPmStrings(currentLocale);\r
+        } catch (final LocaleNotLoadedException e) {\r
+            // TODO redirect to console\r
+            System.out.println(e + ":" + e.getMessage());\r
+        }\r
+        final String[] temp = new String[2];\r
+        temp[0] = "AM";\r
+        temp[1] = "PM";\r
+        return temp;\r
+    }\r
+\r
+    public int getStartWeekDay(Date date) {\r
+        final Date dateForFirstOfThisMonth = new Date(date.getYear(), date\r
+                .getMonth(), 1);\r
+        int firstDay;\r
+        try {\r
+            firstDay = LocaleService.getFirstDayOfWeek(currentLocale);\r
+        } catch (final LocaleNotLoadedException e) {\r
+            firstDay = 0;\r
+            // TODO redirect to console\r
+            System.out.println(e + ":" + e.getMessage());\r
+        }\r
+        int start = dateForFirstOfThisMonth.getDay() - firstDay;\r
+        if (start < 0) {\r
+            start = 6;\r
+        }\r
+        return start;\r
+    }\r
+\r
+    public static int getNumberOfDaysInMonth(Date date) {\r
+        final int month = date.getMonth();\r
+        if (month == 1 && true == isLeapYear(date)) {\r
+            return 29;\r
+        }\r
+        return maxDaysInMonth[month];\r
+    }\r
+\r
+    public static boolean isLeapYear(Date date) {\r
+        // Instantiate the date for 1st March of that year\r
+        final Date firstMarch = new Date(date.getYear(), 2, 1);\r
+\r
+        // Go back 1 day\r
+        final long firstMarchTime = firstMarch.getTime();\r
+        final long lastDayTimeFeb = firstMarchTime - (24 * 60 * 60 * 1000); // NUM_MILLISECS_A_DAY\r
+\r
+        // Instantiate new Date with this time\r
+        final Date febLastDay = new Date(lastDayTimeFeb);\r
+\r
+        // Check for date in this new instance\r
+        return (29 == febLastDay.getDate()) ? true : false;\r
+    }\r
+\r
+    public static boolean isSameDay(Date d1, Date d2) {\r
+        return (getDayInt(d1) == getDayInt(d2));\r
+    }\r
+\r
+    public static boolean isInRange(Date date, Date rangeStart, Date rangeEnd,\r
+            int resolution) {\r
+        Date s;\r
+        Date e;\r
+        if (rangeStart.after(rangeEnd)) {\r
+            s = rangeEnd;\r
+            e = rangeStart;\r
+        } else {\r
+            e = rangeEnd;\r
+            s = rangeStart;\r
+        }\r
+        long start = s.getYear() * 10000000000l;\r
+        long end = e.getYear() * 10000000000l;\r
+        long target = date.getYear() * 10000000000l;\r
+\r
+        if (resolution == RESOLUTION_YEAR) {\r
+            return (start <= target && end >= target);\r
+        }\r
+        start += s.getMonth() * 100000000;\r
+        end += e.getMonth() * 100000000;\r
+        target += date.getMonth() * 100000000;\r
+        if (resolution == RESOLUTION_MONTH) {\r
+            return (start <= target && end >= target);\r
+        }\r
+        start += s.getDate() * 1000000;\r
+        end += e.getDate() * 1000000;\r
+        target += date.getDate() * 1000000;\r
+        if (resolution == RESOLUTION_DAY) {\r
+            return (start <= target && end >= target);\r
+        }\r
+        start += s.getHours() * 10000;\r
+        end += e.getHours() * 10000;\r
+        target += date.getHours() * 10000;\r
+        if (resolution == RESOLUTION_HOUR) {\r
+            return (start <= target && end >= target);\r
+        }\r
+        start += s.getMinutes() * 100;\r
+        end += e.getMinutes() * 100;\r
+        target += date.getMinutes() * 100;\r
+        if (resolution == RESOLUTION_MIN) {\r
+            return (start <= target && end >= target);\r
+        }\r
+        start += s.getSeconds();\r
+        end += e.getSeconds();\r
+        target += date.getSeconds();\r
+        return (start <= target && end >= target);\r
+\r
+    }\r
+\r
+    private static int getDayInt(Date date) {\r
+        final int y = date.getYear();\r
+        final int m = date.getMonth();\r
+        final int d = date.getDate();\r
+\r
+        return ((y + 1900) * 10000 + m * 100 + d) * 1000000000;\r
+    }\r
+\r
+}\r
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/DebugConsole.java b/src/com/itmill/toolkit/terminal/terminal/gwt/client/DebugConsole.java
new file mode 100755 (executable)
index 0000000..6bd0f24
--- /dev/null
@@ -0,0 +1,112 @@
+/* 
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.itmill.toolkit.terminal.gwt.client;
+
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.Element;
+import com.google.gwt.user.client.Event;
+import com.google.gwt.user.client.Window;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.HTML;
+import com.google.gwt.user.client.ui.Label;
+import com.google.gwt.user.client.ui.Panel;
+import com.google.gwt.user.client.ui.ScrollPanel;
+import com.google.gwt.user.client.ui.Widget;
+import com.itmill.toolkit.terminal.gwt.client.ui.IWindow;
+
+public final class DebugConsole extends IWindow implements Console {
+
+    private final Panel panel;
+
+    public DebugConsole(ApplicationConnection client) {
+        super();
+        this.client = client;
+        panel = new FlowPanel();
+        final ScrollPanel p = new ScrollPanel();
+        p.add(panel);
+        setWidget(p);
+        setCaption("Debug window");
+        minimize();
+        show();
+    }
+
+    private void minimize() {
+        // TODO stack to bottom (create window manager of some sort)
+        setPixelSize(60, 60);
+        setPopupPosition(Window.getClientWidth()
+                - (100 + IWindow.BORDER_WIDTH_HORIZONTAL), 0);
+    }
+
+    /*
+     * (non-Javadoc)
+     * 
+     * @see com.itmill.toolkit.terminal.gwt.client.Console#log(java.lang.String)
+     */
+    public void log(String msg) {
+        panel.add(new HTML(msg));
+        System.out.println(msg);
+    }
+
+    /*
+     * (non-Javadoc)
+     * 
+     * @see com.itmill.toolkit.terminal.gwt.client.Console#error(java.lang.String)
+     */
+    public void error(String msg) {
+        panel.add((new HTML(msg)));
+        System.out.println(msg);
+    }
+
+    /*
+     * (non-Javadoc)
+     * 
+     * @see com.itmill.toolkit.terminal.gwt.client.Console#printObject(java.lang.Object)
+     */
+    public void printObject(Object msg) {
+        panel.add((new Label(msg.toString())));
+    }
+
+    /*
+     * (non-Javadoc)
+     * 
+     * @see com.itmill.toolkit.terminal.gwt.client.Console#dirUIDL(com.itmill.toolkit.terminal.gwt.client.UIDL)
+     */
+    public void dirUIDL(UIDL u) {
+        panel.add(u.print_r());
+    }
+
+    public void setSize(Event event, boolean updateVariables) {
+        super.setSize(event, false);
+    }
+
+    public void onScroll(Widget widget, int scrollLeft, int scrollTop) {
+
+    }
+
+    public void setPopupPosition(int left, int top) {
+        // Keep the popup within the browser's client area, so that they can't
+        // get
+        // 'lost' and become impossible to interact with. Note that we don't
+        // attempt
+        // to keep popups pegged to the bottom and right edges, as they will
+        // then
+        // cause scrollbars to appear, so the user can't lose them.
+        if (left < 0) {
+            left = 0;
+        }
+        if (top < 0) {
+            top = 0;
+        }
+
+        // Set the popup's position manually, allowing setPopupPosition() to be
+        // called before show() is called (so a popup can be positioned without
+        // it
+        // 'jumping' on the screen).
+        Element elem = getElement();
+        DOM.setStyleAttribute(elem, "left", left + "px");
+        DOM.setStyleAttribute(elem, "top", top + "px");
+    }
+
+}
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/DefaultWidgetSet.java b/src/com/itmill/toolkit/terminal/terminal/gwt/client/DefaultWidgetSet.java
new file mode 100644 (file)
index 0000000..227becb
--- /dev/null
@@ -0,0 +1,343 @@
+/* 
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.itmill.toolkit.terminal.gwt.client;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.user.client.ui.Widget;
+import com.itmill.toolkit.terminal.gwt.client.ui.IAccordion;
+import com.itmill.toolkit.terminal.gwt.client.ui.IButton;
+import com.itmill.toolkit.terminal.gwt.client.ui.ICheckBox;
+import com.itmill.toolkit.terminal.gwt.client.ui.ICustomComponent;
+import com.itmill.toolkit.terminal.gwt.client.ui.ICustomLayout;
+import com.itmill.toolkit.terminal.gwt.client.ui.IDateFieldCalendar;
+import com.itmill.toolkit.terminal.gwt.client.ui.IEmbedded;
+import com.itmill.toolkit.terminal.gwt.client.ui.IExpandLayout;
+import com.itmill.toolkit.terminal.gwt.client.ui.IFilterSelect;
+import com.itmill.toolkit.terminal.gwt.client.ui.IForm;
+import com.itmill.toolkit.terminal.gwt.client.ui.IFormLayout;
+import com.itmill.toolkit.terminal.gwt.client.ui.IGridLayout;
+import com.itmill.toolkit.terminal.gwt.client.ui.IHorizontalExpandLayout;
+import com.itmill.toolkit.terminal.gwt.client.ui.ILabel;
+import com.itmill.toolkit.terminal.gwt.client.ui.ILink;
+import com.itmill.toolkit.terminal.gwt.client.ui.IListSelect;
+import com.itmill.toolkit.terminal.gwt.client.ui.IMenuBar;
+import com.itmill.toolkit.terminal.gwt.client.ui.INativeSelect;
+import com.itmill.toolkit.terminal.gwt.client.ui.IOptionGroup;
+import com.itmill.toolkit.terminal.gwt.client.ui.IOrderedLayoutHorizontal;
+import com.itmill.toolkit.terminal.gwt.client.ui.IOrderedLayoutVertical;
+import com.itmill.toolkit.terminal.gwt.client.ui.IPanel;
+import com.itmill.toolkit.terminal.gwt.client.ui.IPasswordField;
+import com.itmill.toolkit.terminal.gwt.client.ui.IPopupCalendar;
+import com.itmill.toolkit.terminal.gwt.client.ui.IProgressIndicator;
+import com.itmill.toolkit.terminal.gwt.client.ui.IScrollTable;
+import com.itmill.toolkit.terminal.gwt.client.ui.ISlider;
+import com.itmill.toolkit.terminal.gwt.client.ui.ISplitPanelHorizontal;
+import com.itmill.toolkit.terminal.gwt.client.ui.ISplitPanelVertical;
+import com.itmill.toolkit.terminal.gwt.client.ui.ITablePaging;
+import com.itmill.toolkit.terminal.gwt.client.ui.ITabsheet;
+import com.itmill.toolkit.terminal.gwt.client.ui.ITextArea;
+import com.itmill.toolkit.terminal.gwt.client.ui.ITextField;
+import com.itmill.toolkit.terminal.gwt.client.ui.ITextualDate;
+import com.itmill.toolkit.terminal.gwt.client.ui.ITree;
+import com.itmill.toolkit.terminal.gwt.client.ui.ITwinColSelect;
+import com.itmill.toolkit.terminal.gwt.client.ui.IUnknownComponent;
+import com.itmill.toolkit.terminal.gwt.client.ui.IUpload;
+import com.itmill.toolkit.terminal.gwt.client.ui.IWindow;
+import com.itmill.toolkit.terminal.gwt.client.ui.absolutegrid.ISizeableGridLayout;
+import com.itmill.toolkit.terminal.gwt.client.ui.absolutegrid.ISizeableOrderedLayout;
+import com.itmill.toolkit.terminal.gwt.client.ui.richtextarea.IRichTextArea;
+
+public class DefaultWidgetSet implements WidgetSet {
+
+    /**
+     * This is the entry point method.
+     */
+    public void onModuleLoad() {
+        ArrayList appIds = new ArrayList();
+        ApplicationConfiguration.loadAppIdListFromDOM(appIds);
+        for (Iterator iterator = appIds.iterator(); iterator.hasNext();) {
+            String appId = (String) iterator.next();
+            ApplicationConfiguration appConf = ApplicationConfiguration
+                    .getConfigFromDOM(appId);
+            new ApplicationConnection(this, appConf);
+        }
+    }
+
+    public Widget createWidget(UIDL uidl) {
+
+        final String className = resolveWidgetTypeName(uidl);
+        if ("com.itmill.toolkit.terminal.gwt.client.ui.ICheckBox"
+                .equals(className)) {
+            return new ICheckBox();
+        } else if ("com.itmill.toolkit.terminal.gwt.client.ui.IButton"
+                .equals(className)) {
+            return new IButton();
+        } else if ("com.itmill.toolkit.terminal.gwt.client.ui.IWindow"
+                .equals(className)) {
+            return new IWindow();
+        } else if ("com.itmill.toolkit.terminal.gwt.client.ui.absolutegrid.ISizeableOrderedLayout"
+                .equals(className)) {
+            return new ISizeableOrderedLayout();
+        } else if ("com.itmill.toolkit.terminal.gwt.client.ui.IOrderedLayoutVertical"
+                .equals(className)) {
+            return new IOrderedLayoutVertical();
+        } else if ("com.itmill.toolkit.terminal.gwt.client.ui.IOrderedLayoutHorizontal"
+                .equals(className)) {
+            return new IOrderedLayoutHorizontal();
+        } else if ("com.itmill.toolkit.terminal.gwt.client.ui.ILabel"
+                .equals(className)) {
+            return new ILabel();
+        } else if ("com.itmill.toolkit.terminal.gwt.client.ui.ILink"
+                .equals(className)) {
+            return new ILink();
+        } else if ("com.itmill.toolkit.terminal.gwt.client.ui.absolutegrid.ISizeableGridLayout"
+                .equals(className)) {
+            return new ISizeableGridLayout();
+        } else if ("com.itmill.toolkit.terminal.gwt.client.ui.IGridLayout"
+                .equals(className)) {
+            return new IGridLayout();
+        } else if ("com.itmill.toolkit.terminal.gwt.client.ui.ITree"
+                .equals(className)) {
+            return new ITree();
+        } else if ("com.itmill.toolkit.terminal.gwt.client.ui.IOptionGroup"
+                .equals(className)) {
+            return new IOptionGroup();
+        } else if ("com.itmill.toolkit.terminal.gwt.client.ui.ITwinColSelect"
+                .equals(className)) {
+            return new ITwinColSelect();
+        } else if ("com.itmill.toolkit.terminal.gwt.client.ui.INativeSelect"
+                .equals(className)) {
+            return new INativeSelect();
+        } else if ("com.itmill.toolkit.terminal.gwt.client.ui.IListSelect"
+                .equals(className)) {
+            return new IListSelect();
+        } else if ("com.itmill.toolkit.terminal.gwt.client.ui.IPanel"
+                .equals(className)) {
+            return new IPanel();
+        } else if ("com.itmill.toolkit.terminal.gwt.client.ui.ITabsheet"
+                .equals(className)) {
+            return new ITabsheet();
+        } else if ("com.itmill.toolkit.terminal.gwt.client.ui.IEmbedded"
+                .equals(className)) {
+            return new IEmbedded();
+        } else if ("com.itmill.toolkit.terminal.gwt.client.ui.ICustomLayout"
+                .equals(className)) {
+            return new ICustomLayout();
+        } else if ("com.itmill.toolkit.terminal.gwt.client.ui.ICustomComponent"
+                .equals(className)) {
+            return new ICustomComponent();
+        } else if ("com.itmill.toolkit.terminal.gwt.client.ui.ITextArea"
+                .equals(className)) {
+            return new ITextArea();
+        } else if ("com.itmill.toolkit.terminal.gwt.client.ui.IPasswordField"
+                .equals(className)) {
+            return new IPasswordField();
+        } else if ("com.itmill.toolkit.terminal.gwt.client.ui.ITextField"
+                .equals(className)) {
+            return new ITextField();
+        } else if ("com.itmill.toolkit.terminal.gwt.client.ui.ITablePaging"
+                .equals(className)) {
+            return new ITablePaging();
+        } else if ("com.itmill.toolkit.terminal.gwt.client.ui.IScrollTable"
+                .equals(className)) {
+            return new IScrollTable();
+        } else if ("com.itmill.toolkit.terminal.gwt.client.ui.IDateFieldCalendar"
+                .equals(className)) {
+            return new IDateFieldCalendar();
+        } else if ("com.itmill.toolkit.terminal.gwt.client.ui.ITextualDate"
+                .equals(className)) {
+            return new ITextualDate();
+        } else if ("com.itmill.toolkit.terminal.gwt.client.ui.IPopupCalendar"
+                .equals(className)) {
+            return new IPopupCalendar();
+        } else if ("com.itmill.toolkit.terminal.gwt.client.ui.ISlider"
+                .equals(className)) {
+            return new ISlider();
+        } else if ("com.itmill.toolkit.terminal.gwt.client.ui.IForm"
+                .equals(className)) {
+            return new IForm();
+        } else if ("com.itmill.toolkit.terminal.gwt.client.ui.IFormLayout"
+                .equals(className)) {
+            return new IFormLayout();
+        } else if ("com.itmill.toolkit.terminal.gwt.client.ui.IUpload"
+                .equals(className)) {
+            return new IUpload();
+        } else if ("com.itmill.toolkit.terminal.gwt.client.ui.ISplitPanelHorizontal"
+                .equals(className)) {
+            return new ISplitPanelHorizontal();
+        } else if ("com.itmill.toolkit.terminal.gwt.client.ui.ISplitPanelVertical"
+                .equals(className)) {
+            return new ISplitPanelVertical();
+        } else if ("com.itmill.toolkit.terminal.gwt.client.ui.IFilterSelect"
+                .equals(className)) {
+            return new IFilterSelect();
+        } else if ("com.itmill.toolkit.terminal.gwt.client.ui.IProgressIndicator"
+                .equals(className)) {
+            return new IProgressIndicator();
+        } else if ("com.itmill.toolkit.terminal.gwt.client.ui.IExpandLayout"
+                .equals(className)) {
+            return new IExpandLayout();
+        } else if ("com.itmill.toolkit.terminal.gwt.client.ui.IHorizontalExpandLayout"
+                .equals(className)) {
+            return new IHorizontalExpandLayout();
+        } else if ("com.itmill.toolkit.terminal.gwt.client.ui.richtextarea.IRichTextArea"
+                .equals(className)) {
+            return new IRichTextArea();
+        } else if ("com.itmill.toolkit.terminal.gwt.client.ui.IAccordion"
+                .equals(className)) {
+            return new IAccordion();
+        } else if ("com.itmill.toolkit.terminal.gwt.client.ui.IMenuBar"
+                .equals(className)) {
+            return new IMenuBar();
+        }
+
+        return new IUnknownComponent();
+
+        /*
+         * TODO: Class based impl, use when GWT supports return
+         * (Widget)GWT.create(resolveWidgetClass(uidl));
+         */
+    }
+
+    protected String resolveWidgetTypeName(UIDL uidl) {
+
+        final String tag = uidl.getTag();
+        if ("button".equals(tag)) {
+            if ("switch".equals(uidl.getStringAttribute("type"))) {
+                return "com.itmill.toolkit.terminal.gwt.client.ui.ICheckBox";
+            } else {
+                return "com.itmill.toolkit.terminal.gwt.client.ui.IButton";
+            }
+        } else if ("window".equals(tag)) {
+            return "com.itmill.toolkit.terminal.gwt.client.ui.IWindow";
+        } else if ("orderedlayout".equals(tag)) {
+            if ("horizontal".equals(uidl.getStringAttribute("orientation"))) {
+                if (uidl.hasAttribute("height") && uidl.hasAttribute("width")) {
+                    return "com.itmill.toolkit.terminal.gwt.client.ui.absolutegrid.ISizeableOrderedLayout";
+                } else {
+                    return "com.itmill.toolkit.terminal.gwt.client.ui.IOrderedLayoutHorizontal";
+                }
+            } else {
+                if (uidl.hasAttribute("height")) {
+                    return "com.itmill.toolkit.terminal.gwt.client.ui.absolutegrid.ISizeableOrderedLayout";
+                } else {
+                    return "com.itmill.toolkit.terminal.gwt.client.ui.IOrderedLayoutVertical";
+                }
+            }
+        } else if ("label".equals(tag)) {
+            return "com.itmill.toolkit.terminal.gwt.client.ui.ILabel";
+        } else if ("link".equals(tag)) {
+            return "com.itmill.toolkit.terminal.gwt.client.ui.ILink";
+        } else if ("gridlayout".equals(tag)) {
+            if (uidl.hasAttribute("height")) {
+                // height needs to be set to use sizeable grid layout, with
+                // width only or no size at all it fails to render properly.
+                return "com.itmill.toolkit.terminal.gwt.client.ui.absolutegrid.ISizeableGridLayout";
+            } else {
+                // Fall back to GWT FlexTable based implementation.
+                return "com.itmill.toolkit.terminal.gwt.client.ui.IGridLayout";
+            }
+        } else if ("tree".equals(tag)) {
+            return "com.itmill.toolkit.terminal.gwt.client.ui.ITree";
+        } else if ("select".equals(tag)) {
+            if (uidl.hasAttribute("type")) {
+                final String type = uidl.getStringAttribute("type");
+                if (type.equals("twincol")) {
+                    return "com.itmill.toolkit.terminal.gwt.client.ui.ITwinColSelect";
+                }
+                if (type.equals("optiongroup")) {
+                    return "com.itmill.toolkit.terminal.gwt.client.ui.IOptionGroup";
+                }
+                if (type.equals("native")) {
+                    return "com.itmill.toolkit.terminal.gwt.client.ui.INativeSelect";
+                }
+                if (type.equals("list")) {
+                    return "com.itmill.toolkit.terminal.gwt.client.ui.IListSelect";
+                }
+            } else {
+                if (uidl.hasAttribute("selectmode")
+                        && uidl.getStringAttribute("selectmode")
+                                .equals("multi")) {
+                    return "com.itmill.toolkit.terminal.gwt.client.ui.IListSelect";
+                } else {
+                    return "com.itmill.toolkit.terminal.gwt.client.ui.IFilterSelect";
+                }
+            }
+        } else if ("panel".equals(tag)) {
+            return "com.itmill.toolkit.terminal.gwt.client.ui.IPanel";
+        } else if ("tabsheet".equals(tag)) {
+            return "com.itmill.toolkit.terminal.gwt.client.ui.ITabsheet";
+        } else if ("accordion".equals(tag)) {
+            return "com.itmill.toolkit.terminal.gwt.client.ui.IAccordion";
+        } else if ("embedded".equals(tag)) {
+            return "com.itmill.toolkit.terminal.gwt.client.ui.IEmbedded";
+        } else if ("customlayout".equals(tag)) {
+            return "com.itmill.toolkit.terminal.gwt.client.ui.ICustomLayout";
+        } else if ("customcomponent".equals(tag)) {
+            return "com.itmill.toolkit.terminal.gwt.client.ui.ICustomComponent";
+        } else if ("textfield".equals(tag)) {
+            if (uidl.getBooleanAttribute("richtext")) {
+                return "com.itmill.toolkit.terminal.gwt.client.ui.richtextarea.IRichTextArea";
+            } else if (uidl.hasAttribute("multiline")) {
+                return "com.itmill.toolkit.terminal.gwt.client.ui.ITextArea";
+            } else if (uidl.getBooleanAttribute("secret")) {
+                return "com.itmill.toolkit.terminal.gwt.client.ui.IPasswordField";
+            } else {
+                return "com.itmill.toolkit.terminal.gwt.client.ui.ITextField";
+            }
+        } else if ("table".equals(tag)) {
+            return "com.itmill.toolkit.terminal.gwt.client.ui.IScrollTable";
+        } else if ("pagingtable".equals(tag)) {
+            return "com.itmill.toolkit.terminal.gwt.client.ui.ITablePaging";
+        } else if ("datefield".equals(tag)) {
+            if (uidl.hasAttribute("type")) {
+                if ("inline".equals(uidl.getStringAttribute("type"))) {
+                    return "com.itmill.toolkit.terminal.gwt.client.ui.IDateFieldCalendar";
+                } else if ("popup".equals(uidl.getStringAttribute("type"))) {
+                    return "com.itmill.toolkit.terminal.gwt.client.ui.IPopupCalendar";
+                }
+            }
+            // popup calendar is the default
+            return "com.itmill.toolkit.terminal.gwt.client.ui.IPopupCalendar";
+        } else if ("slider".equals(tag)) {
+            return "com.itmill.toolkit.terminal.gwt.client.ui.ISlider";
+        } else if ("form".equals(tag)) {
+            return "com.itmill.toolkit.terminal.gwt.client.ui.IForm";
+        } else if ("formlayout".equals(tag)) {
+            return "com.itmill.toolkit.terminal.gwt.client.ui.IFormLayout";
+        } else if ("upload".equals(tag)) {
+            return "com.itmill.toolkit.terminal.gwt.client.ui.IUpload";
+        } else if ("hsplitpanel".equals(tag)) {
+            return "com.itmill.toolkit.terminal.gwt.client.ui.ISplitPanelHorizontal";
+        } else if ("vsplitpanel".equals(tag)) {
+            return "com.itmill.toolkit.terminal.gwt.client.ui.ISplitPanelVertical";
+        } else if ("progressindicator".equals(tag)) {
+            return "com.itmill.toolkit.terminal.gwt.client.ui.IProgressIndicator";
+        } else if ("expandlayout".equals(tag)) {
+            if ("horizontal".equals(uidl.getStringAttribute("orientation"))) {
+                return "com.itmill.toolkit.terminal.gwt.client.ui.IHorizontalExpandLayout";
+            } else {
+                return "com.itmill.toolkit.terminal.gwt.client.ui.IExpandLayout";
+            }
+        } else if ("menubar".equals(tag)) {
+            return "com.itmill.toolkit.terminal.gwt.client.ui.IMenuBar";
+        }
+
+        return "com.itmill.toolkit.terminal.gwt.client.ui.IUnknownComponent";
+
+        /*
+         * TODO: use class based impl when GWT supports it
+         */
+    }
+
+    public boolean isCorrectImplementation(Widget currentWidget, UIDL uidl) {
+        return GWT.getTypeName(currentWidget).equals(
+                resolveWidgetTypeName(uidl));
+    }
+
+}
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/ErrorMessage.java b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ErrorMessage.java
new file mode 100644 (file)
index 0000000..e4ca0f1
--- /dev/null
@@ -0,0 +1,73 @@
+/* 
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.itmill.toolkit.terminal.gwt.client;
+
+import java.util.Iterator;
+
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.Element;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.HTML;
+import com.itmill.toolkit.terminal.gwt.client.ui.ToolkitOverlay;
+
+public class ErrorMessage extends FlowPanel {
+    public static final String CLASSNAME = "i-errormessage";
+
+    public ErrorMessage() {
+        super();
+        setStyleName(CLASSNAME);
+    }
+
+    public void updateFromUIDL(UIDL uidl) {
+        clear();
+        if (uidl.getChildCount() == 0) {
+            add(new HTML(" "));
+        } else {
+            for (final Iterator it = uidl.getChildIterator(); it.hasNext();) {
+                final Object child = it.next();
+                if (child instanceof String) {
+                    final String errorMessage = (String) child;
+                    add(new HTML(errorMessage));
+                } else if (child instanceof UIDL.XML) {
+                    final UIDL.XML xml = (UIDL.XML) child;
+                    add(new HTML(xml.getXMLAsString()));
+                } else {
+                    final ErrorMessage childError = new ErrorMessage();
+                    add(childError);
+                    childError.updateFromUIDL((UIDL) child);
+                }
+            }
+        }
+    }
+
+    /**
+     * Shows this error message next to given element.
+     * 
+     * @param indicatorElement
+     */
+    public void showAt(Element indicatorElement) {
+        ToolkitOverlay errorContainer = (ToolkitOverlay) getParent();
+        if (errorContainer == null) {
+            errorContainer = new ToolkitOverlay();
+            errorContainer.setWidget(this);
+        }
+        errorContainer.setPopupPosition(DOM.getAbsoluteLeft(indicatorElement)
+                + 2
+                * DOM.getElementPropertyInt(indicatorElement, "offsetHeight"),
+                DOM.getAbsoluteTop(indicatorElement)
+                        + 2
+                        * DOM.getElementPropertyInt(indicatorElement,
+                                "offsetHeight"));
+        errorContainer.show();
+
+    }
+
+    public void hide() {
+        final ToolkitOverlay errorContainer = (ToolkitOverlay) getParent();
+        if (errorContainer != null) {
+            errorContainer.hide();
+        }
+    }
+}
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/Focusable.java b/src/com/itmill/toolkit/terminal/terminal/gwt/client/Focusable.java
new file mode 100644 (file)
index 0000000..43d07a3
--- /dev/null
@@ -0,0 +1,16 @@
+package com.itmill.toolkit.terminal.gwt.client;
+
+/**
+ * GWT's HasFocus is way too overkill for just receiving focus in simple
+ * components. Toolkit uses this interface in addition to GWT's HasFocus to pass
+ * focus requests from server to actual ui widgets in browsers.
+ * 
+ * So in to make your server side focusable component receive focus on client
+ * side it must either implement this or HasFocus interface.
+ */
+public interface Focusable {
+    /**
+     * Sets focus to this widget.
+     */
+    public void focus();
+}
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/LocaleNotLoadedException.java b/src/com/itmill/toolkit/terminal/terminal/gwt/client/LocaleNotLoadedException.java
new file mode 100644 (file)
index 0000000..39c38bd
--- /dev/null
@@ -0,0 +1,17 @@
+/* \r
+@ITMillApache2LicenseForJavaFiles@\r
+ */\r
+\r
+package com.itmill.toolkit.terminal.gwt.client;\r
+\r
+public class LocaleNotLoadedException extends Exception {\r
+\r
+    /**\r
+     * Serial generated by Eclipse.\r
+     */\r
+    private static final long serialVersionUID = 2005227056545210838L;\r
+\r
+    public LocaleNotLoadedException(String locale) {\r
+        super(locale);\r
+    }\r
+}\r
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/LocaleService.java b/src/com/itmill/toolkit/terminal/terminal/gwt/client/LocaleService.java
new file mode 100644 (file)
index 0000000..ed5a843
--- /dev/null
@@ -0,0 +1,197 @@
+/* \r
+@ITMillApache2LicenseForJavaFiles@\r
+ */\r
+\r
+package com.itmill.toolkit.terminal.gwt.client;\r
+\r
+import java.util.HashMap;\r
+import java.util.Map;\r
+import java.util.Set;\r
+\r
+import com.google.gwt.json.client.JSONArray;\r
+import com.google.gwt.json.client.JSONBoolean;\r
+import com.google.gwt.json.client.JSONNumber;\r
+import com.google.gwt.json.client.JSONObject;\r
+import com.google.gwt.json.client.JSONString;\r
+\r
+/**\r
+ * Date / time etc. localisation service for all widgets. Caches all loaded\r
+ * locales as JSONObjects.\r
+ * \r
+ * @author IT Mill Ltd.\r
+ * \r
+ */\r
+public class LocaleService {\r
+\r
+    private static Map cache = new HashMap();\r
+    private static String defaultLocale;\r
+\r
+    public static void addLocale(JSONObject json) {\r
+        final String key = ((JSONString) json.get("name")).stringValue();\r
+        if (cache.containsKey(key)) {\r
+            cache.remove(key);\r
+        }\r
+        cache.put(key, json);\r
+        if (cache.size() == 1) {\r
+            setDefaultLocale(key);\r
+        }\r
+    }\r
+\r
+    public static void setDefaultLocale(String locale) {\r
+        defaultLocale = locale;\r
+    }\r
+\r
+    public static String getDefaultLocale() {\r
+        return defaultLocale;\r
+    }\r
+\r
+    public static Set getAvailableLocales() {\r
+        return cache.keySet();\r
+    }\r
+\r
+    public static String[] getMonthNames(String locale)\r
+            throws LocaleNotLoadedException {\r
+        if (cache.containsKey(locale)) {\r
+            final JSONObject l = (JSONObject) cache.get(locale);\r
+            final JSONArray mn = (JSONArray) l.get("mn");\r
+            final String[] temp = new String[12];\r
+            temp[0] = ((JSONString) mn.get(0)).stringValue();\r
+            temp[1] = ((JSONString) mn.get(1)).stringValue();\r
+            temp[2] = ((JSONString) mn.get(2)).stringValue();\r
+            temp[3] = ((JSONString) mn.get(3)).stringValue();\r
+            temp[4] = ((JSONString) mn.get(4)).stringValue();\r
+            temp[5] = ((JSONString) mn.get(5)).stringValue();\r
+            temp[6] = ((JSONString) mn.get(6)).stringValue();\r
+            temp[7] = ((JSONString) mn.get(7)).stringValue();\r
+            temp[8] = ((JSONString) mn.get(8)).stringValue();\r
+            temp[9] = ((JSONString) mn.get(9)).stringValue();\r
+            temp[10] = ((JSONString) mn.get(10)).stringValue();\r
+            temp[11] = ((JSONString) mn.get(11)).stringValue();\r
+            return temp;\r
+        } else {\r
+            throw new LocaleNotLoadedException(locale);\r
+        }\r
+    }\r
+\r
+    public static String[] getShortMonthNames(String locale)\r
+            throws LocaleNotLoadedException {\r
+        if (cache.containsKey(locale)) {\r
+            final JSONObject l = (JSONObject) cache.get(locale);\r
+            final JSONArray smn = (JSONArray) l.get("smn");\r
+            final String[] temp = new String[12];\r
+            temp[0] = ((JSONString) smn.get(0)).stringValue();\r
+            temp[1] = ((JSONString) smn.get(1)).stringValue();\r
+            temp[2] = ((JSONString) smn.get(2)).stringValue();\r
+            temp[3] = ((JSONString) smn.get(3)).stringValue();\r
+            temp[4] = ((JSONString) smn.get(4)).stringValue();\r
+            temp[5] = ((JSONString) smn.get(5)).stringValue();\r
+            temp[6] = ((JSONString) smn.get(6)).stringValue();\r
+            temp[7] = ((JSONString) smn.get(7)).stringValue();\r
+            temp[8] = ((JSONString) smn.get(8)).stringValue();\r
+            temp[9] = ((JSONString) smn.get(9)).stringValue();\r
+            temp[10] = ((JSONString) smn.get(10)).stringValue();\r
+            temp[11] = ((JSONString) smn.get(11)).stringValue();\r
+            return temp;\r
+        } else {\r
+            throw new LocaleNotLoadedException(locale);\r
+        }\r
+    }\r
+\r
+    public static String[] getDayNames(String locale)\r
+            throws LocaleNotLoadedException {\r
+        if (cache.containsKey(locale)) {\r
+            final JSONObject l = (JSONObject) cache.get(locale);\r
+            final JSONArray dn = (JSONArray) l.get("dn");\r
+            final String[] temp = new String[7];\r
+            temp[0] = ((JSONString) dn.get(0)).stringValue();\r
+            temp[1] = ((JSONString) dn.get(1)).stringValue();\r
+            temp[2] = ((JSONString) dn.get(2)).stringValue();\r
+            temp[3] = ((JSONString) dn.get(3)).stringValue();\r
+            temp[4] = ((JSONString) dn.get(4)).stringValue();\r
+            temp[5] = ((JSONString) dn.get(5)).stringValue();\r
+            temp[6] = ((JSONString) dn.get(6)).stringValue();\r
+            return temp;\r
+        } else {\r
+            throw new LocaleNotLoadedException(locale);\r
+        }\r
+    }\r
+\r
+    public static String[] getShortDayNames(String locale)\r
+            throws LocaleNotLoadedException {\r
+        if (cache.containsKey(locale)) {\r
+            final JSONObject l = (JSONObject) cache.get(locale);\r
+            final JSONArray sdn = (JSONArray) l.get("sdn");\r
+            final String[] temp = new String[7];\r
+            temp[0] = ((JSONString) sdn.get(0)).stringValue();\r
+            temp[1] = ((JSONString) sdn.get(1)).stringValue();\r
+            temp[2] = ((JSONString) sdn.get(2)).stringValue();\r
+            temp[3] = ((JSONString) sdn.get(3)).stringValue();\r
+            temp[4] = ((JSONString) sdn.get(4)).stringValue();\r
+            temp[5] = ((JSONString) sdn.get(5)).stringValue();\r
+            temp[6] = ((JSONString) sdn.get(6)).stringValue();\r
+            return temp;\r
+        } else {\r
+            throw new LocaleNotLoadedException(locale);\r
+        }\r
+    }\r
+\r
+    public static int getFirstDayOfWeek(String locale)\r
+            throws LocaleNotLoadedException {\r
+        if (cache.containsKey(locale)) {\r
+            final JSONObject l = (JSONObject) cache.get(locale);\r
+            final JSONNumber fdow = (JSONNumber) l.get("fdow");\r
+            return (int) fdow.getValue();\r
+        } else {\r
+            throw new LocaleNotLoadedException(locale);\r
+        }\r
+    }\r
+\r
+    public static String getDateFormat(String locale)\r
+            throws LocaleNotLoadedException {\r
+        if (cache.containsKey(locale)) {\r
+            final JSONObject l = (JSONObject) cache.get(locale);\r
+            final JSONString df = (JSONString) l.get("df");\r
+            return df.stringValue();\r
+        } else {\r
+            throw new LocaleNotLoadedException(locale);\r
+        }\r
+    }\r
+\r
+    public static boolean isTwelveHourClock(String locale)\r
+            throws LocaleNotLoadedException {\r
+        if (cache.containsKey(locale)) {\r
+            final JSONObject l = (JSONObject) cache.get(locale);\r
+            final JSONBoolean thc = (JSONBoolean) l.get("thc");\r
+            return thc.booleanValue();\r
+        } else {\r
+            throw new LocaleNotLoadedException(locale);\r
+        }\r
+    }\r
+\r
+    public static String getClockDelimiter(String locale)\r
+            throws LocaleNotLoadedException {\r
+        if (cache.containsKey(locale)) {\r
+            final JSONObject l = (JSONObject) cache.get(locale);\r
+            final JSONString hmd = (JSONString) l.get("hmd");\r
+            return hmd.stringValue();\r
+        } else {\r
+            throw new LocaleNotLoadedException(locale);\r
+        }\r
+    }\r
+\r
+    public static String[] getAmPmStrings(String locale)\r
+            throws LocaleNotLoadedException {\r
+        if (cache.containsKey(locale)) {\r
+            final JSONObject l = (JSONObject) cache.get(locale);\r
+            final JSONArray ampm = (JSONArray) l.get("ampm");\r
+            final String[] temp = new String[2];\r
+            temp[0] = ((JSONString) ampm.get(0)).stringValue();\r
+            temp[1] = ((JSONString) ampm.get(1)).stringValue();\r
+            return temp;\r
+        } else {\r
+            throw new LocaleNotLoadedException(locale);\r
+        }\r
+\r
+    }\r
+\r
+}\r
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/NullConsole.java b/src/com/itmill/toolkit/terminal/terminal/gwt/client/NullConsole.java
new file mode 100644 (file)
index 0000000..2c158b6
--- /dev/null
@@ -0,0 +1,26 @@
+/* 
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.itmill.toolkit.terminal.gwt.client;
+
+/**
+ * Client side console implementation for non-debug mode that discards all
+ * messages.
+ * 
+ */
+public class NullConsole implements Console {
+
+    public void dirUIDL(UIDL u) {
+    }
+
+    public void error(String msg) {
+    }
+
+    public void log(String msg) {
+    }
+
+    public void printObject(Object msg) {
+    }
+
+}
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/Paintable.java b/src/com/itmill/toolkit/terminal/terminal/gwt/client/Paintable.java
new file mode 100644 (file)
index 0000000..81c024b
--- /dev/null
@@ -0,0 +1,10 @@
+/* 
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.itmill.toolkit.terminal.gwt.client;
+
+public interface Paintable {
+
+    public void updateFromUIDL(UIDL uidl, ApplicationConnection client);
+}
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/StyleConstants.java b/src/com/itmill/toolkit/terminal/terminal/gwt/client/StyleConstants.java
new file mode 100644 (file)
index 0000000..b4d5499
--- /dev/null
@@ -0,0 +1,17 @@
+/* 
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.itmill.toolkit.terminal.gwt.client;
+
+public class StyleConstants {
+
+    public static final String MARGIN_TOP = "margin-top";
+    public static final String MARGIN_RIGHT = "margin-right";
+    public static final String MARGIN_BOTTOM = "margin-bottom";
+    public static final String MARGIN_LEFT = "margin-left";
+    
+    public static final String VERTICAL_SPACING = "vspacing";
+    public static final String HORIZONTAL_SPACING = "hspacing";
+
+}
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/Tooltip.java b/src/com/itmill/toolkit/terminal/terminal/gwt/client/Tooltip.java
new file mode 100644 (file)
index 0000000..cf1d6bd
--- /dev/null
@@ -0,0 +1,185 @@
+/* 
+@ITMillApache2LicenseForJavaFiles@
+ */
+package com.itmill.toolkit.terminal.gwt.client;
+
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.Element;
+import com.google.gwt.user.client.Event;
+import com.google.gwt.user.client.Timer;
+import com.google.gwt.user.client.Window;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.itmill.toolkit.terminal.gwt.client.ui.ToolkitOverlay;
+
+/**
+ * TODO open for extension
+ */
+public class Tooltip extends ToolkitOverlay {
+    private static final String CLASSNAME = "i-tooltip";
+    private static final int MARGIN = 4;
+    public static final int TOOLTIP_EVENTS = Event.ONKEYDOWN
+            | Event.ONMOUSEOVER | Event.ONMOUSEOUT | Event.ONMOUSEMOVE
+            | Event.ONCLICK;
+    protected static final int MAX_WIDTH = 500;
+    ErrorMessage em = new ErrorMessage();
+    Element description = DOM.createDiv();
+    private Paintable tooltipOwner;
+    private boolean closing = false;
+    private boolean opening = false;
+    private ApplicationConnection ac;
+
+    public Tooltip(ApplicationConnection client) {
+        ac = client;
+        setStyleName(CLASSNAME);
+        FlowPanel layout = new FlowPanel();
+        setWidget(layout);
+        layout.add(em);
+        DOM.setElementProperty(description, "className", CLASSNAME + "-text");
+        DOM.appendChild(layout.getElement(), description);
+    }
+
+    private void show(TooltipInfo info) {
+        boolean hasContent = false;
+        if (info.getErrorUidl() != null) {
+            em.setVisible(true);
+            em.updateFromUIDL(info.getErrorUidl());
+            hasContent = true;
+        } else {
+            em.setVisible(false);
+        }
+        if (info.getTitle() != null && !"".equals(info.getTitle())) {
+            DOM.setInnerHTML(description, info.getTitle());
+            DOM.setStyleAttribute(description, "display", "");
+            hasContent = true;
+        } else {
+            DOM.setInnerHTML(description, "");
+            DOM.setStyleAttribute(description, "display", "none");
+        }
+        if (hasContent) {
+            setPopupPositionAndShow(new PositionCallback() {
+                public void setPosition(int offsetWidth, int offsetHeight) {
+
+                    if (offsetWidth > MAX_WIDTH) {
+                        setWidth(MAX_WIDTH + "px");
+                    }
+
+                    offsetWidth = getOffsetWidth();
+
+                    int x = tooltipEventMouseX + 10 + Window.getScrollLeft();
+                    int y = tooltipEventMouseY + 10 + Window.getScrollTop();
+
+                    if (x + offsetWidth + MARGIN - Window.getScrollLeft() > Window
+                            .getClientWidth()) {
+                        x = Window.getClientWidth() - offsetWidth - MARGIN;
+                    }
+
+                    if (y + offsetHeight + MARGIN - Window.getScrollTop() > Window
+                            .getClientHeight()) {
+                        y = tooltipEventMouseY - 5 - offsetHeight;
+                    }
+
+                    setPopupPosition(x, y);
+                    sinkEvents(Event.ONMOUSEOVER | Event.ONMOUSEOUT);
+                }
+            });
+        } else {
+            hide();
+        }
+    }
+
+    public void showTooltip(Paintable owner, Event event) {
+        if (closing && tooltipOwner == owner) {
+            closeTimer.cancel();
+            closing = false;
+            return;
+        }
+        updatePosition(event);
+
+        if (opening) {
+            showTimer.cancel();
+        }
+        tooltipOwner = owner;
+        showTimer.schedule(1000);
+        opening = true;
+
+    }
+
+    private Timer showTimer = new Timer() {
+        public void run() {
+            TooltipInfo info = ac.getTitleInfo(tooltipOwner);
+            show(info);
+            opening = false;
+
+        }
+    };
+
+    private Timer closeTimer = new Timer() {
+        public void run() {
+            hide();
+            closing = false;
+            tooltipOwner = null;
+            setWidth("");
+        }
+    };
+
+    public void hideTooltip() {
+        if (opening) {
+            showTimer.cancel();
+            opening = false;
+            tooltipOwner = null;
+        }
+        if (!isAttached()) {
+            return;
+        }
+        if (closing) {
+            // already about to close
+            return;
+        }
+        closeTimer.schedule(300);
+        closing = true;
+    }
+
+    private int tooltipEventMouseX;
+    private int tooltipEventMouseY;
+
+    public void updatePosition(Event event) {
+        tooltipEventMouseX = DOM.eventGetClientX(event);
+        tooltipEventMouseY = DOM.eventGetClientY(event);
+
+    }
+
+    public void handleTooltipEvent(Event event, Paintable owner) {
+        final int type = DOM.eventGetType(event);
+        if ((Tooltip.TOOLTIP_EVENTS & type) == type) {
+            if (type == Event.ONMOUSEOVER) {
+                showTooltip(owner, event);
+            } else if (type == Event.ONMOUSEMOVE) {
+                updatePosition(event);
+            } else {
+                hideTooltip();
+            }
+        } else {
+            // non-tooltip event, hide tooltip
+            hideTooltip();
+        }
+    }
+
+    public void onBrowserEvent(Event event) {
+        final int type = DOM.eventGetType(event);
+        // cancel closing event if tooltip is mouseovered; the user might want
+        // to scroll of cut&paste
+
+        switch (type) {
+        case Event.ONMOUSEOVER:
+            closeTimer.cancel();
+            closing = false;
+            break;
+        case Event.ONMOUSEOUT:
+            hideTooltip();
+            break;
+        default:
+            // NOP
+        }
+    }
+
+}
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/TooltipInfo.java b/src/com/itmill/toolkit/terminal/terminal/gwt/client/TooltipInfo.java
new file mode 100644 (file)
index 0000000..0ac23f3
--- /dev/null
@@ -0,0 +1,28 @@
+/* 
+@ITMillApache2LicenseForJavaFiles@
+ */
+package com.itmill.toolkit.terminal.gwt.client;
+
+public class TooltipInfo {
+
+    private String title;
+
+    private UIDL errorUidl;
+
+    public String getTitle() {
+        return title;
+    }
+
+    public void setTitle(String title) {
+        this.title = title;
+    }
+
+    public UIDL getErrorUidl() {
+        return errorUidl;
+    }
+
+    public void setErrorUidl(UIDL errorUidl) {
+        this.errorUidl = errorUidl;
+    }
+
+}
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/UIDL.java b/src/com/itmill/toolkit/terminal/terminal/gwt/client/UIDL.java
new file mode 100644 (file)
index 0000000..0b8d319
--- /dev/null
@@ -0,0 +1,470 @@
+/* 
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.itmill.toolkit.terminal.gwt.client;
+
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Set;
+
+import com.google.gwt.json.client.JSONArray;
+import com.google.gwt.json.client.JSONBoolean;
+import com.google.gwt.json.client.JSONNumber;
+import com.google.gwt.json.client.JSONObject;
+import com.google.gwt.json.client.JSONString;
+import com.google.gwt.json.client.JSONValue;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.ui.Tree;
+import com.google.gwt.user.client.ui.TreeItem;
+import com.google.gwt.user.client.ui.TreeListener;
+
+public class UIDL {
+
+    JSONArray json;
+
+    public UIDL(JSONArray json) {
+        this.json = json;
+    }
+
+    public String getId() {
+        final JSONValue val = ((JSONObject) json.get(1)).get("id");
+        if (val == null) {
+            return null;
+        }
+        return ((JSONString) val).stringValue();
+    }
+
+    public String getTag() {
+        return ((JSONString) json.get(0)).stringValue();
+    }
+
+    public String getStringAttribute(String name) {
+        final JSONValue val = ((JSONObject) json.get(1)).get(name);
+        if (val == null) {
+            return null;
+        }
+        return ((JSONString) val).stringValue();
+    }
+
+    public Set getAttributeNames() {
+        final HashSet attrs = new HashSet(((JSONObject) json.get(1)).keySet());
+        attrs.remove("v");
+        return attrs;
+    }
+
+    public int getIntAttribute(String name) {
+        final JSONValue val = ((JSONObject) json.get(1)).get(name);
+        if (val == null) {
+            return 0;
+        }
+        final double num = ((JSONNumber) val).getValue();
+        return (int) num;
+    }
+
+    public long getLongAttribute(String name) {
+        final JSONValue val = ((JSONObject) json.get(1)).get(name);
+        if (val == null) {
+            return 0;
+        }
+        final double num = ((JSONNumber) val).getValue();
+        return (long) num;
+    }
+
+    public float getFloatAttribute(String name) {
+        final JSONValue val = ((JSONObject) json.get(1)).get(name);
+        if (val == null) {
+            return 0;
+        }
+        final double num = ((JSONNumber) val).getValue();
+        return (float) num;
+    }
+
+    public double getDoubleAttribute(String name) {
+        final JSONValue val = ((JSONObject) json.get(1)).get(name);
+        if (val == null) {
+            return 0;
+        }
+        final double num = ((JSONNumber) val).getValue();
+        return num;
+    }
+
+    public boolean getBooleanAttribute(String name) {
+        final JSONValue val = ((JSONObject) json.get(1)).get(name);
+        if (val == null) {
+            return false;
+        }
+        return ((JSONBoolean) val).booleanValue();
+    }
+
+    public String[] getStringArrayAttribute(String name) {
+        final JSONArray a = (JSONArray) ((JSONObject) json.get(1)).get(name);
+        final String[] s = new String[a.size()];
+        for (int i = 0; i < a.size(); i++) {
+            s[i] = ((JSONString) a.get(i)).stringValue();
+        }
+        return s;
+    }
+
+    public int[] getIntArrayAttribute(String name) {
+        final JSONArray a = (JSONArray) ((JSONObject) json.get(1)).get(name);
+        final int[] s = new int[a.size()];
+        for (int i = 0; i < a.size(); i++) {
+            s[i] = Integer.parseInt(((JSONString) a.get(i)).stringValue());
+        }
+        return s;
+    }
+
+    public HashSet getStringArrayAttributeAsSet(String string) {
+        final JSONArray a = getArrayVariable(string);
+        final HashSet s = new HashSet();
+        for (int i = 0; i < a.size(); i++) {
+            s.add(((JSONString) a.get(i)).stringValue());
+        }
+        return s;
+    }
+
+    /**
+     * Get attributes value as string whateever the type is
+     * 
+     * @param name
+     * @return string presentation of attribute
+     */
+    private String getAttribute(String name) {
+        return json.get(1).isObject().get(name).toString();
+    }
+
+    public boolean hasAttribute(String name) {
+        return ((JSONObject) json.get(1)).get(name) != null;
+    }
+
+    public UIDL getChildUIDL(int i) {
+
+        final JSONValue c = json.get(i + 2);
+        if (c == null) {
+            return null;
+        }
+        if (c.isArray() != null) {
+            return new UIDL(c.isArray());
+        }
+        throw new IllegalStateException("Child node " + i
+                + " is not of type UIDL");
+    }
+
+    public String getChildString(int i) {
+
+        final JSONValue c = json.get(i + 2);
+        if (c.isString() != null) {
+            return ((JSONString) c).stringValue();
+        }
+        throw new IllegalStateException("Child node " + i
+                + " is not of type String");
+    }
+
+    public Iterator getChildIterator() {
+
+        return new Iterator() {
+
+            int index = 2;
+
+            public void remove() {
+                throw new UnsupportedOperationException();
+            }
+
+            public Object next() {
+
+                if (json.size() > index) {
+                    final JSONValue c = json.get(index++);
+                    if (c.isString() != null) {
+                        return c.isString().stringValue();
+                    } else if (c.isArray() != null) {
+                        return new UIDL(c.isArray());
+                    } else if (c.isObject() != null) {
+                        return new XML(c.isObject());
+                    } else {
+                        throw new IllegalStateException("Illegal child " + c
+                                + " in tag " + getTag() + " at index " + index);
+                    }
+                }
+                return null;
+            }
+
+            public boolean hasNext() {
+                return json.size() > index;
+            }
+
+        };
+    }
+
+    public int getNumberOfChildren() {
+        return json.size() - 2;
+    }
+
+    public String toString() {
+        String s = "<" + getTag();
+
+        for (final Iterator i = getAttributeNames().iterator(); i.hasNext();) {
+            final String name = i.next().toString();
+            s += " " + name + "=";
+            final JSONValue v = ((JSONObject) json.get(1)).get(name);
+            if (v.isString() != null) {
+                s += v;
+            } else {
+                s += "\"" + v + "\"";
+            }
+        }
+
+        s += ">\n";
+
+        final Iterator i = getChildIterator();
+        while (i.hasNext()) {
+            final Object c = i.next();
+            s += c.toString();
+        }
+
+        s += "</" + getTag() + ">\n";
+
+        return s;
+    }
+
+    public String getChildrenAsXML() {
+        String s = "";
+        final Iterator i = getChildIterator();
+        while (i.hasNext()) {
+            final Object c = i.next();
+            s += c.toString();
+        }
+        return s;
+    }
+
+    public UIDLBrowser print_r() {
+        return new UIDLBrowser();
+    }
+
+    private class UIDLBrowser extends Tree {
+        public UIDLBrowser() {
+
+            DOM.setStyleAttribute(getElement(), "position", "");
+
+            final TreeItem root = new TreeItem(getTag());
+            addItem(root);
+            root.addItem("");
+            addTreeListener(new TreeListener() {
+
+                public void onTreeItemStateChanged(TreeItem item) {
+                    if (item == root) {
+                        removeItem(root);
+                        UIDLBrowser.this.removeTreeListener(this);
+                        addItem(dir());
+                        final Iterator it = treeItemIterator();
+                        while (it.hasNext()) {
+                            ((TreeItem) it.next()).setState(true);
+                        }
+                    }
+                }
+
+                public void onTreeItemSelected(TreeItem item) {
+                }
+
+            });
+
+        }
+
+        protected boolean isKeyboardNavigationEnabled(TreeItem currentItem) {
+            return false;
+        }
+
+    }
+
+    public TreeItem dir() {
+
+        String nodeName = getTag();
+        for (final Iterator i = getAttributeNames().iterator(); i.hasNext();) {
+            final String name = i.next().toString();
+            final String value = getAttribute(name);
+            nodeName += " " + name + "=" + value;
+        }
+        final TreeItem item = new TreeItem(nodeName);
+
+        try {
+            TreeItem tmp = null;
+            for (final Iterator i = getVariableHash().keySet().iterator(); i
+                    .hasNext();) {
+                final String name = i.next().toString();
+                String value = "";
+                try {
+                    value = getStringVariable(name);
+                } catch (final Exception e) {
+                    try {
+                        final JSONArray a = getArrayVariable(name);
+                        value = a.toString();
+                    } catch (final Exception e2) {
+                        try {
+                            final int intVal = getIntVariable(name);
+                            value = String.valueOf(intVal);
+                        } catch (final Exception e3) {
+                            value = "unknown";
+                        }
+                    }
+                }
+                if (tmp == null) {
+                    tmp = new TreeItem("variables");
+                }
+                tmp.addItem(name + "=" + value);
+            }
+            if (tmp != null) {
+                item.addItem(tmp);
+            }
+        } catch (final Exception e) {
+            // Ingonered, no variables
+        }
+
+        final Iterator i = getChildIterator();
+        while (i.hasNext()) {
+            final Object child = i.next();
+            try {
+                final UIDL c = (UIDL) child;
+                item.addItem(c.dir());
+
+            } catch (final Exception e) {
+                item.addItem(child.toString());
+            }
+        }
+        return item;
+    }
+
+    private JSONObject getVariableHash() {
+        final JSONObject v = (JSONObject) ((JSONObject) json.get(1)).get("v");
+        if (v == null) {
+            throw new IllegalArgumentException("No variables defined in tag.");
+        }
+        return v;
+    }
+
+    public boolean hasVariable(String name) {
+        Object v = null;
+        try {
+            v = getVariableHash().get(name);
+        } catch (final IllegalArgumentException e) {
+        }
+        return v != null;
+    }
+
+    public String getStringVariable(String name) {
+        final JSONString t = (JSONString) getVariableHash().get(name);
+        if (t == null) {
+            throw new IllegalArgumentException("No such variable: " + name);
+        }
+        return t.stringValue();
+    }
+
+    public int getIntVariable(String name) {
+        final JSONNumber t = (JSONNumber) getVariableHash().get(name);
+        if (t == null) {
+            throw new IllegalArgumentException("No such variable: " + name);
+        }
+        return (int) t.getValue();
+    }
+
+    public long getLongVariable(String name) {
+        final JSONNumber t = (JSONNumber) getVariableHash().get(name);
+        if (t == null) {
+            throw new IllegalArgumentException("No such variable: " + name);
+        }
+        return (long) t.getValue();
+    }
+
+    public float getFloatVariable(String name) {
+        final JSONNumber t = (JSONNumber) getVariableHash().get(name);
+        if (t == null) {
+            throw new IllegalArgumentException("No such variable: " + name);
+        }
+        return (float) t.getValue();
+    }
+
+    public double getDoubleVariable(String name) {
+        final JSONNumber t = (JSONNumber) getVariableHash().get(name);
+        if (t == null) {
+            throw new IllegalArgumentException("No such variable: " + name);
+        }
+        return t.getValue();
+    }
+
+    public boolean getBooleanVariable(String name) {
+        final JSONBoolean t = (JSONBoolean) getVariableHash().get(name);
+        if (t == null) {
+            throw new IllegalArgumentException("No such variable: " + name);
+        }
+        return t.booleanValue();
+    }
+
+    private JSONArray getArrayVariable(String name) {
+        final JSONArray t = (JSONArray) getVariableHash().get(name);
+        if (t == null) {
+            throw new IllegalArgumentException("No such variable: " + name);
+        }
+        return t;
+    }
+
+    public String[] getStringArrayVariable(String name) {
+        final JSONArray a = getArrayVariable(name);
+        final String[] s = new String[a.size()];
+        for (int i = 0; i < a.size(); i++) {
+            s[i] = ((JSONString) a.get(i)).stringValue();
+        }
+        return s;
+    }
+
+    public Set getStringArrayVariableAsSet(String name) {
+        final JSONArray a = getArrayVariable(name);
+        final HashSet s = new HashSet();
+        for (int i = 0; i < a.size(); i++) {
+            s.add(((JSONString) a.get(i)).stringValue());
+        }
+        return s;
+    }
+
+    public int[] getIntArrayVariable(String name) {
+        final JSONArray a = getArrayVariable(name);
+        final int[] s = new int[a.size()];
+        for (int i = 0; i < a.size(); i++) {
+            final JSONValue v = a.get(i);
+            s[i] = v.isNumber() != null ? (int) ((JSONNumber) v).getValue()
+                    : Integer.parseInt(v.toString());
+        }
+        return s;
+    }
+
+    public class XML {
+        JSONObject x;
+
+        private XML(JSONObject x) {
+            this.x = x;
+        }
+
+        public String getXMLAsString() {
+            final StringBuffer sb = new StringBuffer();
+            for (final Iterator it = x.keySet().iterator(); it.hasNext();) {
+                final String tag = (String) it.next();
+                sb.append("<");
+                sb.append(tag);
+                sb.append(">");
+                sb.append(x.get(tag).isString().stringValue());
+                sb.append("</");
+                sb.append(tag);
+                sb.append(">");
+            }
+            return sb.toString();
+        }
+    }
+
+    public int getChildCount() {
+        return json.size() - 2;
+    }
+
+    public UIDL getErrors() {
+        final JSONArray a = (JSONArray) ((JSONObject) json.get(1)).get("error");
+        return new UIDL(a);
+    }
+
+}
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/Util.java b/src/com/itmill/toolkit/terminal/terminal/gwt/client/Util.java
new file mode 100644 (file)
index 0000000..3058a68
--- /dev/null
@@ -0,0 +1,149 @@
+/* 
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.itmill.toolkit.terminal.gwt.client;
+
+import java.util.Iterator;
+
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.Element;
+import com.google.gwt.user.client.ui.HasWidgets;
+import com.google.gwt.user.client.ui.Widget;
+
+public class Util {
+
+    /**
+     * Helper method for debugging purposes.
+     * 
+     * Stops execution on firefox browsers on a breakpoint.
+     * 
+     */
+    public static native void browserDebugger()
+    /*-{
+        if(window.console)
+            debugger;
+    }-*/;
+
+    /**
+     * Nulls oncontextmenu function on given element. We need to manually clear
+     * context menu events due bad browsers memory leaks, since GWT don't
+     * support them.
+     * 
+     * @param el
+     */
+    public native static void removeContextMenuEvent(Element el)
+    /*-{
+       el.oncontextmenu = null;
+    }-*/;
+
+    /**
+     * Traverses recursively ancestors until ContainerResizedListener child
+     * widget is found. They will delegate it futher if needed.
+     * 
+     * @param container
+     */
+    public static void runDescendentsLayout(HasWidgets container) {
+        final Iterator childWidgets = container.iterator();
+        while (childWidgets.hasNext()) {
+            final Widget child = (Widget) childWidgets.next();
+            if (child instanceof ContainerResizedListener) {
+                ((ContainerResizedListener) child).iLayout();
+            } else if (child instanceof HasWidgets) {
+                final HasWidgets childContainer = (HasWidgets) child;
+                runDescendentsLayout(childContainer);
+            }
+        }
+    }
+
+    /**
+     * Returns closest parent Widget in hierarchy that implements Container
+     * interface
+     * 
+     * @param component
+     * @return closest parent Container
+     */
+    public static Container getParentLayout(Widget component) {
+        Widget parent = component.getParent();
+        while (parent != null && !(parent instanceof Container)) {
+            parent = parent.getParent();
+        }
+        if (parent != null && ((Container) parent).hasChildComponent(component)) {
+            return (Container) parent;
+        }
+        return null;
+    }
+
+    /**
+     * Detects if current browser is IE.
+     * 
+     * @deprecated use BrowserInfo class instead
+     * 
+     * @return true if IE
+     */
+    public static boolean isIE() {
+        return BrowserInfo.get().isIE();
+    }
+
+    /**
+     * Detects if current browser is IE6.
+     * 
+     * @deprecated use BrowserInfo class instead
+     * 
+     * @return true if IE6
+     */
+    public static boolean isIE6() {
+        return BrowserInfo.get().isIE6();
+    }
+
+    /**
+     * @deprecated use BrowserInfo class instead
+     * @return
+     */
+    public static boolean isIE7() {
+        return BrowserInfo.get().isIE7();
+    }
+
+    /**
+     * @deprecated use BrowserInfo class instead
+     * @return
+     */
+    public static boolean isFF2() {
+        return BrowserInfo.get().isFF2();
+    }
+
+    private static final Element escapeHtmlHelper = DOM.createDiv();
+
+    /**
+     * Converts html entities to text.
+     * 
+     * @param html
+     * @return escaped string presentation of given html
+     */
+    public static String escapeHTML(String html) {
+        DOM.setInnerText(escapeHtmlHelper, html);
+        return DOM.getInnerHTML(escapeHtmlHelper);
+    }
+
+    /**
+     * Adds transparent PNG fix to image element; only use for IE6.
+     * 
+     * @param el
+     *                IMG element
+     * @param blankImageUrl
+     *                URL to transparent one-pixel gif
+     */
+    public native static void addPngFix(Element el, String blankImageUrl)
+    /*-{
+        el.attachEvent("onload", function() {
+            var src = el.src;
+            if (src.indexOf(".png")<1) return;
+            var w = el.width||16; 
+            var h = el.height||16;
+            el.src =blankImageUrl;
+            el.style.height = h+"px";
+            el.style.width = w+"px";
+            el.style.filter = "progid:DXImageTransform.Microsoft.AlphaImageLoader(src='"+src+"', sizingMethod='crop');";  
+        },false);
+    }-*/;
+}
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/WidgetSet.java b/src/com/itmill/toolkit/terminal/terminal/gwt/client/WidgetSet.java
new file mode 100644 (file)
index 0000000..e55da2c
--- /dev/null
@@ -0,0 +1,33 @@
+/* 
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.itmill.toolkit.terminal.gwt.client;
+
+import com.google.gwt.core.client.EntryPoint;
+import com.google.gwt.user.client.ui.Widget;
+
+public interface WidgetSet extends EntryPoint {
+
+    /**
+     * Create an uninitialized component that best matches given UIDL.
+     * 
+     * @param uidl
+     *                UIDL to be painted with returned component.
+     * @return New uninitialized and unregistered component that can paint given
+     *         UIDL.
+     */
+    public Widget createWidget(UIDL uidl);
+
+    /**
+     * Test if the given component implementation conforms to UIDL.
+     * 
+     * @param currentWidget
+     *                Current implementation of the component
+     * @param uidl
+     *                UIDL to test against
+     * @return true iff createWidget would return a new component of the same
+     *         class than currentWidget
+     */
+    public boolean isCorrectImplementation(Widget currentWidget, UIDL uidl);
+}
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/Action.java b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/Action.java
new file mode 100644 (file)
index 0000000..0f05987
--- /dev/null
@@ -0,0 +1,55 @@
+/* 
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.itmill.toolkit.terminal.gwt.client.ui;
+
+import com.google.gwt.user.client.Command;
+
+/**
+ * 
+ */
+public abstract class Action implements Command {
+
+    protected ActionOwner owner;
+
+    protected String iconUrl = null;
+
+    protected String caption = "";
+
+    public Action(ActionOwner owner) {
+        this.owner = owner;
+    }
+
+    /**
+     * Executed when action fired
+     */
+    public abstract void execute();
+
+    public String getHTML() {
+        final StringBuffer sb = new StringBuffer();
+        sb.append("<div>");
+        if (getIconUrl() != null) {
+            sb.append("<img src=\"" + getIconUrl() + "\" alt=\"icon\" />");
+        }
+        sb.append(getCaption());
+        sb.append("</div>");
+        return sb.toString();
+    }
+
+    public String getCaption() {
+        return caption;
+    }
+
+    public void setCaption(String caption) {
+        this.caption = caption;
+    }
+
+    public String getIconUrl() {
+        return iconUrl;
+    }
+
+    public void setIconUrl(String url) {
+        iconUrl = url;
+    }
+}
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/ActionOwner.java b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/ActionOwner.java
new file mode 100644 (file)
index 0000000..647a1cf
--- /dev/null
@@ -0,0 +1,16 @@
+package com.itmill.toolkit.terminal.gwt.client.ui;
+
+import com.itmill.toolkit.terminal.gwt.client.ApplicationConnection;
+
+public interface ActionOwner {
+
+    /**
+     * @return Array of IActions
+     */
+    public Action[] getActions();
+
+    public ApplicationConnection getClient();
+
+    public String getPaintableId();
+
+}
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/AlignmentInfo.java b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/AlignmentInfo.java
new file mode 100644 (file)
index 0000000..68024d8
--- /dev/null
@@ -0,0 +1,76 @@
+/* 
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.itmill.toolkit.terminal.gwt.client.ui;
+
+public class AlignmentInfo {
+
+    public static final int ALIGNMENT_LEFT = 1;
+    public static final int ALIGNMENT_RIGHT = 2;
+    public static final int ALIGNMENT_TOP = 4;
+    public static final int ALIGNMENT_BOTTOM = 8;
+    public static final int ALIGNMENT_HORIZONTAL_CENTER = 16;
+    public static final int ALIGNMENT_VERTICAL_CENTER = 32;
+
+    private int bitMask;
+
+    public AlignmentInfo(int bitMask) {
+        this.bitMask = bitMask;
+    }
+
+    public AlignmentInfo(int horizontal, int vertical) {
+        setAlignment(horizontal, vertical);
+    }
+
+    public void setAlignment(int horiz, int vert) {
+        bitMask = horiz + vert;
+    }
+
+    public int getBitMask() {
+        return bitMask;
+    }
+
+    public boolean isTop() {
+        return (bitMask & ALIGNMENT_TOP) == ALIGNMENT_TOP;
+    }
+
+    public boolean isBottom() {
+        return (bitMask & ALIGNMENT_BOTTOM) == ALIGNMENT_BOTTOM;
+    }
+
+    public boolean isLeft() {
+        return (bitMask & ALIGNMENT_LEFT) == ALIGNMENT_LEFT;
+    }
+
+    public boolean isRight() {
+        return (bitMask & ALIGNMENT_RIGHT) == ALIGNMENT_RIGHT;
+    }
+
+    public boolean isVerticalCenter() {
+        return (bitMask & ALIGNMENT_VERTICAL_CENTER) == ALIGNMENT_VERTICAL_CENTER;
+    }
+
+    public boolean isHorizontalCenter() {
+        return (bitMask & ALIGNMENT_HORIZONTAL_CENTER) == ALIGNMENT_HORIZONTAL_CENTER;
+    }
+
+    public String getVerticalAlignment() {
+        if (isBottom()) {
+            return "bottom";
+        } else if (isVerticalCenter()) {
+            return "middle";
+        }
+        return "top";
+    }
+
+    public String getHorizontalAlignment() {
+        if (isRight()) {
+            return "right";
+        } else if (isHorizontalCenter()) {
+            return "center";
+        }
+        return "left";
+    }
+
+}
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/CalendarEntry.java b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/CalendarEntry.java
new file mode 100644 (file)
index 0000000..551c458
--- /dev/null
@@ -0,0 +1,126 @@
+/* \r
+@ITMillApache2LicenseForJavaFiles@\r
+ */\r
+\r
+package com.itmill.toolkit.terminal.gwt.client.ui;\r
+\r
+import java.util.Date;\r
+\r
+import com.itmill.toolkit.terminal.gwt.client.DateTimeService;\r
+\r
+public class CalendarEntry {\r
+    private final String styleName;\r
+    private Date start;\r
+    private Date end;\r
+    private String title;\r
+    private String description;\r
+    private boolean notime;\r
+\r
+    public CalendarEntry(String styleName, Date start, Date end, String title,\r
+            String description, boolean notime) {\r
+        this.styleName = styleName;\r
+        if (notime) {\r
+            Date d = new Date(start.getTime());\r
+            d.setSeconds(0);\r
+            d.setMinutes(0);\r
+            this.start = d;\r
+            if (end != null) {\r
+                d = new Date(end.getTime());\r
+                d.setSeconds(0);\r
+                d.setMinutes(0);\r
+                this.end = d;\r
+            } else {\r
+                end = start;\r
+            }\r
+        } else {\r
+            this.start = start;\r
+            this.end = end;\r
+        }\r
+        this.title = title;\r
+        this.description = description;\r
+        this.notime = notime;\r
+    }\r
+\r
+    public CalendarEntry(String styleName, Date start, Date end, String title,\r
+            String description) {\r
+        this(styleName, start, end, title, description, false);\r
+    }\r
+\r
+    public String getStyleName() {\r
+        return styleName;\r
+    }\r
+\r
+    public Date getStart() {\r
+        return start;\r
+    }\r
+\r
+    public void setStart(Date start) {\r
+        this.start = start;\r
+    }\r
+\r
+    public Date getEnd() {\r
+        return end;\r
+    }\r
+\r
+    public void setEnd(Date end) {\r
+        this.end = end;\r
+    }\r
+\r
+    public String getTitle() {\r
+        return title;\r
+    }\r
+\r
+    public void setTitle(String title) {\r
+        this.title = title;\r
+    }\r
+\r
+    public String getDescription() {\r
+        return description;\r
+    }\r
+\r
+    public void setDescription(String description) {\r
+        this.description = description;\r
+    }\r
+\r
+    public boolean isNotime() {\r
+        return notime;\r
+    }\r
+\r
+    public void setNotime(boolean notime) {\r
+        this.notime = notime;\r
+    }\r
+\r
+    public String getStringForDate(Date d) {\r
+        // TODO format from DateTimeService\r
+        String s = "";\r
+        if (!notime) {\r
+            if (!DateTimeService.isSameDay(d, start)) {\r
+                s += (start.getYear() + 1900) + "." + (start.getMonth() + 1)\r
+                        + "." + start.getDate() + " ";\r
+            }\r
+            int i = start.getHours();\r
+            s += (i < 10 ? "0" : "") + i;\r
+            s += ":";\r
+            i = start.getMinutes();\r
+            s += (i < 10 ? "0" : "") + i;\r
+            if (!start.equals(end)) {\r
+                s += " - ";\r
+                if (!DateTimeService.isSameDay(start, end)) {\r
+                    s += (end.getYear() + 1900) + "." + (end.getMonth() + 1)\r
+                            + "." + end.getDate() + " ";\r
+                }\r
+                i = end.getHours();\r
+                s += (i < 10 ? "0" : "") + i;\r
+                s += ":";\r
+                i = end.getMinutes();\r
+                s += (i < 10 ? "0" : "") + i;\r
+            }\r
+            s += " ";\r
+        }\r
+        if (title != null) {\r
+            s += title;\r
+        }\r
+        return s;\r
+    }\r
+\r
+}
\ No newline at end of file
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/CalendarPanel.java b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/CalendarPanel.java
new file mode 100644 (file)
index 0000000..f5ecf33
--- /dev/null
@@ -0,0 +1,491 @@
+/* \r
+@ITMillApache2LicenseForJavaFiles@\r
+ */\r
+\r
+package com.itmill.toolkit.terminal.gwt.client.ui;\r
+\r
+import java.util.Date;\r
+import java.util.Iterator;\r
+import java.util.List;\r
+\r
+import com.google.gwt.user.client.DOM;\r
+import com.google.gwt.user.client.Event;\r
+import com.google.gwt.user.client.Timer;\r
+import com.google.gwt.user.client.ui.ClickListener;\r
+import com.google.gwt.user.client.ui.FlexTable;\r
+import com.google.gwt.user.client.ui.MouseListener;\r
+import com.google.gwt.user.client.ui.MouseListenerCollection;\r
+import com.google.gwt.user.client.ui.SourcesMouseEvents;\r
+import com.google.gwt.user.client.ui.SourcesTableEvents;\r
+import com.google.gwt.user.client.ui.TableListener;\r
+import com.google.gwt.user.client.ui.Widget;\r
+import com.itmill.toolkit.terminal.gwt.client.DateTimeService;\r
+import com.itmill.toolkit.terminal.gwt.client.LocaleService;\r
+\r
+public class CalendarPanel extends FlexTable implements MouseListener,\r
+        ClickListener {\r
+\r
+    private final IDateField datefield;\r
+\r
+    private IEventButton prevYear;\r
+\r
+    private IEventButton nextYear;\r
+\r
+    private IEventButton prevMonth;\r
+\r
+    private IEventButton nextMonth;\r
+\r
+    private Time time;\r
+\r
+    private Date minDate = null;\r
+\r
+    private Date maxDate = null;\r
+\r
+    private CalendarEntrySource entrySource;\r
+\r
+    /* Needed to identify resolution changes */\r
+    private int resolution = IDateField.RESOLUTION_YEAR;\r
+\r
+    /* Needed to identify locale changes */\r
+    private String locale = LocaleService.getDefaultLocale();\r
+\r
+    public CalendarPanel(IDateField parent) {\r
+        datefield = parent;\r
+        setStyleName(IDateField.CLASSNAME + "-calendarpanel");\r
+        // buildCalendar(true);\r
+        addTableListener(new DateClickListener(this));\r
+    }\r
+\r
+    public CalendarPanel(IDateField parent, Date min, Date max) {\r
+        datefield = parent;\r
+        setStyleName(IDateField.CLASSNAME + "-calendarpanel");\r
+        // buildCalendar(true);\r
+        addTableListener(new DateClickListener(this));\r
+\r
+    }\r
+\r
+    private void buildCalendar(boolean forceRedraw) {\r
+        final boolean needsMonth = datefield.getCurrentResolution() > IDateField.RESOLUTION_YEAR;\r
+        boolean needsBody = datefield.getCurrentResolution() >= IDateField.RESOLUTION_DAY;\r
+        final boolean needsTime = datefield.getCurrentResolution() >= IDateField.RESOLUTION_HOUR;\r
+        buildCalendarHeader(forceRedraw, needsMonth);\r
+        clearCalendarBody(!needsBody);\r
+        if (needsBody) {\r
+            buildCalendarBody();\r
+        }\r
+        if (needsTime) {\r
+            buildTime(forceRedraw);\r
+        } else if (time != null) {\r
+            remove(time);\r
+            time = null;\r
+        }\r
+    }\r
+\r
+    private void clearCalendarBody(boolean remove) {\r
+        if (!remove) {\r
+            for (int row = 2; row < 8; row++) {\r
+                for (int col = 0; col < 7; col++) {\r
+                    setHTML(row, col, "&nbsp;");\r
+                }\r
+            }\r
+        } else if (getRowCount() > 2) {\r
+            while (getRowCount() > 2) {\r
+                removeRow(2);\r
+            }\r
+        }\r
+    }\r
+\r
+    private void buildCalendarHeader(boolean forceRedraw, boolean needsMonth) {\r
+        if (forceRedraw) {\r
+            if (prevMonth == null) { // Only do once\r
+                prevYear = new IEventButton();\r
+                prevYear.setHTML("&laquo;");\r
+                prevYear.setStyleName("i-button-prevyear");\r
+                nextYear = new IEventButton();\r
+                nextYear.setHTML("&raquo;");\r
+                nextYear.setStyleName("i-button-nextyear");\r
+                prevYear.addMouseListener(this);\r
+                nextYear.addMouseListener(this);\r
+                prevYear.addClickListener(this);\r
+                nextYear.addClickListener(this);\r
+                setWidget(0, 0, prevYear);\r
+                setWidget(0, 4, nextYear);\r
+\r
+                if (needsMonth) {\r
+                    prevMonth = new IEventButton();\r
+                    prevMonth.setHTML("&lsaquo;");\r
+                    prevMonth.setStyleName("i-button-prevmonth");\r
+                    nextMonth = new IEventButton();\r
+                    nextMonth.setHTML("&rsaquo;");\r
+                    nextMonth.setStyleName("i-button-nextmonth");\r
+                    prevMonth.addMouseListener(this);\r
+                    nextMonth.addMouseListener(this);\r
+                    prevMonth.addClickListener(this);\r
+                    nextMonth.addClickListener(this);\r
+                    setWidget(0, 3, nextMonth);\r
+                    setWidget(0, 1, prevMonth);\r
+                }\r
+\r
+                getFlexCellFormatter().setColSpan(0, 2, 3);\r
+                getRowFormatter().addStyleName(0,\r
+                        IDateField.CLASSNAME + "-calendarpanel-header");\r
+            } else if (!needsMonth) {\r
+                // Remove month traverse buttons\r
+                prevMonth.removeClickListener(this);\r
+                prevMonth.removeMouseListener(this);\r
+                nextMonth.removeClickListener(this);\r
+                nextMonth.removeMouseListener(this);\r
+                remove(prevMonth);\r
+                remove(nextMonth);\r
+                prevMonth = null;\r
+                nextMonth = null;\r
+            }\r
+\r
+            // Print weekday names\r
+            final int firstDay = datefield.getDateTimeService()\r
+                    .getFirstDayOfWeek();\r
+            for (int i = 0; i < 7; i++) {\r
+                int day = i + firstDay;\r
+                if (day > 6) {\r
+                    day = 0;\r
+                }\r
+                if (datefield.getCurrentResolution() > IDateField.RESOLUTION_MONTH) {\r
+                    setHTML(1, i, "<strong>"\r
+                            + datefield.getDateTimeService().getShortDay(day)\r
+                            + "</strong>");\r
+                } else {\r
+                    setHTML(1, i, "");\r
+                }\r
+            }\r
+        }\r
+\r
+        final String monthName = needsMonth ? datefield.getDateTimeService()\r
+                .getMonth(datefield.getShowingDate().getMonth()) : "";\r
+        final int year = datefield.getShowingDate().getYear() + 1900;\r
+        setHTML(0, 2, "<span class=\"" + IDateField.CLASSNAME\r
+                + "-calendarpanel-month\">" + monthName + " " + year\r
+                + "</span>");\r
+    }\r
+\r
+    private void buildCalendarBody() {\r
+        // date actually selected?\r
+        Date currentDate = datefield.getCurrentDate();\r
+        Date showing = datefield.getShowingDate();\r
+        boolean selected = (currentDate != null\r
+                && currentDate.getMonth() == showing.getMonth() && currentDate\r
+                .getYear() == showing.getYear());\r
+\r
+        final int startWeekDay = datefield.getDateTimeService()\r
+                .getStartWeekDay(datefield.getShowingDate());\r
+        final int numDays = DateTimeService.getNumberOfDaysInMonth(datefield\r
+                .getShowingDate());\r
+        int dayCount = 0;\r
+        final Date today = new Date();\r
+        final Date curr = new Date(datefield.getShowingDate().getTime());\r
+        for (int row = 2; row < 8; row++) {\r
+            for (int col = 0; col < 7; col++) {\r
+                if (!(row == 2 && col < startWeekDay)) {\r
+                    if (dayCount < numDays) {\r
+                        final int selectedDate = ++dayCount;\r
+                        String title = "";\r
+                        if (entrySource != null) {\r
+                            curr.setDate(dayCount);\r
+                            final List entries = entrySource.getEntries(curr,\r
+                                    IDateField.RESOLUTION_DAY);\r
+                            if (entries != null) {\r
+                                for (final Iterator it = entries.iterator(); it\r
+                                        .hasNext();) {\r
+                                    final CalendarEntry entry = (CalendarEntry) it\r
+                                            .next();\r
+                                    title += (title.length() > 0 ? ", " : "")\r
+                                            + entry.getStringForDate(curr);\r
+                                }\r
+                            }\r
+                        }\r
+                        final String baseclass = IDateField.CLASSNAME\r
+                                + "-calendarpanel-day";\r
+                        String cssClass = baseclass;\r
+                        if (!isEnabledDate(curr)) {\r
+                            cssClass += " " + baseclass + "-disabled";\r
+                        }\r
+                        if (selected\r
+                                && datefield.getShowingDate().getDate() == dayCount) {\r
+                            cssClass += " " + baseclass + "-selected";\r
+                        }\r
+                        if (today.getDate() == dayCount\r
+                                && today.getMonth() == datefield\r
+                                        .getShowingDate().getMonth()\r
+                                && today.getYear() == datefield\r
+                                        .getShowingDate().getYear()) {\r
+                            cssClass += " " + baseclass + "-today";\r
+                        }\r
+                        if (title.length() > 0) {\r
+                            cssClass += " " + baseclass + "-entry";\r
+                        }\r
+                        setHTML(row, col, "<span title=\"" + title\r
+                                + "\" class=\"" + cssClass + "\">"\r
+                                + selectedDate + "</span>");\r
+                    } else {\r
+                        break;\r
+                    }\r
+\r
+                }\r
+            }\r
+        }\r
+    }\r
+\r
+    private void buildTime(boolean forceRedraw) {\r
+        if (time == null) {\r
+            time = new Time(datefield);\r
+            setText(8, 0, ""); // Add new row\r
+            getFlexCellFormatter().setColSpan(8, 0, 7);\r
+            setWidget(8, 0, time);\r
+        }\r
+        time.updateTime(forceRedraw);\r
+    }\r
+\r
+    /**\r
+     * \r
+     * @param forceRedraw\r
+     *                Build all from scratch, in case of e.g. locale changes\r
+     */\r
+    public void updateCalendar() {\r
+        // Locale and resolution changes force a complete redraw\r
+        buildCalendar(locale != datefield.getCurrentLocale()\r
+                || resolution != datefield.getCurrentResolution());\r
+        if (datefield instanceof ITextualDate) {\r
+            ((ITextualDate) datefield).buildDate();\r
+        }\r
+        locale = datefield.getCurrentLocale();\r
+        resolution = datefield.getCurrentResolution();\r
+    }\r
+\r
+    public void onClick(Widget sender) {\r
+        // processClickEvent(sender, true);\r
+    }\r
+\r
+    private boolean isEnabledDate(Date date) {\r
+        if ((minDate != null && date.before(minDate))\r
+                || (maxDate != null && date.after(maxDate))) {\r
+            return false;\r
+        }\r
+        return true;\r
+    }\r
+\r
+    private void processClickEvent(Widget sender, boolean updateVariable) {\r
+        if (!datefield.isEnabled() || datefield.isReadonly()) {\r
+            return;\r
+        }\r
+        Date showingDate = datefield.getShowingDate();\r
+        if (!updateVariable) {\r
+            if (sender == prevYear) {\r
+                showingDate.setYear(showingDate.getYear() - 1);\r
+                updateCalendar();\r
+            } else if (sender == nextYear) {\r
+                showingDate.setYear(showingDate.getYear() + 1);\r
+                updateCalendar();\r
+            } else if (sender == prevMonth) {\r
+                showingDate.setMonth(showingDate.getMonth() - 1);\r
+                updateCalendar();\r
+            } else if (sender == nextMonth) {\r
+                showingDate.setMonth(showingDate.getMonth() + 1);\r
+                updateCalendar();\r
+            }\r
+        } else {\r
+            if (datefield.getCurrentResolution() == IDateField.RESOLUTION_YEAR\r
+                    || datefield.getCurrentResolution() == IDateField.RESOLUTION_MONTH) {\r
+                // Due to current UI, update variable if res=year/month\r
+                datefield.setCurrentDate(new Date(showingDate.getTime()));\r
+                if (datefield.getCurrentResolution() == IDateField.RESOLUTION_MONTH) {\r
+                    datefield.getClient().updateVariable(datefield.getId(),\r
+                            "month", datefield.getCurrentDate().getMonth() + 1,\r
+                            false);\r
+                }\r
+                datefield.getClient().updateVariable(datefield.getId(), "year",\r
+                        datefield.getCurrentDate().getYear() + 1900,\r
+                        datefield.isImmediate());\r
+            }\r
+        }\r
+    }\r
+\r
+    private Timer timer;\r
+\r
+    public void onMouseDown(final Widget sender, int x, int y) {\r
+        if (sender instanceof IEventButton) {\r
+            processClickEvent(sender, false);\r
+            timer = new Timer() {\r
+                public void run() {\r
+                    processClickEvent(sender, false);\r
+                }\r
+            };\r
+            timer.scheduleRepeating(100);\r
+        }\r
+    }\r
+\r
+    public void onMouseEnter(Widget sender) {\r
+    }\r
+\r
+    public void onMouseLeave(Widget sender) {\r
+        if (timer != null) {\r
+            timer.cancel();\r
+        }\r
+    }\r
+\r
+    public void onMouseMove(Widget sender, int x, int y) {\r
+    }\r
+\r
+    public void onMouseUp(Widget sender, int x, int y) {\r
+        if (timer != null) {\r
+            timer.cancel();\r
+        }\r
+        processClickEvent(sender, true);\r
+    }\r
+\r
+    private class IEventButton extends IButton implements SourcesMouseEvents {\r
+\r
+        private MouseListenerCollection mouseListeners;\r
+\r
+        public IEventButton() {\r
+            super();\r
+            sinkEvents(Event.FOCUSEVENTS | Event.KEYEVENTS | Event.ONCLICK\r
+                    | Event.MOUSEEVENTS);\r
+        }\r
+\r
+        public void addMouseListener(MouseListener listener) {\r
+            if (mouseListeners == null) {\r
+                mouseListeners = new MouseListenerCollection();\r
+            }\r
+            mouseListeners.add(listener);\r
+        }\r
+\r
+        public void removeMouseListener(MouseListener listener) {\r
+            if (mouseListeners != null) {\r
+                mouseListeners.remove(listener);\r
+            }\r
+        }\r
+\r
+        public void onBrowserEvent(Event event) {\r
+            super.onBrowserEvent(event);\r
+            switch (DOM.eventGetType(event)) {\r
+            case Event.ONMOUSEDOWN:\r
+            case Event.ONMOUSEUP:\r
+            case Event.ONMOUSEMOVE:\r
+            case Event.ONMOUSEOVER:\r
+            case Event.ONMOUSEOUT:\r
+                if (mouseListeners != null) {\r
+                    mouseListeners.fireMouseEvent(this, event);\r
+                }\r
+                break;\r
+            }\r
+        }\r
+    }\r
+\r
+    private class DateClickListener implements TableListener {\r
+\r
+        private final CalendarPanel cal;\r
+\r
+        public DateClickListener(CalendarPanel panel) {\r
+            cal = panel;\r
+        }\r
+\r
+        public void onCellClicked(SourcesTableEvents sender, int row, int col) {\r
+            if (sender != cal || row < 2 || row > 7\r
+                    || !cal.datefield.isEnabled() || cal.datefield.isReadonly()) {\r
+                return;\r
+            }\r
+\r
+            final String text = cal.getText(row, col);\r
+            if (text.equals(" ")) {\r
+                return;\r
+            }\r
+\r
+            try {\r
+                final Integer day = new Integer(text);\r
+                final Date newDate = cal.datefield.getShowingDate();\r
+                newDate.setDate(day.intValue());\r
+                if (!isEnabledDate(newDate)) {\r
+                    return;\r
+                }\r
+                if (cal.datefield.getCurrentDate() == null) {\r
+                    cal.datefield.setCurrentDate(new Date(newDate.getTime()));\r
+\r
+                    // Init variables with current time\r
+                    datefield.getClient().updateVariable(cal.datefield.getId(),\r
+                            "hour", newDate.getHours(), false);\r
+                    datefield.getClient().updateVariable(cal.datefield.getId(),\r
+                            "min", newDate.getMinutes(), false);\r
+                    datefield.getClient().updateVariable(cal.datefield.getId(),\r
+                            "sec", newDate.getSeconds(), false);\r
+                    datefield.getClient().updateVariable(cal.datefield.getId(),\r
+                            "msec", datefield.getMilliseconds(), false);\r
+                }\r
+\r
+                cal.datefield.getCurrentDate().setTime(newDate.getTime());\r
+                cal.datefield.getClient().updateVariable(cal.datefield.getId(),\r
+                        "day", cal.datefield.getCurrentDate().getDate(), false);\r
+                cal.datefield.getClient().updateVariable(cal.datefield.getId(),\r
+                        "month", cal.datefield.getCurrentDate().getMonth() + 1,\r
+                        false);\r
+                cal.datefield.getClient().updateVariable(cal.datefield.getId(),\r
+                        "year",\r
+                        cal.datefield.getCurrentDate().getYear() + 1900,\r
+                        cal.datefield.isImmediate());\r
+\r
+                if (datefield instanceof ITextualDate\r
+                        && resolution < IDateField.RESOLUTION_HOUR) {\r
+                    ((ToolkitOverlay) getParent()).hide();\r
+                } else {\r
+                    updateCalendar();\r
+                }\r
+\r
+            } catch (final NumberFormatException e) {\r
+                // Not a number, ignore and stop here\r
+                return;\r
+            }\r
+        }\r
+\r
+    }\r
+\r
+    public void setLimits(Date min, Date max) {\r
+        if (min != null) {\r
+            final Date d = new Date(min.getTime());\r
+            d.setHours(0);\r
+            d.setMinutes(0);\r
+            d.setSeconds(1);\r
+            minDate = d;\r
+        } else {\r
+            minDate = null;\r
+        }\r
+        if (max != null) {\r
+            final Date d = new Date(max.getTime());\r
+            d.setHours(24);\r
+            d.setMinutes(59);\r
+            d.setSeconds(59);\r
+            maxDate = d;\r
+        } else {\r
+            maxDate = null;\r
+        }\r
+    }\r
+\r
+    public void setCalendarEntrySource(CalendarEntrySource entrySource) {\r
+        this.entrySource = entrySource;\r
+    }\r
+\r
+    public CalendarEntrySource getCalendarEntrySource() {\r
+        return entrySource;\r
+    }\r
+\r
+    public interface CalendarEntrySource {\r
+        public List getEntries(Date date, int resolution);\r
+    }\r
+\r
+    /**\r
+     * Sets focus to Calendar panel.\r
+     * \r
+     * @param focus\r
+     */\r
+    public void setFocus(boolean focus) {\r
+        nextYear.setFocus(focus);\r
+    }\r
+\r
+}\r
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/ContextMenu.java b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/ContextMenu.java
new file mode 100644 (file)
index 0000000..b3c14ba
--- /dev/null
@@ -0,0 +1,103 @@
+/* 
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.itmill.toolkit.terminal.gwt.client.ui;
+
+import com.google.gwt.user.client.Window;
+import com.google.gwt.user.client.ui.MenuBar;
+import com.google.gwt.user.client.ui.MenuItem;
+import com.google.gwt.user.client.ui.PopupPanel;
+
+public class ContextMenu extends ToolkitOverlay {
+
+    private ActionOwner actionOwner;
+
+    private final CMenuBar menu = new CMenuBar();
+
+    private int left;
+
+    private int top;
+
+    /**
+     * This method should be used only by Client object as only one per client
+     * should exists. Request an instance via client.getContextMenu();
+     * 
+     * @param cli
+     *                to be set as an owner of menu
+     */
+    public ContextMenu() {
+        super(true);
+        setWidget(menu);
+        setStyleName("i-contextmenu");
+    }
+
+    /**
+     * Sets the element from which to build menu
+     * 
+     * @param ao
+     */
+    public void setActionOwner(ActionOwner ao) {
+        actionOwner = ao;
+    }
+
+    /**
+     * Shows context menu at given location.
+     * 
+     * @param left
+     * @param top
+     */
+    public void showAt(int left, int top) {
+        this.left = left;
+        this.top = top;
+        menu.clearItems();
+        final Action[] actions = actionOwner.getActions();
+        for (int i = 0; i < actions.length; i++) {
+            final Action a = actions[i];
+            menu.addItem(new MenuItem(a.getHTML(), true, a));
+        }
+
+        setPopupPositionAndShow(new PositionCallback() {
+            public void setPosition(int offsetWidth, int offsetHeight) {
+                // mac FF gets bad width due GWT popups overflow hacks,
+                // re-determine width
+                offsetWidth = menu.getOffsetWidth();
+                int left = ContextMenu.this.left;
+                int top = ContextMenu.this.top;
+                if (offsetWidth + left > Window.getClientWidth()) {
+                    left = left - offsetWidth;
+                    if (left < 0) {
+                        left = 0;
+                    }
+                }
+                if (offsetHeight + top > Window.getClientHeight()) {
+                    top = top - offsetHeight;
+                    if (top < 0) {
+                        top = 0;
+                    }
+                }
+                setPopupPosition(left, top);
+            }
+        });
+    }
+
+    public void showAt(ActionOwner ao, int left, int top) {
+        setActionOwner(ao);
+        showAt(left, top);
+    }
+
+    /**
+     * Extend standard Gwt MenuBar to set proper settings and to override
+     * onPopupClosed method so that PopupPanel gets closed.
+     */
+    class CMenuBar extends MenuBar {
+        public CMenuBar() {
+            super(true);
+        }
+
+        public void onPopupClosed(PopupPanel sender, boolean autoClosed) {
+            super.onPopupClosed(sender, autoClosed);
+            ContextMenu.this.hide();
+        }
+    }
+}
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/Field.java b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/Field.java
new file mode 100644 (file)
index 0000000..d0e1d0b
--- /dev/null
@@ -0,0 +1,13 @@
+/**\r
+ * \r
+ */\r
+package com.itmill.toolkit.terminal.gwt.client.ui;\r
+\r
+/**\r
+ * This interface indicates that the component is a Field (serverside), and\r
+ * wants (for instance) to automatically get the i-modified classname.\r
+ * \r
+ */\r
+public interface Field {\r
+\r
+}\r
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/IAccordion.java b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/IAccordion.java
new file mode 100644 (file)
index 0000000..bd591bd
--- /dev/null
@@ -0,0 +1,267 @@
+package com.itmill.toolkit.terminal.gwt.client.ui;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Set;
+
+import com.google.gwt.user.client.Command;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.DeferredCommand;
+import com.google.gwt.user.client.Element;
+import com.google.gwt.user.client.ui.ClickListener;
+import com.google.gwt.user.client.ui.ComplexPanel;
+import com.google.gwt.user.client.ui.Widget;
+import com.itmill.toolkit.terminal.gwt.client.ApplicationConnection;
+import com.itmill.toolkit.terminal.gwt.client.Caption;
+import com.itmill.toolkit.terminal.gwt.client.ContainerResizedListener;
+import com.itmill.toolkit.terminal.gwt.client.Paintable;
+import com.itmill.toolkit.terminal.gwt.client.UIDL;
+import com.itmill.toolkit.terminal.gwt.client.Util;
+
+public class IAccordion extends ITabsheetBase implements
+        ContainerResizedListener {
+
+    public static final String CLASSNAME = "i-accordion";
+
+    private ArrayList stack = new ArrayList();
+
+    private Set paintables = new HashSet();
+
+    private String height;
+
+    public IAccordion() {
+        super(CLASSNAME);
+        // IE6 needs this to calculate offsetHeight correctly
+        if (Util.isIE6()) {
+            DOM.setStyleAttribute(getElement(), "zoom", "1");
+        }
+    }
+
+    public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+        super.updateFromUIDL(uidl, client);
+        iLayout();
+    }
+
+    private StackItem getSelectedStack() {
+        if (stack.size() == 0) {
+            return null;
+        }
+        return (StackItem) stack.get(activeTabIndex);
+    }
+
+    protected void renderTab(UIDL tabUidl, int index, boolean selected) {
+        // TODO check indexes, now new tabs get placed last (changing tab order
+        // is not supported from server-side)
+
+        StackItem item = new StackItem(tabUidl);
+
+        if (stack.size() == 0) {
+            item.addStyleDependentName("first");
+        }
+
+        stack.add(item);
+        add(item, getElement());
+
+        if (selected) {
+            item.open();
+            item.setContent(tabUidl.getChildUIDL(0));
+        } else if (tabUidl.getChildCount() > 0) {
+            // updating a drawn child on hidden tab
+            Paintable paintable = client.getPaintable(tabUidl.getChildUIDL(0));
+            paintable.updateFromUIDL(tabUidl.getChildUIDL(0), client);
+        }
+    }
+
+    protected void selectTab(final int index, final UIDL contentUidl) {
+        StackItem item = (StackItem) stack.get(index);
+        if (index != activeTabIndex) {
+            activeTabIndex = index;
+            item.open();
+            iLayout();
+        }
+        item.setContent(contentUidl);
+    }
+
+    public void onSelectTab(StackItem item) {
+        final int index = stack.indexOf(item);
+        if (index != activeTabIndex && !disabled && !readonly
+                && !disabledTabKeys.contains(tabKeys.get(index))) {
+            if (getSelectedStack() != null) {
+                getSelectedStack().close();
+            }
+            addStyleDependentName("loading");
+            // run updating variables in deferred command to bypass some FF
+            // optimization issues
+            DeferredCommand.addCommand(new Command() {
+                public void execute() {
+                    client.updateVariable(id, "selected", ""
+                            + tabKeys.get(index), true);
+                }
+            });
+        }
+    }
+
+    public void setWidth(String width) {
+        if (width.equals("100%")) {
+            super.setWidth("");
+        } else {
+            super.setWidth(width);
+        }
+    }
+
+    public void setHeight(String height) {
+        this.height = height;
+    }
+
+    public void iLayout() {
+        StackItem item = getSelectedStack();
+        if (item == null) {
+            return;
+        }
+
+        if (height != null && height != "") {
+            // Detach visible widget from document flow for a while to calculate
+            // used height correctly
+            Widget w = item.getPaintable();
+            String originalPositioning = "";
+            if (w != null) {
+                originalPositioning = DOM.getStyleAttribute(w.getElement(),
+                        "position");
+                DOM.setStyleAttribute(w.getElement(), "visibility", "hidden");
+                DOM.setStyleAttribute(w.getElement(), "position", "absolute");
+            }
+            DOM.setStyleAttribute(item.getContainerElement(), "height", "0");
+
+            // Calculate target height
+            super.setHeight(height);
+            int targetHeight = DOM.getElementPropertyInt(DOM
+                    .getParent(getElement()), "offsetHeight");
+            super.setHeight("");
+
+            // Calculate used height
+            int usedHeight = getOffsetHeight();
+
+            int h = targetHeight - usedHeight;
+            if (h < 0) {
+                h = 0;
+            }
+            DOM.setStyleAttribute(item.getContainerElement(), "height", h
+                    + "px");
+
+            // Put widget back into normal flow
+            if (w != null) {
+                DOM.setStyleAttribute(w.getElement(), "position",
+                        originalPositioning);
+                DOM.setStyleAttribute(w.getElement(), "visibility", "");
+            }
+        } else {
+            DOM.setStyleAttribute(item.getContainerElement(), "height", "");
+        }
+
+        Util.runDescendentsLayout(this);
+    }
+
+    /**
+     * TODO Caption widget not properly attached
+     */
+    protected class StackItem extends ComplexPanel implements ClickListener {
+
+        private Caption caption;
+        private boolean open = false;
+        private Element content;
+        private Element captionNode;
+        private Paintable paintable;
+
+        public StackItem(UIDL tabUidl) {
+            setElement(DOM.createDiv());
+            caption = new Caption(null, client);
+            caption.addClickListener(this);
+            content = DOM.createDiv();
+            captionNode = DOM.createDiv();
+            super.add(caption, captionNode);
+            DOM.appendChild(captionNode, caption.getElement());
+            DOM.appendChild(getElement(), captionNode);
+            DOM.appendChild(getElement(), content);
+            setStylePrimaryName(CLASSNAME + "-item");
+            DOM.setElementProperty(content, "className", CLASSNAME
+                    + "-item-content");
+            DOM.setElementProperty(captionNode, "className", CLASSNAME
+                    + "-item-caption");
+            DOM.setStyleAttribute(content, "overflow", "auto");
+            DOM.setStyleAttribute(content, "display", "none");
+            // Force 'hasLayout' in IE6 (prevents layout problems)
+            if (Util.isIE6()) {
+                DOM.setStyleAttribute(content, "zoom", "1");
+            }
+
+            caption.updateCaption(tabUidl);
+        }
+
+        public Element getContainerElement() {
+            return content;
+        }
+
+        public Widget getPaintable() {
+            if (getWidgetCount() > 1) {
+                return getWidget(1);
+            } else {
+                return null;
+            }
+        }
+
+        public void open() {
+            open = true;
+            DOM.setStyleAttribute(content, "display", "");
+            addStyleDependentName("open");
+            if (getPaintable() != null) {
+                add(getPaintable(), content);
+            }
+        }
+
+        public void close() {
+            open = false;
+            if (getPaintable() != null) {
+                remove(getPaintable());
+            }
+            DOM.setStyleAttribute(content, "display", "none");
+            removeStyleDependentName("open");
+        }
+
+        public boolean isOpen() {
+            return open;
+        }
+
+        public void setContent(UIDL contentUidl) {
+            final Paintable newPntbl = client.getPaintable(contentUidl);
+            // due hack #1 in ITabsheetBase
+            ((Widget) newPntbl).setVisible(true);
+            if (getPaintable() == null) {
+                add((Widget) newPntbl, content);
+                paintables.add(newPntbl);
+            } else if (getPaintable() != newPntbl) {
+                client.unregisterPaintable((Paintable) getWidget(1));
+                paintables.remove(getWidget(1));
+                remove(1);
+                add((Widget) newPntbl, content);
+                paintables.add(newPntbl);
+            }
+            paintable = newPntbl;
+            paintable.updateFromUIDL(contentUidl, client);
+        }
+
+        public void onClick(Widget sender) {
+            onSelectTab(this);
+        }
+    }
+
+    protected void clearPaintables() {
+        stack.clear();
+        clear();
+    }
+
+    protected Iterator getPaintableIterator() {
+        return paintables.iterator();
+    }
+
+}
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/IButton.java b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/IButton.java
new file mode 100644 (file)
index 0000000..6f1b301
--- /dev/null
@@ -0,0 +1,111 @@
+/* 
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.itmill.toolkit.terminal.gwt.client.ui;
+
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.Element;
+import com.google.gwt.user.client.Event;
+import com.google.gwt.user.client.ui.Button;
+import com.google.gwt.user.client.ui.ClickListener;
+import com.google.gwt.user.client.ui.Widget;
+import com.itmill.toolkit.terminal.gwt.client.ApplicationConnection;
+import com.itmill.toolkit.terminal.gwt.client.Paintable;
+import com.itmill.toolkit.terminal.gwt.client.Tooltip;
+import com.itmill.toolkit.terminal.gwt.client.UIDL;
+
+public class IButton extends Button implements Paintable {
+
+    public static final String CLASSNAME = "i-button";
+
+    String id;
+
+    ApplicationConnection client;
+
+    private Element errorIndicatorElement;
+
+    private final Element captionElement = DOM.createSpan();
+
+    private Icon icon;
+
+    public IButton() {
+        setStyleName(CLASSNAME);
+
+        DOM.appendChild(getElement(), captionElement);
+
+        addClickListener(new ClickListener() {
+            public void onClick(Widget sender) {
+                if (id == null || client == null) {
+                    return;
+                }
+                /*
+                 * TODO isolata workaround. Safari don't always seem to fire
+                 * onblur previously focused component before button is clicked.
+                 */
+                IButton.this.setFocus(true);
+                client.updateVariable(id, "state", true, true);
+            }
+        });
+        sinkEvents(Tooltip.TOOLTIP_EVENTS);
+    }
+
+    public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+
+        // Ensure correct implementation,
+        // but don't let container manage caption etc.
+        if (client.updateComponent(this, uidl, false)) {
+            return;
+        }
+
+        // Save details
+        this.client = client;
+        id = uidl.getId();
+
+        // Set text
+        setText(uidl.getStringAttribute("caption"));
+
+        // handle error
+        if (uidl.hasAttribute("error")) {
+            if (errorIndicatorElement == null) {
+                errorIndicatorElement = DOM.createDiv();
+                DOM.setElementProperty(errorIndicatorElement, "className",
+                        "i-errorindicator");
+            }
+            DOM.insertChild(getElement(), errorIndicatorElement, 0);
+
+        } else if (errorIndicatorElement != null) {
+            DOM.removeChild(getElement(), errorIndicatorElement);
+            errorIndicatorElement = null;
+        }
+
+        if (uidl.hasAttribute("readonly")) {
+            setEnabled(false);
+        }
+
+        if (uidl.hasAttribute("icon")) {
+            if (icon == null) {
+                icon = new Icon(client);
+                DOM.insertChild(getElement(), icon.getElement(), 0);
+            }
+            icon.setUri(uidl.getStringAttribute("icon"));
+        } else {
+            if (icon != null) {
+                DOM.removeChild(getElement(), icon.getElement());
+                icon = null;
+            }
+        }
+    }
+
+    public void setText(String text) {
+        DOM.setInnerText(captionElement, text);
+    }
+
+    public void onBrowserEvent(Event event) {
+        super.onBrowserEvent(event);
+        if (client != null) {
+            client.handleTooltipEvent(event, this);
+        }
+    }
+
+}
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/ICheckBox.java b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/ICheckBox.java
new file mode 100644 (file)
index 0000000..191db60
--- /dev/null
@@ -0,0 +1,118 @@
+/* 
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.itmill.toolkit.terminal.gwt.client.ui;
+
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.Element;
+import com.google.gwt.user.client.Event;
+import com.google.gwt.user.client.ui.ClickListener;
+import com.google.gwt.user.client.ui.Widget;
+import com.itmill.toolkit.terminal.gwt.client.ApplicationConnection;
+import com.itmill.toolkit.terminal.gwt.client.Paintable;
+import com.itmill.toolkit.terminal.gwt.client.Tooltip;
+import com.itmill.toolkit.terminal.gwt.client.UIDL;
+
+public class ICheckBox extends com.google.gwt.user.client.ui.CheckBox implements
+        Paintable, Field {
+
+    public static final String CLASSNAME = "i-checkbox";
+
+    String id;
+
+    boolean immediate;
+
+    ApplicationConnection client;
+
+    private Element errorIndicatorElement;
+
+    private Icon icon;
+
+    private boolean isBlockMode = false;
+
+    public ICheckBox() {
+        setStyleName(CLASSNAME);
+        addClickListener(new ClickListener() {
+
+            public void onClick(Widget sender) {
+                if (id == null || client == null) {
+                    return;
+                }
+                client.updateVariable(id, "state", isChecked(), immediate);
+            }
+
+        });
+        sinkEvents(Tooltip.TOOLTIP_EVENTS);
+    }
+
+    public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+        // Save details
+        this.client = client;
+        id = uidl.getId();
+
+        // Ensure correct implementation
+        if (client.updateComponent(this, uidl, false)) {
+            return;
+        }
+
+        if (uidl.hasAttribute("error")) {
+            if (errorIndicatorElement == null) {
+                errorIndicatorElement = DOM.createDiv();
+                DOM.setElementProperty(errorIndicatorElement, "className",
+                        "i-errorindicator");
+                DOM.appendChild(getElement(), errorIndicatorElement);
+            }
+        } else if (errorIndicatorElement != null) {
+            DOM.setStyleAttribute(errorIndicatorElement, "display", "none");
+        }
+
+        if (uidl.hasAttribute("readonly")) {
+            setEnabled(false);
+        }
+
+        if (uidl.hasAttribute("icon")) {
+            if (icon == null) {
+                icon = new Icon(client);
+                DOM.insertChild(getElement(), icon.getElement(), 1);
+            }
+            icon.setUri(uidl.getStringAttribute("icon"));
+        } else if (icon != null) {
+            // detach icon
+            DOM.removeChild(getElement(), icon.getElement());
+            icon = null;
+        }
+
+        // Set text
+        setText(uidl.getStringAttribute("caption"));
+        setChecked(uidl.getBooleanVariable("state"));
+        immediate = uidl.getBooleanAttribute("immediate");
+    }
+
+    public void onBrowserEvent(Event event) {
+        super.onBrowserEvent(event);
+        if (client != null) {
+            client.handleTooltipEvent(event, this);
+        }
+    }
+
+    public void setWidth(String width) {
+        setBlockMode();
+        super.setWidth(width);
+    }
+
+    public void setHeight(String height) {
+        setBlockMode();
+        super.setHeight(height);
+    }
+
+    /**
+     * makes container element (span) to be block element to enable sizing.
+     */
+    private void setBlockMode() {
+        if (!isBlockMode) {
+            DOM.setStyleAttribute(getElement(), "display", "block");
+            isBlockMode = true;
+        }
+    }
+}
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/ICustomComponent.java b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/ICustomComponent.java
new file mode 100644 (file)
index 0000000..fcd4d47
--- /dev/null
@@ -0,0 +1,64 @@
+/* 
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.itmill.toolkit.terminal.gwt.client.ui;
+
+import com.google.gwt.user.client.ui.SimplePanel;
+import com.google.gwt.user.client.ui.Widget;
+import com.itmill.toolkit.terminal.gwt.client.ApplicationConnection;
+import com.itmill.toolkit.terminal.gwt.client.Container;
+import com.itmill.toolkit.terminal.gwt.client.Paintable;
+import com.itmill.toolkit.terminal.gwt.client.UIDL;
+
+public class ICustomComponent extends SimplePanel implements Container {
+
+    private static final String CLASSNAME = "i-customcomponent";
+
+    public ICustomComponent() {
+        super();
+        setStyleName(CLASSNAME);
+    }
+
+    public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+        if (client.updateComponent(this, uidl, false)) {
+            return;
+        }
+
+        final UIDL child = uidl.getChildUIDL(0);
+        if (child != null) {
+            final Paintable p = client.getPaintable(child);
+            if (p != getWidget()) {
+                if (getWidget() != null) {
+                    client.unregisterPaintable((Paintable) getWidget());
+                    clear();
+                }
+                setWidget((Widget) p);
+            }
+            p.updateFromUIDL(child, client);
+        }
+
+    }
+
+    public boolean hasChildComponent(Widget component) {
+        if (getWidget() == component) {
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    public void replaceChildComponent(Widget oldComponent, Widget newComponent) {
+        if (hasChildComponent(oldComponent)) {
+            clear();
+            setWidget(newComponent);
+        } else {
+            throw new IllegalStateException();
+        }
+    }
+
+    public void updateCaption(Paintable component, UIDL uidl) {
+        // TODO custom component could handle its composition roots caption
+    }
+
+}
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/ICustomLayout.java b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/ICustomLayout.java
new file mode 100644 (file)
index 0000000..366257f
--- /dev/null
@@ -0,0 +1,463 @@
+/* 
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.itmill.toolkit.terminal.gwt.client.ui;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Set;
+
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.Element;
+import com.google.gwt.user.client.ui.ComplexPanel;
+import com.google.gwt.user.client.ui.Widget;
+import com.itmill.toolkit.terminal.gwt.client.ApplicationConnection;
+import com.itmill.toolkit.terminal.gwt.client.Caption;
+import com.itmill.toolkit.terminal.gwt.client.CaptionWrapper;
+import com.itmill.toolkit.terminal.gwt.client.Container;
+import com.itmill.toolkit.terminal.gwt.client.ContainerResizedListener;
+import com.itmill.toolkit.terminal.gwt.client.Paintable;
+import com.itmill.toolkit.terminal.gwt.client.UIDL;
+import com.itmill.toolkit.terminal.gwt.client.Util;
+
+/**
+ * Custom Layout implements complex layout defined with HTML template.
+ * 
+ * @author IT Mill
+ * 
+ */
+public class ICustomLayout extends ComplexPanel implements Paintable,
+        Container, ContainerResizedListener {
+
+    public static final String CLASSNAME = "i-customlayout";
+
+    /** Location-name to containing element in DOM map */
+    private final HashMap locationToElement = new HashMap();
+
+    /** Location-name to contained widget map */
+    private final HashMap locationToWidget = new HashMap();
+
+    /** Widget to captionwrapper map */
+    private final HashMap widgetToCaptionWrapper = new HashMap();
+
+    /** Currently rendered style */
+    String currentTemplate;
+
+    /** Unexecuted scripts loaded from the template */
+    private String scripts = "";
+
+    /** Paintable ID of this paintable */
+    private String pid;
+
+    private ApplicationConnection client;
+
+    public ICustomLayout() {
+        setElement(DOM.createDiv());
+        // Clear any unwanted styling
+        DOM.setStyleAttribute(getElement(), "border", "none");
+        DOM.setStyleAttribute(getElement(), "margin", "0");
+        DOM.setStyleAttribute(getElement(), "padding", "0");
+        setStyleName(CLASSNAME);
+    }
+
+    /**
+     * Sets widget to given location.
+     * 
+     * If location already contains a widget it will be removed.
+     * 
+     * @param widget
+     *                Widget to be set into location.
+     * @param location
+     *                location name where widget will be added
+     * 
+     * @throws IllegalArgumentException
+     *                 if no such location is found in the layout.
+     */
+    public void setWidget(Widget widget, String location) {
+
+        if (widget == null) {
+            return;
+        }
+
+        // If no given location is found in the layout, and exception is throws
+        Element elem = (Element) locationToElement.get(location);
+        if (elem == null && hasTemplate()) {
+            throw new IllegalArgumentException("No location " + location
+                    + " found");
+        }
+
+        // Get previous widget
+        final Widget previous = (Widget) locationToWidget.get(location);
+        // NOP if given widget already exists in this location
+        if (previous == widget) {
+            return;
+        }
+        remove(previous);
+
+        // if template is missing add element in order
+        if (!hasTemplate()) {
+            elem = getElement();
+        }
+
+        // Add widget to location
+        super.add(widget, elem);
+        locationToWidget.put(location, widget);
+    }
+
+    /** Update the layout from UIDL */
+    public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+        this.client = client;
+        // Client manages general cases
+        if (client.updateComponent(this, uidl, false)) {
+            return;
+        }
+
+        // Update PID
+        pid = uidl.getId();
+        if (!hasTemplate()) {
+            // Update HTML template only once
+            initializeHTML(uidl, client);
+        }
+
+        // Set size
+        if (uidl.hasAttribute("width")) {
+            setWidth(uidl.getStringAttribute("width"));
+        } else {
+            setWidth("100%");
+        }
+        if (uidl.hasAttribute("height")) {
+            setHeight(uidl.getStringAttribute("height"));
+        } else {
+            setHeight("100%");
+        }
+
+        // Evaluate scripts
+        eval(scripts);
+        scripts = null;
+
+        iLayout();
+
+        Set oldWidgets = new HashSet();
+        oldWidgets.addAll(locationToWidget.values());
+
+        // For all contained widgets
+        for (final Iterator i = uidl.getChildIterator(); i.hasNext();) {
+            final UIDL uidlForChild = (UIDL) i.next();
+            if (uidlForChild.getTag().equals("location")) {
+                final String location = uidlForChild.getStringAttribute("name");
+                final Paintable child = client.getPaintable(uidlForChild
+                        .getChildUIDL(0));
+                try {
+                    setWidget((Widget) child, location);
+                    child.updateFromUIDL(uidlForChild.getChildUIDL(0), client);
+                } catch (final IllegalArgumentException e) {
+                    // If no location is found, this component is not visible
+                }
+                oldWidgets.remove(child);
+            }
+        }
+        for (Iterator iterator = oldWidgets.iterator(); iterator.hasNext();) {
+            Widget oldWidget = (Widget) iterator.next();
+            if (oldWidget.isAttached()) {
+                // slot of this widget is emptied, remove it
+                remove(oldWidget);
+            }
+        }
+
+        iLayout();
+    }
+
+    /** Initialize HTML-layout. */
+    private void initializeHTML(UIDL uidl, ApplicationConnection client) {
+
+        final String newTemplate = uidl.getStringAttribute("template");
+
+        // Get the HTML-template from client
+        String template = client
+                .getResource("layouts/" + newTemplate + ".html");
+        if (template == null) {
+            template = "<em>Layout file layouts/"
+                    + newTemplate
+                    + ".html is missing. Components will be drawn for debug purposes.</em>";
+        } else {
+            currentTemplate = newTemplate;
+        }
+
+        // Connect body of the template to DOM
+        template = extractBodyAndScriptsFromTemplate(template);
+        DOM.setInnerHTML(getElement(), template);
+
+        // Remap locations to elements
+        locationToElement.clear();
+        scanForLocations(getElement());
+
+        String themeUri = client.getThemeUri();
+        prefixImgSrcs(getElement(), themeUri + "/layouts/");
+
+        publishResizedFunction(DOM.getFirstChild(getElement()));
+
+    }
+
+    private native boolean uriEndsWithSlash()
+    /*-{
+        var path =  $wnd.location.pathname;
+        if(path.charAt(path.length - 1) == "/")
+            return true;
+        return false;
+    }-*/;
+
+    private boolean hasTemplate() {
+        if (currentTemplate == null) {
+            return false;
+        } else {
+            return true;
+        }
+    }
+
+    /** Collect locations from template */
+    private void scanForLocations(Element elem) {
+
+        final String location = getLocation(elem);
+        if (location != null) {
+            locationToElement.put(location, elem);
+            DOM.setInnerHTML(elem, "");
+        } else {
+            final int len = DOM.getChildCount(elem);
+            for (int i = 0; i < len; i++) {
+                scanForLocations(DOM.getChild(elem, i));
+            }
+        }
+    }
+
+    /** Get the location attribute for given element */
+    private static native String getLocation(Element elem)
+    /*-{
+        return elem.getAttribute("location");
+    }-*/;
+
+    /** Evaluate given script in browser document */
+    private static native void eval(String script)
+    /*-{
+      try {
+        if (script != null) 
+      eval("{ var document = $doc; var window = $wnd; "+ script + "}");
+      } catch (e) {
+      }
+    }-*/;
+
+    /** Prefix all img tag srcs with given prefix. */
+    private static native void prefixImgSrcs(Element e, String srcPrefix)
+    /*-{
+      try {
+          var divs = e.getElementsByTagName("img"); 
+          var base = "" + $doc.location;
+          var l = base.length-1;
+          while (l >= 0 && base.charAt(l) != "/") l--;
+          base = base.substring(0,l+1);
+          for (var i = 0; i < divs.length; i++) {
+              var div = divs[i];
+              var src = div.getAttribute("src");
+              if (src.indexOf("/")==0 || src.match(/\w+:\/\//)) {
+                  continue;
+              }
+              div.setAttribute("src",srcPrefix + src);             
+          }                    
+      } catch (e) { alert(e + " " + srcPrefix);}
+    }-*/;
+
+    /**
+     * Extract body part and script tags from raw html-template.
+     * 
+     * Saves contents of all script-tags to private property: scripts. Returns
+     * contents of the body part for the html without script-tags. Also replaces
+     * all _UID_ tags with an unique id-string.
+     * 
+     * @param html
+     *                Original HTML-template received from server
+     * @return html that is used to create the HTMLPanel.
+     */
+    private String extractBodyAndScriptsFromTemplate(String html) {
+
+        // Replace UID:s
+        html = html.replaceAll("_UID_", pid + "__");
+
+        // Exctract script-tags
+        scripts = "";
+        int endOfPrevScript = 0;
+        int nextPosToCheck = 0;
+        String lc = html.toLowerCase();
+        String res = "";
+        int scriptStart = lc.indexOf("<script", nextPosToCheck);
+        while (scriptStart > 0) {
+            res += html.substring(endOfPrevScript, scriptStart);
+            scriptStart = lc.indexOf(">", scriptStart);
+            final int j = lc.indexOf("</script>", scriptStart);
+            scripts += html.substring(scriptStart + 1, j) + ";";
+            nextPosToCheck = endOfPrevScript = j + "</script>".length();
+            scriptStart = lc.indexOf("<script", nextPosToCheck);
+        }
+        res += html.substring(endOfPrevScript);
+
+        // Extract body
+        html = res;
+        lc = html.toLowerCase();
+        int startOfBody = lc.indexOf("<body");
+        if (startOfBody < 0) {
+            res = html;
+        } else {
+            res = "";
+            startOfBody = lc.indexOf(">", startOfBody) + 1;
+            final int endOfBody = lc.indexOf("</body>", startOfBody);
+            if (endOfBody > startOfBody) {
+                res = html.substring(startOfBody, endOfBody);
+            } else {
+                res = html.substring(startOfBody);
+            }
+        }
+
+        return res;
+    }
+
+    /** Replace child components */
+    public void replaceChildComponent(Widget from, Widget to) {
+        final String location = getLocation(from);
+        if (location == null) {
+            throw new IllegalArgumentException();
+        }
+        setWidget(to, location);
+    }
+
+    /** Does this layout contain given child */
+    public boolean hasChildComponent(Widget component) {
+        return locationToWidget.containsValue(component);
+    }
+
+    /** Update caption for given widget */
+    public void updateCaption(Paintable component, UIDL uidl) {
+        CaptionWrapper wrapper = (CaptionWrapper) widgetToCaptionWrapper
+                .get(component);
+        if (Caption.isNeeded(uidl)) {
+            if (wrapper == null) {
+                final String loc = getLocation((Widget) component);
+                super.remove((Widget) component);
+                wrapper = new CaptionWrapper(component, client);
+                super.add(wrapper, (Element) locationToElement.get(loc));
+                widgetToCaptionWrapper.put(component, wrapper);
+            }
+            wrapper.updateCaption(uidl);
+        } else {
+            if (wrapper != null) {
+                final String loc = getLocation((Widget) component);
+                super.remove(wrapper);
+                super.add((Widget) wrapper.getPaintable(),
+                        (Element) locationToElement.get(loc));
+                widgetToCaptionWrapper.remove(component);
+            }
+        }
+    }
+
+    /** Get the location of an widget */
+    public String getLocation(Widget w) {
+        for (final Iterator i = locationToWidget.keySet().iterator(); i
+                .hasNext();) {
+            final String location = (String) i.next();
+            if (locationToWidget.get(location) == w) {
+                return location;
+            }
+        }
+        return null;
+    }
+
+    /** Removes given widget from the layout */
+    public boolean remove(Widget w) {
+        client.unregisterPaintable((Paintable) w);
+        final String location = getLocation(w);
+        if (location != null) {
+            locationToWidget.remove(location);
+        }
+        final CaptionWrapper cw = (CaptionWrapper) widgetToCaptionWrapper
+                .get(w);
+        if (cw != null) {
+            widgetToCaptionWrapper.remove(w);
+            return super.remove(cw);
+        } else if (w != null) {
+            return super.remove(w);
+        }
+        return false;
+    }
+
+    /** Adding widget without specifying location is not supported */
+    public void add(Widget w) {
+        throw new UnsupportedOperationException();
+    }
+
+    /** Clear all widgets from the layout */
+    public void clear() {
+        super.clear();
+        locationToWidget.clear();
+        widgetToCaptionWrapper.clear();
+    }
+
+    public void iLayout() {
+        if (!iLayoutJS(DOM.getFirstChild(getElement()))) {
+            Util.runDescendentsLayout(this);
+        }
+    }
+
+    /**
+     * This method is published to JS side with the same name into first DOM
+     * node of custom layout. This way if one implements some resizeable
+     * containers in custom layout he/she can notify children after resize.
+     */
+    public void notifyChildrenOfSizeChange() {
+        Util.runDescendentsLayout(this);
+    }
+
+    public void onDetach() {
+        detachResizedFunction(DOM.getFirstChild(getElement()));
+    }
+
+    private native void detachResizedFunction(Element element)
+    /*-{
+       element.notifyChildrenOfSizeChange = null;
+    }-*/;
+
+    private native void publishResizedFunction(Element element)
+    /*-{
+       var self = this;
+       element.notifyChildrenOfSizeChange = function() {
+               self.@com.itmill.toolkit.terminal.gwt.client.ui.ICustomLayout::notifyChildrenOfSizeChange()();
+       };
+    }-*/;
+
+    /**
+     * In custom layout one may want to run layout functions made with
+     * JavaScript. This function tests if one exists (with name "iLayoutJS" in
+     * layouts first DOM node) and runs et. Return value is used to determine if
+     * children needs to be notified of size changes.
+     * 
+     * Note! When implementing a JS layout function you most likely want to call
+     * notifyChildrenOfSizeChange() function on your custom layouts main
+     * element. That method is used to control whether child components layout
+     * functions are to be run.
+     * 
+     * @param el
+     * @return true if layout function exists and was run successfully, else
+     *         false.
+     */
+    private native boolean iLayoutJS(Element el)
+    /*-{
+       if(el && el.iLayoutJS) {
+               try {
+                       el.iLayoutJS();
+                       return true;
+               } catch (e) {
+                       return false;
+               }
+       } else {
+               return false;
+       }
+    }-*/;
+}
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/IDateField.java b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/IDateField.java
new file mode 100644 (file)
index 0000000..123ded4
--- /dev/null
@@ -0,0 +1,231 @@
+/* \r
+@ITMillApache2LicenseForJavaFiles@\r
+ */\r
+\r
+package com.itmill.toolkit.terminal.gwt.client.ui;\r
+\r
+import java.util.Date;\r
+\r
+import com.google.gwt.user.client.Event;\r
+import com.google.gwt.user.client.ui.FlowPanel;\r
+import com.itmill.toolkit.terminal.gwt.client.ApplicationConnection;\r
+import com.itmill.toolkit.terminal.gwt.client.DateTimeService;\r
+import com.itmill.toolkit.terminal.gwt.client.LocaleNotLoadedException;\r
+import com.itmill.toolkit.terminal.gwt.client.Paintable;\r
+import com.itmill.toolkit.terminal.gwt.client.Tooltip;\r
+import com.itmill.toolkit.terminal.gwt.client.UIDL;\r
+\r
+public class IDateField extends FlowPanel implements Paintable, Field {\r
+\r
+    public static final String CLASSNAME = "i-datefield";\r
+\r
+    protected String id;\r
+\r
+    protected ApplicationConnection client;\r
+\r
+    protected boolean immediate;\r
+\r
+    public static final int RESOLUTION_YEAR = 0;\r
+    public static final int RESOLUTION_MONTH = 1;\r
+    public static final int RESOLUTION_DAY = 2;\r
+    public static final int RESOLUTION_HOUR = 3;\r
+    public static final int RESOLUTION_MIN = 4;\r
+    public static final int RESOLUTION_SEC = 5;\r
+    public static final int RESOLUTION_MSEC = 6;\r
+\r
+    protected int currentResolution = RESOLUTION_YEAR;\r
+\r
+    protected String currentLocale;\r
+\r
+    protected boolean readonly;\r
+\r
+    protected boolean enabled;\r
+\r
+    protected Date date = null;\r
+    // e.g when paging a calendar, before actually selecting\r
+    protected Date showingDate = new Date();\r
+\r
+    protected DateTimeService dts;\r
+\r
+    public IDateField() {\r
+        setStyleName(CLASSNAME);\r
+        dts = new DateTimeService();\r
+        sinkEvents(Tooltip.TOOLTIP_EVENTS);\r
+    }\r
+\r
+    public void onBrowserEvent(Event event) {\r
+        super.onBrowserEvent(event);\r
+        if (client != null) {\r
+            client.handleTooltipEvent(event, this);\r
+        }\r
+    }\r
+\r
+    public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {\r
+        // Ensure correct implementation and let layout manage caption\r
+        if (client.updateComponent(this, uidl, true)) {\r
+            return;\r
+        }\r
+\r
+        // Save details\r
+        this.client = client;\r
+        id = uidl.getId();\r
+        immediate = uidl.getBooleanAttribute("immediate");\r
+\r
+        readonly = uidl.getBooleanAttribute("readonly");\r
+        enabled = !uidl.getBooleanAttribute("disabled");\r
+\r
+        if (uidl.hasAttribute("locale")) {\r
+            final String locale = uidl.getStringAttribute("locale");\r
+            try {\r
+                dts.setLocale(locale);\r
+                currentLocale = locale;\r
+            } catch (final LocaleNotLoadedException e) {\r
+                currentLocale = dts.getLocale();\r
+                // TODO redirect this to console\r
+                System.out.println("Tried to use an unloaded locale \""\r
+                        + locale + "\". Using default locale (" + currentLocale\r
+                        + ").");\r
+            }\r
+        }\r
+\r
+        int newResolution;\r
+        if (uidl.hasVariable("msec")) {\r
+            newResolution = RESOLUTION_MSEC;\r
+        } else if (uidl.hasVariable("sec")) {\r
+            newResolution = RESOLUTION_SEC;\r
+        } else if (uidl.hasVariable("min")) {\r
+            newResolution = RESOLUTION_MIN;\r
+        } else if (uidl.hasVariable("hour")) {\r
+            newResolution = RESOLUTION_HOUR;\r
+        } else if (uidl.hasVariable("day")) {\r
+            newResolution = RESOLUTION_DAY;\r
+        } else if (uidl.hasVariable("month")) {\r
+            newResolution = RESOLUTION_MONTH;\r
+        } else {\r
+            newResolution = RESOLUTION_YEAR;\r
+        }\r
+\r
+        currentResolution = newResolution;\r
+\r
+        final int year = uidl.getIntVariable("year");\r
+        final int month = (currentResolution >= RESOLUTION_MONTH) ? uidl\r
+                .getIntVariable("month") : -1;\r
+        final int day = (currentResolution >= RESOLUTION_DAY) ? uidl\r
+                .getIntVariable("day") : -1;\r
+        final int hour = (currentResolution >= RESOLUTION_HOUR) ? uidl\r
+                .getIntVariable("hour") : 0;\r
+        final int min = (currentResolution >= RESOLUTION_MIN) ? uidl\r
+                .getIntVariable("min") : 0;\r
+        final int sec = (currentResolution >= RESOLUTION_SEC) ? uidl\r
+                .getIntVariable("sec") : 0;\r
+        final int msec = (currentResolution >= RESOLUTION_MSEC) ? uidl\r
+                .getIntVariable("msec") : 0;\r
+\r
+        // Construct new date for this datefield (only if not null)\r
+        if (year > -1) {\r
+            date = new Date((long) getTime(year, month, day, hour, min, sec,\r
+                    msec));\r
+            showingDate.setTime(date.getTime());\r
+        } else {\r
+            date = null;\r
+            showingDate = new Date();\r
+        }\r
+\r
+    }\r
+\r
+    /*\r
+     * We need this redundant native function because Java's Date object doesn't\r
+     * have a setMilliseconds method.\r
+     */\r
+    private static native double getTime(int y, int m, int d, int h, int mi,\r
+            int s, int ms)\r
+    /*-{\r
+       try {\r
+               var date = new Date(2000,1,1,1); // don't use current date here\r
+               if(y && y >= 0) date.setFullYear(y);\r
+               if(m && m >= 1) date.setMonth(m-1);\r
+               if(d && d >= 0) date.setDate(d);\r
+               if(h >= 0) date.setHours(h);\r
+               if(mi >= 0) date.setMinutes(mi);\r
+               if(s >= 0) date.setSeconds(s);\r
+               if(ms >= 0) date.setMilliseconds(ms);\r
+               return date.getTime();\r
+       } catch (e) {\r
+               // TODO print some error message on the console\r
+               //console.log(e);\r
+               return (new Date()).getTime();\r
+       }\r
+    }-*/;\r
+\r
+    public int getMilliseconds() {\r
+        return (int) (date.getTime() - date.getTime() / 1000 * 1000);\r
+    }\r
+\r
+    public void setMilliseconds(int ms) {\r
+        date.setTime(date.getTime() / 1000 * 1000 + ms);\r
+    }\r
+\r
+    public int getShowingMilliseconds() {\r
+        return (int) (showingDate.getTime() - showingDate.getTime() / 1000 * 1000);\r
+    }\r
+\r
+    public void setShowingMilliseconds(int ms) {\r
+        showingDate.setTime(showingDate.getTime() / 1000 * 1000 + ms);\r
+    }\r
+\r
+    public int getCurrentResolution() {\r
+        return currentResolution;\r
+    }\r
+\r
+    public void setCurrentResolution(int currentResolution) {\r
+        this.currentResolution = currentResolution;\r
+    }\r
+\r
+    public String getCurrentLocale() {\r
+        return currentLocale;\r
+    }\r
+\r
+    public void setCurrentLocale(String currentLocale) {\r
+        this.currentLocale = currentLocale;\r
+    }\r
+\r
+    public Date getCurrentDate() {\r
+        return date;\r
+    }\r
+\r
+    public void setCurrentDate(Date date) {\r
+        this.date = date;\r
+    }\r
+\r
+    public Date getShowingDate() {\r
+        return showingDate;\r
+    }\r
+\r
+    public void setShowingDate(Date date) {\r
+        showingDate = date;\r
+    }\r
+\r
+    public boolean isImmediate() {\r
+        return immediate;\r
+    }\r
+\r
+    public boolean isReadonly() {\r
+        return readonly;\r
+    }\r
+\r
+    public boolean isEnabled() {\r
+        return enabled;\r
+    }\r
+\r
+    public DateTimeService getDateTimeService() {\r
+        return dts;\r
+    }\r
+\r
+    public String getId() {\r
+        return id;\r
+    }\r
+\r
+    public ApplicationConnection getClient() {\r
+        return client;\r
+    }\r
+}\r
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/IDateFieldCalendar.java b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/IDateFieldCalendar.java
new file mode 100644 (file)
index 0000000..895b2c7
--- /dev/null
@@ -0,0 +1,25 @@
+/* \r
+@ITMillApache2LicenseForJavaFiles@\r
+ */\r
+\r
+package com.itmill.toolkit.terminal.gwt.client.ui;\r
+\r
+import com.itmill.toolkit.terminal.gwt.client.ApplicationConnection;\r
+import com.itmill.toolkit.terminal.gwt.client.UIDL;\r
+\r
+public class IDateFieldCalendar extends IDateField {\r
+\r
+    private final CalendarPanel date;\r
+\r
+    public IDateFieldCalendar() {\r
+        super();\r
+        date = new CalendarPanel(this);\r
+        add(date);\r
+    }\r
+\r
+    public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {\r
+        super.updateFromUIDL(uidl, client);\r
+        date.updateCalendar();\r
+    }\r
+\r
+}\r
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/IEmbedded.java b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/IEmbedded.java
new file mode 100644 (file)
index 0000000..d1df560
--- /dev/null
@@ -0,0 +1,127 @@
+/* 
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.itmill.toolkit.terminal.gwt.client.ui;
+
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.Element;
+import com.google.gwt.user.client.ui.HTML;
+import com.itmill.toolkit.terminal.gwt.client.ApplicationConnection;
+import com.itmill.toolkit.terminal.gwt.client.Paintable;
+import com.itmill.toolkit.terminal.gwt.client.UIDL;
+
+public class IEmbedded extends HTML implements Paintable {
+    private static String CLASSNAME = "i-embedded";
+
+    private String heigth;
+    private String width;
+    private Element browserElement;
+
+    public IEmbedded() {
+        setStyleName(CLASSNAME);
+    }
+
+    public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+        if (client.updateComponent(this, uidl, true)) {
+            return;
+        }
+
+        boolean clearBrowserElement = true;
+
+        if (uidl.hasAttribute("type")) {
+            final String type = uidl.getStringAttribute("type");
+            if (type.equals("image")) {
+                String w = uidl.getStringAttribute("width");
+                if (w != null) {
+                    w = " width=\"" + w + "\" ";
+                } else {
+                    w = "";
+                }
+                String h = uidl.getStringAttribute("height");
+                if (h != null) {
+                    h = " height=\"" + h + "\" ";
+                } else {
+                    h = "";
+                }
+                setHTML("<img src=\"" + getSrc(uidl, client) + "\"" + w + h
+                        + "/>");
+                client.addPngFix(DOM.getFirstChild(getElement()));
+
+            } else if (type.equals("browser")) {
+                if (browserElement == null) {
+                    setHTML("<iframe width=\"100%\" height=\"100%\" frameborder=\"0\" src=\""
+                            + getSrc(uidl, client) + "\"></iframe>");
+                    browserElement = DOM.getFirstChild(getElement());
+                } else {
+                    DOM.setElementAttribute(browserElement, "src", getSrc(uidl,
+                            client));
+                }
+                clearBrowserElement = false;
+            } else {
+                ApplicationConnection.getConsole().log(
+                        "Unknown Embedded type '" + type + "'");
+            }
+        } else if (uidl.hasAttribute("mimetype")) {
+            final String mime = uidl.getStringAttribute("mimetype");
+            if (mime.equals("application/x-shockwave-flash")) {
+                setHTML("<object width=\"" + width + "\" height=\"" + heigth
+                        + "\"><param name=\"movie\" value=\""
+                        + getSrc(uidl, client) + "\"><embed src=\""
+                        + getSrc(uidl, client) + "\" width=\"" + width
+                        + "\" height=\"" + heigth + "\"></embed></object>");
+            } else {
+                ApplicationConnection.getConsole().log(
+                        "Unknown Embedded mimetype '" + mime + "'");
+            }
+        } else {
+            ApplicationConnection.getConsole().log(
+                    "Unknown Embedded; no type or mimetype attribute");
+        }
+
+        if (clearBrowserElement) {
+            browserElement = null;
+        }
+
+    }
+
+    /**
+     * Helper to return translated src-attribute from embedded's UIDL
+     * 
+     * @param uidl
+     * @param client
+     * @return
+     */
+    private String getSrc(UIDL uidl, ApplicationConnection client) {
+        String url = client.translateToolkitUri(uidl.getStringAttribute("src"));
+        if (url == null) {
+            return "";
+        }
+        return url;
+    }
+
+    public void setWidth(String width) {
+        if (width == null || width.equals("")) {
+            width = "100%";
+        }
+        this.width = width;
+        super.setHeight(width);
+    }
+
+    public void setHeight(String height) {
+        if (height == null || height.equals("")) {
+            height = "100%";
+        }
+        heigth = height;
+        super.setHeight(height);
+    }
+
+    protected void onDetach() {
+        // Force browser to fire unload event when component is detached from
+        // the view (IE doesn't do this automatically)
+        if (browserElement != null) {
+            DOM.setElementAttribute(browserElement, "src", "javascript:false");
+        }
+        super.onDetach();
+    }
+}
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/IExpandLayout.java b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/IExpandLayout.java
new file mode 100644 (file)
index 0000000..b52af23
--- /dev/null
@@ -0,0 +1,713 @@
+/* 
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.itmill.toolkit.terminal.gwt.client.ui;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.Element;
+import com.google.gwt.user.client.ui.ComplexPanel;
+import com.google.gwt.user.client.ui.RootPanel;
+import com.google.gwt.user.client.ui.UIObject;
+import com.google.gwt.user.client.ui.Widget;
+import com.itmill.toolkit.terminal.gwt.client.ApplicationConnection;
+import com.itmill.toolkit.terminal.gwt.client.Caption;
+import com.itmill.toolkit.terminal.gwt.client.Container;
+import com.itmill.toolkit.terminal.gwt.client.ContainerResizedListener;
+import com.itmill.toolkit.terminal.gwt.client.Paintable;
+import com.itmill.toolkit.terminal.gwt.client.StyleConstants;
+import com.itmill.toolkit.terminal.gwt.client.UIDL;
+import com.itmill.toolkit.terminal.gwt.client.Util;
+
+/**
+ * @author IT Mill Ltd
+ */
+public class IExpandLayout extends ComplexPanel implements
+        ContainerResizedListener, Container {
+
+    public static final String CLASSNAME = "i-expandlayout";
+    public static final int ORIENTATION_HORIZONTAL = 1;
+
+    public static final int ORIENTATION_VERTICAL = 0;
+
+    /**
+     * Minimum pixels reserved for expanded element to avoid "odd" situations
+     * where expanded element is 0 size. Default is 5 pixels to show user a hint
+     * that there is a component. Then user can often use splitpanel or resize
+     * window to show component properly. This value may be insane in some
+     * applications. Override this to specify a proper for your case.
+     */
+    protected static final int EXPANDED_ELEMENTS_MIN_WIDTH = 5;
+
+    /**
+     * Contains reference to Element where Paintables are wrapped.
+     */
+    protected Element childContainer;
+
+    protected ApplicationConnection client;
+
+    protected HashMap componentToCaption = new HashMap();
+
+    /*
+     * Elements that provides the Layout interface implementation.
+     */
+    protected Element element;
+    private Widget expandedWidget;
+
+    private UIDL expandedWidgetUidl;
+
+    int orientationMode = ORIENTATION_VERTICAL;
+
+    protected int topMargin = -1;
+    private String width;
+    private String height;
+    private Element marginElement;
+    private Element breakElement;
+    private int bottomMargin = -1;
+    private boolean hasComponentSpacing;
+    private int spacingSize = -1;
+
+    public IExpandLayout() {
+        this(IExpandLayout.ORIENTATION_VERTICAL);
+    }
+
+    public IExpandLayout(int orientation) {
+        orientationMode = orientation;
+        constructDOM();
+        setStyleName(CLASSNAME);
+    }
+
+    public void add(Widget w) {
+        final WidgetWrapper wrapper = createWidgetWrappper();
+        DOM.appendChild(childContainer, wrapper.getElement());
+        super.add(w, wrapper.getContainerElement());
+    }
+
+    protected void constructDOM() {
+        element = DOM.createDiv();
+        // DOM.setStyleAttribute(element, "overflow", "hidden");
+
+        if (orientationMode == ORIENTATION_HORIZONTAL) {
+            marginElement = DOM.createDiv();
+            if (Util.isIE()) {
+                DOM.setStyleAttribute(marginElement, "zoom", "1");
+                DOM.setStyleAttribute(marginElement, "overflow", "hidden");
+            }
+            childContainer = DOM.createDiv();
+            if (Util.isIE()) {
+                DOM.setStyleAttribute(childContainer, "zoom", "1");
+                DOM.setStyleAttribute(childContainer, "overflow", "hidden");
+            }
+            DOM.setStyleAttribute(childContainer, "height", "100%");
+            breakElement = DOM.createDiv();
+            DOM.setStyleAttribute(breakElement, "overflow", "hidden");
+            DOM.setStyleAttribute(breakElement, "height", "0px");
+            DOM.setStyleAttribute(breakElement, "clear", "both");
+            DOM.appendChild(marginElement, childContainer);
+            DOM.appendChild(marginElement, breakElement);
+            DOM.appendChild(element, marginElement);
+        } else {
+            childContainer = DOM.createDiv();
+            DOM.appendChild(element, childContainer);
+            marginElement = childContainer;
+        }
+        setElement(element);
+    }
+
+    protected WidgetWrapper createWidgetWrappper() {
+        if (orientationMode == ORIENTATION_HORIZONTAL) {
+            return new HorizontalWidgetWrapper();
+        } else {
+            return new VerticalWidgetWrapper();
+        }
+    }
+
+    /**
+     * Returns given widgets WidgetWrapper
+     * 
+     * @param child
+     * @return
+     */
+    public WidgetWrapper getWidgetWrapperFor(Widget child) {
+        final Element containerElement = DOM.getParent(child.getElement());
+        if (orientationMode == ORIENTATION_HORIZONTAL) {
+            return new HorizontalWidgetWrapper(containerElement);
+        } else {
+            return new VerticalWidgetWrapper(containerElement);
+        }
+    }
+
+    abstract class WidgetWrapper extends UIObject {
+        /**
+         * @return element that contains Widget
+         */
+        public Element getContainerElement() {
+            return getElement();
+        }
+
+        abstract void setExpandedSize(int pixels);
+
+        abstract void setAlignment(String verticalAlignment,
+                String horizontalAlignment);
+
+        abstract void setSpacingEnabled(boolean b);
+    }
+
+    class VerticalWidgetWrapper extends WidgetWrapper {
+
+        public VerticalWidgetWrapper(Element div) {
+            setElement(div);
+        }
+
+        public VerticalWidgetWrapper() {
+            setElement(DOM.createDiv());
+            // Set to 'hidden' at first (prevent IE6 content overflows), and set
+            // to 'auto' later.
+            DOM.setStyleAttribute(getContainerElement(), "overflow", "hidden");
+        }
+
+        void setExpandedSize(int pixels) {
+            final int spaceForMarginsAndSpacings = getOffsetHeight()
+                    - DOM.getElementPropertyInt(getElement(), "clientHeight");
+            int fixedInnerSize = pixels - spaceForMarginsAndSpacings;
+            if (fixedInnerSize < 0) {
+                fixedInnerSize = 0;
+            }
+            setHeight(fixedInnerSize + "px");
+            DOM.setStyleAttribute(getContainerElement(), "overflow", "auto");
+        }
+
+        void setAlignment(String verticalAlignment, String horizontalAlignment) {
+            DOM.setStyleAttribute(getElement(), "textAlign",
+                    horizontalAlignment);
+            // ignoring vertical alignment
+        }
+
+        void setSpacingEnabled(boolean b) {
+            setStyleName(getElement(), CLASSNAME + "-"
+                    + StyleConstants.VERTICAL_SPACING, b);
+        }
+    }
+
+    class HorizontalWidgetWrapper extends WidgetWrapper {
+
+        private Element td;
+        private String valign = "top";
+        private String align = "left";
+
+        public HorizontalWidgetWrapper(Element element) {
+            if (DOM.getElementProperty(element, "nodeName").equals("TD")) {
+                td = element;
+                setElement(DOM.getParent(DOM.getParent(DOM.getParent(DOM
+                        .getParent(td)))));
+            } else {
+                setElement(element);
+            }
+        }
+
+        public HorizontalWidgetWrapper() {
+            setElement(DOM.createDiv());
+            DOM.setStyleAttribute(getElement(), "cssFloat", "left");
+            if (Util.isIE()) {
+                DOM.setStyleAttribute(getElement(), "styleFloat", "left");
+            }
+            DOM.setStyleAttribute(getElement(), "height", "100%");
+        }
+
+        void setExpandedSize(int pixels) {
+            setWidth(pixels + "px");
+            DOM.setStyleAttribute(getElement(), "overflow", "hidden");
+        }
+
+        void setAlignment(String verticalAlignment, String horizontalAlignment) {
+            DOM.setStyleAttribute(getElement(), "verticalAlign",
+                    verticalAlignment);
+            if (!valign.equals(verticalAlignment)) {
+                if (verticalAlignment.equals("top")) {
+                    // remove table, move content to div
+
+                } else {
+                    if (td == null) {
+                        // build one cell table
+                        final Element table = DOM.createTable();
+                        final Element tBody = DOM.createTBody();
+                        final Element tr = DOM.createTR();
+                        td = DOM.createTD();
+                        DOM.appendChild(table, tBody);
+                        DOM.appendChild(tBody, tr);
+                        DOM.appendChild(tr, td);
+                        DOM.setElementProperty(table, "className", CLASSNAME
+                                + "-valign");
+                        DOM.setElementProperty(tr, "className", CLASSNAME
+                                + "-valign");
+                        DOM.setElementProperty(td, "className", CLASSNAME
+                                + "-valign");
+                        // move possible content to cell
+                        final Element content = DOM.getFirstChild(getElement());
+                        if (content != null) {
+                            DOM.removeChild(getElement(), content);
+                            DOM.appendChild(td, content);
+                        }
+                        DOM.appendChild(getElement(), table);
+                    }
+                    // set alignment
+                    DOM.setStyleAttribute(td, "verticalAlign",
+                            verticalAlignment);
+                }
+                valign = verticalAlignment;
+            }
+            if (!align.equals(horizontalAlignment)) {
+                DOM.setStyleAttribute(getContainerElement(), "textAlign",
+                        horizontalAlignment);
+                align = horizontalAlignment;
+            }
+        }
+
+        public Element getContainerElement() {
+            if (td == null) {
+                return super.getContainerElement();
+            } else {
+                return td;
+            }
+        }
+
+        void setSpacingEnabled(boolean b) {
+            setStyleName(getElement(), CLASSNAME + "-"
+                    + StyleConstants.HORIZONTAL_SPACING, b);
+        }
+    }
+
+    protected ArrayList getPaintables() {
+        final ArrayList al = new ArrayList();
+        final Iterator it = iterator();
+        while (it.hasNext()) {
+            final Widget w = (Widget) it.next();
+            if (w instanceof Paintable) {
+                al.add(w);
+            }
+        }
+        return al;
+    }
+
+    public Widget getWidget(int index) {
+        return getChildren().get(index);
+    }
+
+    public int getWidgetCount() {
+        return getChildren().size();
+    }
+
+    public int getWidgetIndex(Widget child) {
+        return getChildren().indexOf(child);
+    }
+
+    protected void handleAlignments(UIDL uidl) {
+        // Component alignments as a comma separated list.
+        // See com.itmill.toolkit.terminal.gwt.client.ui.AlignmentInfo.java for
+        // possible values.
+        final int[] alignments = uidl.getIntArrayAttribute("alignments");
+        int alignmentIndex = 0;
+        // Set alignment attributes
+        final Iterator it = getPaintables().iterator();
+        boolean first = true;
+        while (it.hasNext()) {
+            // Calculate alignment info
+            final AlignmentInfo ai = new AlignmentInfo(
+                    alignments[alignmentIndex++]);
+            final WidgetWrapper wr = getWidgetWrapperFor((Widget) it.next());
+            wr.setAlignment(ai.getVerticalAlignment(), ai
+                    .getHorizontalAlignment());
+            if (first) {
+                wr.setSpacingEnabled(false);
+                first = false;
+            } else {
+                wr.setSpacingEnabled(hasComponentSpacing);
+            }
+
+        }
+    }
+
+    protected void handleMargins(UIDL uidl) {
+        if (uidl.hasAttribute("margins")) {
+            final MarginInfo margins = new MarginInfo(uidl
+                    .getIntAttribute("margins"));
+            setStyleName(marginElement, CLASSNAME + "-"
+                    + StyleConstants.MARGIN_TOP, margins.hasTop());
+            setStyleName(marginElement, CLASSNAME + "-"
+                    + StyleConstants.MARGIN_RIGHT, margins.hasRight());
+            setStyleName(marginElement, CLASSNAME + "-"
+                    + StyleConstants.MARGIN_BOTTOM, margins.hasBottom());
+            setStyleName(marginElement, CLASSNAME + "-"
+                    + StyleConstants.MARGIN_LEFT, margins.hasLeft());
+        }
+    }
+
+    public boolean hasChildComponent(Widget component) {
+        return getWidgetIndex(component) >= 0;
+    }
+
+    public void iLayout() {
+        if (orientationMode == ORIENTATION_HORIZONTAL) {
+            int pixels;
+            if ("".equals(height)) {
+                // try to find minimum height by looping all widgets
+                int maxHeight = 0;
+                Iterator iterator = getPaintables().iterator();
+                while (iterator.hasNext()) {
+                    Widget w = (Widget) iterator.next();
+                    int h = w.getOffsetHeight();
+                    if (h > maxHeight) {
+                        maxHeight = h;
+                    }
+                }
+                pixels = maxHeight;
+            } else {
+                pixels = getOffsetHeight() - getTopMargin() - getBottomMargin();
+                if (pixels < 0) {
+                    pixels = 0;
+                }
+            }
+            DOM.setStyleAttribute(marginElement, "height", pixels + "px");
+            DOM.setStyleAttribute(marginElement, "overflow", "hidden");
+        }
+
+        if (expandedWidget == null) {
+            return;
+        }
+
+        final int availableSpace = getAvailableSpace();
+
+        final int usedSpace = getUsedSpace();
+
+        int spaceForExpandedWidget = availableSpace - usedSpace;
+
+        if (spaceForExpandedWidget < EXPANDED_ELEMENTS_MIN_WIDTH) {
+            // TODO fire warning for developer
+            spaceForExpandedWidget = EXPANDED_ELEMENTS_MIN_WIDTH;
+        }
+
+        final WidgetWrapper wr = getWidgetWrapperFor(expandedWidget);
+        wr.setExpandedSize(spaceForExpandedWidget);
+
+        // TODO save previous size and only propagate if really changed
+        Util.runDescendentsLayout(this);
+    }
+
+    private int getTopMargin() {
+        if (topMargin < 0) {
+            topMargin = DOM.getElementPropertyInt(childContainer, "offsetTop")
+                    - DOM.getElementPropertyInt(getElement(), "offsetTop");
+        }
+        if (topMargin < 0) {
+            // FIXME shouldn't happen
+            return 0;
+        } else {
+            return topMargin;
+        }
+    }
+
+    private int getBottomMargin() {
+        if (bottomMargin < 0) {
+            bottomMargin = DOM
+                    .getElementPropertyInt(marginElement, "offsetTop")
+                    + DOM.getElementPropertyInt(marginElement, "offsetHeight")
+                    - DOM.getElementPropertyInt(breakElement, "offsetTop");
+            if (bottomMargin < 0) {
+                // FIXME shouldn't happen
+                return 0;
+            }
+        }
+        return bottomMargin;
+    }
+
+    private int getUsedSpace() {
+        int total = 0;
+        final int widgetCount = getWidgetCount();
+        final Iterator it = iterator();
+        while (it.hasNext()) {
+            final Widget w = (Widget) it.next();
+            if (w != expandedWidget) {
+                final WidgetWrapper wr = getWidgetWrapperFor(w);
+                if (orientationMode == ORIENTATION_VERTICAL) {
+                    total += wr.getOffsetHeight();
+                } else {
+                    total += wr.getOffsetWidth();
+                }
+            }
+        }
+        total += getSpacingSize() * (widgetCount - 1);
+        return total;
+    }
+
+    private int getSpacingSize() {
+        if (hasComponentSpacing) {
+            if (spacingSize < 0) {
+                final Element temp = DOM.createDiv();
+                final WidgetWrapper wr = createWidgetWrappper();
+                wr.setSpacingEnabled(true);
+                DOM.appendChild(temp, wr.getElement());
+                DOM.setStyleAttribute(temp, "position", "absolute");
+                DOM.setStyleAttribute(temp, "top", "0");
+                DOM.setStyleAttribute(temp, "visibility", "hidden");
+                DOM.appendChild(RootPanel.getBodyElement(), temp);
+                if (orientationMode == ORIENTATION_HORIZONTAL) {
+                    spacingSize = DOM.getElementPropertyInt(wr.getElement(),
+                            "offsetLeft");
+                } else {
+                    spacingSize = DOM.getElementPropertyInt(wr.getElement(),
+                            "offsetTop");
+                }
+                DOM.removeChild(RootPanel.getBodyElement(), temp);
+            }
+            return spacingSize;
+        } else {
+            return 0;
+        }
+    }
+
+    private int getAvailableSpace() {
+        int size;
+        if (orientationMode == ORIENTATION_VERTICAL) {
+            if (Util.isIE6()) {
+                DOM.setStyleAttribute(getElement(), "overflow", "hidden");
+            }
+            size = getOffsetHeight();
+            if (Util.isIE6()) {
+                DOM.setStyleAttribute(getElement(), "overflow", "visible");
+            }
+
+            final int marginTop = DOM.getElementPropertyInt(DOM
+                    .getFirstChild(marginElement), "offsetTop")
+                    - DOM.getElementPropertyInt(element, "offsetTop");
+
+            final Element lastElement = DOM.getChild(marginElement, (DOM
+                    .getChildCount(marginElement) - 1));
+            final int marginBottom = DOM.getElementPropertyInt(marginElement,
+                    "offsetHeight")
+                    + DOM.getElementPropertyInt(marginElement, "offsetTop")
+                    - (DOM.getElementPropertyInt(lastElement, "offsetTop") + DOM
+                            .getElementPropertyInt(lastElement, "offsetHeight"));
+            size -= (marginTop + marginBottom);
+        } else {
+            // horizontal mode
+            size = DOM.getElementPropertyInt(childContainer, "offsetWidth");
+        }
+        return size;
+    }
+
+    protected void insert(Widget w, int beforeIndex) {
+        if (w instanceof Caption) {
+            final Caption c = (Caption) w;
+            // captions go into same container element as their
+            // owners
+            final Element container = DOM.getParent(((UIObject) c.getOwner())
+                    .getElement());
+            final Element captionContainer = DOM.createDiv();
+            DOM.insertChild(container, captionContainer, 0);
+            insert(w, captionContainer, beforeIndex, false);
+        } else {
+            final WidgetWrapper wrapper = createWidgetWrappper();
+            DOM.insertChild(childContainer, wrapper.getElement(), beforeIndex);
+            insert(w, wrapper.getContainerElement(), beforeIndex, false);
+        }
+    }
+
+    public boolean remove(int index) {
+        return remove(getWidget(index));
+    }
+
+    public boolean remove(Widget w) {
+        final WidgetWrapper ww = getWidgetWrapperFor(w);
+        final boolean removed = super.remove(w);
+        if (removed) {
+            if (!(w instanceof Caption)) {
+                DOM.removeChild(childContainer, ww.getElement());
+            }
+            return true;
+        }
+        return false;
+    }
+
+    public void removeCaption(Widget w) {
+        final Caption c = (Caption) componentToCaption.get(w);
+        if (c != null) {
+            this.remove(c);
+            componentToCaption.remove(w);
+        }
+    }
+
+    public boolean removePaintable(Paintable p) {
+        final Caption c = (Caption) componentToCaption.get(p);
+        if (c != null) {
+            componentToCaption.remove(c);
+            remove(c);
+        }
+        client.unregisterPaintable(p);
+        if (expandedWidget == p) {
+            expandedWidget = null;
+        }
+        return remove((Widget) p);
+    }
+
+    public void replaceChildComponent(Widget from, Widget to) {
+        client.unregisterPaintable((Paintable) from);
+        final Caption c = (Caption) componentToCaption.get(from);
+        if (c != null) {
+            remove(c);
+            componentToCaption.remove(c);
+        }
+        final int index = getWidgetIndex(from);
+        if (index >= 0) {
+            remove(index);
+            insert(to, index);
+        }
+    }
+
+    public void updateCaption(Paintable component, UIDL uidl) {
+
+        Caption c = (Caption) componentToCaption.get(component);
+
+        if (Caption.isNeeded(uidl)) {
+            if (c == null) {
+                final int index = getWidgetIndex((Widget) component);
+                c = new Caption(component, client);
+                insert(c, index);
+                componentToCaption.put(component, c);
+            }
+            c.updateCaption(uidl);
+        } else {
+            if (c != null) {
+                remove(c);
+                componentToCaption.remove(component);
+            }
+        }
+    }
+
+    public void setWidth(String newWidth) {
+        if (newWidth.equals(width)) {
+            return;
+        }
+        width = newWidth;
+        super.setWidth(width);
+    }
+
+    public void setHeight(String newHeight) {
+        if (newHeight.equals(height)) {
+            return;
+        }
+        height = newHeight;
+        super.setHeight(height);
+        if (orientationMode == ORIENTATION_HORIZONTAL) {
+            iLayout();
+        }
+    }
+
+    public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+
+        this.client = client;
+
+        // Modify layout margins
+        handleMargins(uidl);
+
+        // Ensure correct implementation
+        if (client.updateComponent(this, uidl, false)) {
+            return;
+        }
+
+        hasComponentSpacing = uidl.getBooleanAttribute("spacing");
+
+        final ArrayList uidlWidgets = new ArrayList();
+        for (final Iterator it = uidl.getChildIterator(); it.hasNext();) {
+            final UIDL cellUidl = (UIDL) it.next();
+            final Paintable child = client.getPaintable(cellUidl
+                    .getChildUIDL(0));
+            uidlWidgets.add(child);
+            if (cellUidl.hasAttribute("expanded")) {
+                expandedWidget = (Widget) child;
+                expandedWidgetUidl = cellUidl.getChildUIDL(0);
+            }
+        }
+
+        final ArrayList oldWidgets = getPaintables();
+
+        final Iterator oldIt = oldWidgets.iterator();
+        final Iterator newIt = uidlWidgets.iterator();
+        final Iterator newUidl = uidl.getChildIterator();
+
+        Widget oldChild = null;
+        while (newIt.hasNext()) {
+            final Widget child = (Widget) newIt.next();
+            final UIDL childUidl = ((UIDL) newUidl.next()).getChildUIDL(0);
+            if (oldChild == null && oldIt.hasNext()) {
+                // search for next old Paintable which still exists in layout
+                // and delete others
+                while (oldIt.hasNext()) {
+                    oldChild = (Widget) oldIt.next();
+                    // now oldChild is an instance of Paintable
+                    if (uidlWidgets.contains(oldChild)) {
+                        break;
+                    } else {
+                        removePaintable((Paintable) oldChild);
+                        oldChild = null;
+                    }
+                }
+            }
+            if (oldChild == null) {
+                // we are adding components to layout
+                add(child);
+            } else if (child == oldChild) {
+                // child already attached and updated
+                oldChild = null;
+            } else if (hasChildComponent(child)) {
+                // current child has been moved, re-insert before current
+                // oldChild
+                // TODO this might be optimized by moving only container element
+                // to correct position
+                removeCaption(child);
+                int index = getWidgetIndex(oldChild);
+                if (componentToCaption.containsKey(oldChild)) {
+                    index--;
+                }
+                remove(child);
+                insert(child, index);
+            } else {
+                // insert new child before old one
+                final int index = getWidgetIndex(oldChild);
+                insert(child, index);
+            }
+            if (child != expandedWidget) {
+                ((Paintable) child).updateFromUIDL(childUidl, client);
+            }
+        }
+        // remove possibly remaining old Paintable object which were not updated
+        while (oldIt.hasNext()) {
+            oldChild = (Widget) oldIt.next();
+            final Paintable p = (Paintable) oldChild;
+            if (!uidlWidgets.contains(p)) {
+                removePaintable(p);
+            }
+        }
+
+        if (uidlWidgets.size() == 0) {
+            return;
+        }
+
+        // Set component alignments
+        handleAlignments(uidl);
+
+        iLayout();
+
+        /*
+         * Expanded widget is updated after layout function so it has its
+         * container fixed at the moment of updateFromUIDL.
+         */
+        if (expandedWidget != null) {
+            ((Paintable) expandedWidget).updateFromUIDL(expandedWidgetUidl,
+                    client);
+        }
+    }
+}
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/IFilterSelect.java b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/IFilterSelect.java
new file mode 100644 (file)
index 0000000..05c05b7
--- /dev/null
@@ -0,0 +1,766 @@
+/* 
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.itmill.toolkit.terminal.gwt.client.ui;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Date;
+import java.util.Iterator;
+
+import com.google.gwt.user.client.Command;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.Element;
+import com.google.gwt.user.client.Event;
+import com.google.gwt.user.client.Window;
+import com.google.gwt.user.client.ui.ClickListener;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.FocusListener;
+import com.google.gwt.user.client.ui.HTML;
+import com.google.gwt.user.client.ui.Image;
+import com.google.gwt.user.client.ui.KeyboardListener;
+import com.google.gwt.user.client.ui.PopupListener;
+import com.google.gwt.user.client.ui.PopupPanel;
+import com.google.gwt.user.client.ui.TextBox;
+import com.google.gwt.user.client.ui.Widget;
+import com.google.gwt.user.client.ui.PopupPanel.PositionCallback;
+import com.google.gwt.user.client.ui.SuggestOracle.Suggestion;
+import com.itmill.toolkit.terminal.gwt.client.ApplicationConnection;
+import com.itmill.toolkit.terminal.gwt.client.Focusable;
+import com.itmill.toolkit.terminal.gwt.client.Paintable;
+import com.itmill.toolkit.terminal.gwt.client.Tooltip;
+import com.itmill.toolkit.terminal.gwt.client.UIDL;
+import com.itmill.toolkit.terminal.gwt.client.Util;
+
+/**
+ * 
+ * TODO needs major refactoring (to be extensible etc)
+ */
+public class IFilterSelect extends Composite implements Paintable, Field,
+        KeyboardListener, ClickListener, FocusListener, Focusable {
+
+    public class FilterSelectSuggestion implements Suggestion, Command {
+
+        private final String key;
+        private final String caption;
+        private String iconUri;
+
+        public FilterSelectSuggestion(UIDL uidl) {
+            key = uidl.getStringAttribute("key");
+            caption = uidl.getStringAttribute("caption");
+            if (uidl.hasAttribute("icon")) {
+                iconUri = client.translateToolkitUri(uidl
+                        .getStringAttribute("icon"));
+            }
+        }
+
+        public String getDisplayString() {
+            final StringBuffer sb = new StringBuffer();
+            if (iconUri != null) {
+                sb.append("<img src=\"");
+                sb.append(iconUri);
+                sb.append("\" alt=\"icon\" class=\"i-icon\" />");
+            }
+            sb.append(Util.escapeHTML(caption));
+            return sb.toString();
+        }
+
+        public String getReplacementString() {
+            return caption;
+        }
+
+        public int getOptionKey() {
+            return Integer.parseInt(key);
+        }
+
+        public String getIconUri() {
+            return iconUri;
+        }
+
+        public void execute() {
+            onSuggestionSelected(this);
+        }
+    }
+
+    /**
+     * @author mattitahvonen
+     * 
+     */
+    public class SuggestionPopup extends ToolkitOverlay implements
+            PositionCallback, PopupListener {
+        private static final int EXTRASPACE = 8;
+
+        private static final String Z_INDEX = "30000";
+
+        private final SuggestionMenu menu;
+
+        private final Element up = DOM.createDiv();
+        private final Element down = DOM.createDiv();
+        private final Element status = DOM.createDiv();
+
+        private boolean isPagingEnabled = true;
+
+        private long lastAutoClosed;
+
+        SuggestionPopup() {
+            super(true);
+            menu = new SuggestionMenu();
+            setWidget(menu);
+            setStyleName(CLASSNAME + "-suggestpopup");
+            DOM.setStyleAttribute(getElement(), "zIndex", Z_INDEX);
+
+            final Element root = getContainerElement();
+
+            DOM.setInnerHTML(up, "<span>Prev</span>");
+            DOM.sinkEvents(up, Event.ONCLICK);
+            DOM.setInnerHTML(down, "<span>Next</span>");
+            DOM.sinkEvents(down, Event.ONCLICK);
+            DOM.insertChild(root, up, 0);
+            DOM.appendChild(root, down);
+            DOM.appendChild(root, status);
+            DOM.setElementProperty(status, "className", CLASSNAME + "-status");
+
+            addPopupListener(this);
+        }
+
+        public void showSuggestions(Collection currentSuggestions,
+                int currentPage, int totalSuggestions) {
+
+            if (ApplicationConnection.isTestingMode()) {
+                // Add TT anchor point
+                DOM.setElementProperty(getElement(), "id", paintableId
+                        + "_OPTIONLIST");
+            }
+
+            menu.setSuggestions(currentSuggestions);
+            final int x = IFilterSelect.this.getAbsoluteLeft();
+            int y = tb.getAbsoluteTop();
+            y += tb.getOffsetHeight();
+            setPopupPosition(x, y);
+            final int first = currentPage * PAGELENTH
+                    + (nullSelectionAllowed && currentPage > 0 ? 0 : 1);
+            final int last = first + currentSuggestions.size() - 1;
+            final int matches = totalSuggestions
+                    - (nullSelectionAllowed ? 1 : 0);
+            if (last > 0) {
+                // nullsel not counted, as requested by user
+                DOM.setInnerText(status, (matches == 0 ? 0 : first)
+                        + "-"
+                        + ("".equals(lastFilter) && nullSelectionAllowed
+                                && currentPage == 0 ? last - 1 : last) + "/"
+                        + matches);
+            } else {
+                DOM.setInnerText(status, "");
+            }
+            setPrevButtonActive(first > 1);
+            setNextButtonActive(last < matches);
+
+            // clear previously fixed width
+            menu.setWidth("");
+            DOM.setStyleAttribute(DOM.getFirstChild(menu.getElement()),
+                    "width", "");
+
+            setPopupPositionAndShow(this);
+        }
+
+        private void setNextButtonActive(boolean b) {
+            if (b) {
+                DOM.sinkEvents(down, Event.ONCLICK);
+                DOM.setElementProperty(down, "className", CLASSNAME
+                        + "-nextpage");
+            } else {
+                DOM.sinkEvents(down, 0);
+                DOM.setElementProperty(down, "className", CLASSNAME
+                        + "-nextpage-off");
+            }
+        }
+
+        private void setPrevButtonActive(boolean b) {
+            if (b) {
+                DOM.sinkEvents(up, Event.ONCLICK);
+                DOM
+                        .setElementProperty(up, "className", CLASSNAME
+                                + "-prevpage");
+            } else {
+                DOM.sinkEvents(up, 0);
+                DOM.setElementProperty(up, "className", CLASSNAME
+                        + "-prevpage-off");
+            }
+
+        }
+
+        public void selectNextItem() {
+            final MenuItem cur = menu.getSelectedItem();
+            final int index = 1 + menu.getItems().indexOf(cur);
+            if (menu.getItems().size() > index) {
+                final MenuItem newSelectedItem = (MenuItem) menu.getItems()
+                        .get(index);
+                menu.selectItem(newSelectedItem);
+                tb.setText(newSelectedItem.getText());
+                tb.setSelectionRange(lastFilter.length(), newSelectedItem
+                        .getText().length()
+                        - lastFilter.length());
+
+            } else if (hasNextPage()) {
+                filterOptions(currentPage + 1, lastFilter);
+            }
+        }
+
+        public void selectPrevItem() {
+            final MenuItem cur = menu.getSelectedItem();
+            final int index = -1 + menu.getItems().indexOf(cur);
+            if (index > -1) {
+                final MenuItem newSelectedItem = (MenuItem) menu.getItems()
+                        .get(index);
+                menu.selectItem(newSelectedItem);
+                tb.setText(newSelectedItem.getText());
+                tb.setSelectionRange(lastFilter.length(), newSelectedItem
+                        .getText().length()
+                        - lastFilter.length());
+            } else if (index == -1) {
+                if (currentPage > 0) {
+                    filterOptions(currentPage - 1, lastFilter);
+                }
+            } else {
+                final MenuItem newSelectedItem = (MenuItem) menu.getItems()
+                        .get(menu.getItems().size() - 1);
+                menu.selectItem(newSelectedItem);
+                tb.setText(newSelectedItem.getText());
+                tb.setSelectionRange(lastFilter.length(), newSelectedItem
+                        .getText().length()
+                        - lastFilter.length());
+            }
+        }
+
+        public void onBrowserEvent(Event event) {
+            final Element target = DOM.eventGetTarget(event);
+            if (DOM.compare(target, up)
+                    || DOM.compare(target, DOM.getChild(up, 0))) {
+                filterOptions(currentPage - 1, lastFilter);
+            } else if (DOM.compare(target, down)
+                    || DOM.compare(target, DOM.getChild(down, 0))) {
+                filterOptions(currentPage + 1, lastFilter);
+            }
+            tb.setFocus(true);
+        }
+
+        public void setPagingEnabled(boolean paging) {
+            if (isPagingEnabled == paging) {
+                return;
+            }
+            if (paging) {
+                DOM.setStyleAttribute(down, "display", "block");
+                DOM.setStyleAttribute(up, "display", "block");
+                DOM.setStyleAttribute(status, "display", "block");
+            } else {
+                DOM.setStyleAttribute(down, "display", "none");
+                DOM.setStyleAttribute(up, "display", "none");
+                DOM.setStyleAttribute(status, "display", "none");
+            }
+            isPagingEnabled = paging;
+        }
+
+        /*
+         * (non-Javadoc)
+         * 
+         * @see com.google.gwt.user.client.ui.PopupPanel$PositionCallback#setPosition(int,
+         *      int)
+         */
+        public void setPosition(int offsetWidth, int offsetHeight) {
+
+            int top = -1;
+            int left = -1;
+
+            // reset menu size and retrieve its "natural" size
+            menu.setHeight("");
+            if (currentPage > 0) {
+                // fix height to avoid height change when getting to last page
+                menu.fixHeightTo(PAGELENTH);
+            }
+            offsetHeight = getOffsetHeight();
+
+            final int desiredWidth = IFilterSelect.this.getOffsetWidth();
+            int naturalMenuWidth = DOM.getElementPropertyInt(DOM
+                    .getFirstChild(menu.getElement()), "offsetWidth");
+            if (naturalMenuWidth < desiredWidth) {
+                menu.setWidth(desiredWidth + "px");
+                DOM.setStyleAttribute(DOM.getFirstChild(menu.getElement()),
+                        "width", "100%");
+                naturalMenuWidth = desiredWidth;
+            }
+            if (Util.isIE()) {
+                DOM.setStyleAttribute(getElement(), "width", naturalMenuWidth
+                        + "px");
+            }
+
+            if (offsetHeight + getPopupTop() > Window.getClientHeight()
+                    + Window.getScrollTop()) {
+                // popup on top of input instead
+                top = getPopupTop() - offsetHeight
+                        - IFilterSelect.this.getOffsetHeight();
+                if (top < 0) {
+                    top = 0;
+                }
+            } else {
+                top = getPopupTop();
+            }
+
+            // fetch real width (mac FF bugs here due GWT popups overflow:auto )
+            offsetWidth = DOM.getElementPropertyInt(DOM.getFirstChild(menu
+                    .getElement()), "offsetWidth");
+            if (offsetWidth + getPopupLeft() > Window.getClientWidth()
+                    + Window.getScrollLeft()) {
+                left = IFilterSelect.this.getAbsoluteLeft()
+                        + IFilterSelect.this.getOffsetWidth()
+                        + Window.getScrollLeft() - offsetWidth;
+                if (left < 0) {
+                    left = 0;
+                }
+            } else {
+                left = getPopupLeft();
+            }
+            setPopupPosition(left, top);
+
+        }
+
+        /**
+         * @return true if popup was just closed
+         */
+        public boolean isJustClosed() {
+            final long now = (new Date()).getTime();
+            return (lastAutoClosed > 0 && (now - lastAutoClosed) < 200);
+        }
+
+        public void onPopupClosed(PopupPanel sender, boolean autoClosed) {
+            if (autoClosed) {
+                lastAutoClosed = (new Date()).getTime();
+            }
+        }
+
+    }
+
+    public class SuggestionMenu extends MenuBar {
+
+        SuggestionMenu() {
+            super(true);
+            setStyleName(CLASSNAME + "-suggestmenu");
+        }
+
+        /**
+         * Fixes menus height to use same space as full page would use. Needed
+         * to avoid height changes when quickly "scrolling" to last page
+         */
+        public void fixHeightTo(int pagelenth) {
+            if (currentSuggestions.size() > 0) {
+                final int pixels = pagelenth * (getOffsetHeight() - 2)
+                        / currentSuggestions.size();
+                setHeight((pixels + 2) + "px");
+            }
+        }
+
+        public void setSuggestions(Collection suggestions) {
+            clearItems();
+            final Iterator it = suggestions.iterator();
+            while (it.hasNext()) {
+                final FilterSelectSuggestion s = (FilterSelectSuggestion) it
+                        .next();
+                final MenuItem mi = new MenuItem(s.getDisplayString(), true, s);
+                this.addItem(mi);
+                if (s == currentSuggestion) {
+                    selectItem(mi);
+                }
+            }
+        }
+
+        public void doSelectedItemAction() {
+            final MenuItem item = getSelectedItem();
+            final String enteredItemValue = tb.getText();
+            // check for exact match in menu
+            int p = getItems().size();
+            if (p > 0) {
+                for (int i = 0; i < p; i++) {
+                    final MenuItem potentialExactMatch = (MenuItem) getItems()
+                            .get(i);
+                    if (potentialExactMatch.getText().equals(enteredItemValue)) {
+                        selectItem(potentialExactMatch);
+                        doItemAction(potentialExactMatch, true);
+                        suggestionPopup.hide();
+                        return;
+                    }
+                }
+            }
+            if (allowNewItem) {
+
+                if (!enteredItemValue.equals(emptyText)) {
+                    client.updateVariable(paintableId, "newitem",
+                            enteredItemValue, immediate);
+                }
+            } else if (item != null
+                    && item.getText().toLowerCase().startsWith(
+                            lastFilter.toLowerCase())) {
+                doItemAction(item, true);
+            } else {
+                if (currentSuggestion != null) {
+                    String text = currentSuggestion.getReplacementString();
+                    tb.setText((text.equals("") ? emptyText : text));
+                    // TODO add/remove class CLASSNAME_EMPTY
+                    selectedOptionKey = currentSuggestion.key;
+                } else {
+                    tb.setText(emptyText);
+                    // TODO add class CLASSNAME_EMPTY
+                    selectedOptionKey = null;
+                }
+            }
+            suggestionPopup.hide();
+        }
+    }
+
+    public static final int FILTERINGMODE_OFF = 0;
+    public static final int FILTERINGMODE_STARTSWITH = 1;
+    public static final int FILTERINGMODE_CONTAINS = 2;
+
+    private static final String CLASSNAME = "i-filterselect";
+
+    public static final int PAGELENTH = 10;
+
+    private final FlowPanel panel = new FlowPanel();
+
+    private final TextBox tb = new TextBox() {
+        public void onBrowserEvent(Event event) {
+            super.onBrowserEvent(event);
+            if (client != null) {
+                client.handleTooltipEvent(event, IFilterSelect.this);
+            }
+        }
+    };
+
+    private final SuggestionPopup suggestionPopup = new SuggestionPopup();
+
+    private final HTML popupOpener = new HTML("");
+
+    private final Image selectedItemIcon = new Image();
+
+    private ApplicationConnection client;
+
+    private String paintableId;
+
+    private int currentPage;
+
+    private final Collection currentSuggestions = new ArrayList();
+
+    private boolean immediate;
+
+    private String selectedOptionKey;
+
+    private boolean filtering = false;
+
+    private String lastFilter = "";
+
+    private FilterSelectSuggestion currentSuggestion;
+
+    private int totalMatches;
+    private boolean allowNewItem;
+    private boolean nullSelectionAllowed;
+    private boolean enabled;
+
+    // shown in unfocused empty field, disappears on focus (e.g "Search here")
+    private String emptyText = "";
+    private static final String CLASSNAME_EMPTY = "empty";
+    private static final String ATTR_EMPTYTEXT = "emptytext";
+
+    public IFilterSelect() {
+        selectedItemIcon.setVisible(false);
+        panel.add(selectedItemIcon);
+        tb.sinkEvents(Tooltip.TOOLTIP_EVENTS);
+        panel.add(tb);
+        panel.add(popupOpener);
+        initWidget(panel);
+        setStyleName(CLASSNAME);
+        tb.addKeyboardListener(this);
+        tb.setStyleName(CLASSNAME + "-input");
+        tb.addFocusListener(this);
+        popupOpener.setStyleName(CLASSNAME + "-button");
+        popupOpener.addClickListener(this);
+    }
+
+    public boolean hasNextPage() {
+        if (totalMatches > (currentPage + 1) * PAGELENTH) {
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    public void filterOptions(int page) {
+        filterOptions(page, tb.getText());
+    }
+
+    public void filterOptions(int page, String filter) {
+        if (filter.equals(lastFilter) && currentPage == page) {
+            if (!suggestionPopup.isAttached()) {
+                suggestionPopup.showSuggestions(currentSuggestions,
+                        currentPage, totalMatches);
+            }
+            return;
+        }
+        if (!filter.equals(lastFilter)) {
+            // we are on subsequent page and text has changed -> reset page
+            if ("".equals(filter)) {
+                // let server decide
+                page = -1;
+            } else {
+                page = 0;
+            }
+        }
+
+        filtering = true;
+        client.updateVariable(paintableId, "filter", filter, false);
+        client.updateVariable(paintableId, "page", page, true);
+        lastFilter = filter;
+        currentPage = page;
+    }
+
+    public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+        paintableId = uidl.getId();
+        this.client = client;
+
+        boolean readonly = uidl.hasAttribute("readonly");
+        boolean disabled = uidl.hasAttribute("disabled");
+
+        if (disabled || readonly) {
+            tb.setEnabled(false);
+            enabled = false;
+        } else {
+            tb.setEnabled(true);
+            enabled = true;
+        }
+
+        if (client.updateComponent(this, uidl, true)) {
+            return;
+        }
+
+        immediate = uidl.hasAttribute("immediate");
+
+        nullSelectionAllowed = uidl.hasAttribute("nullselect");
+
+        currentPage = uidl.getIntVariable("page");
+
+        if (uidl.hasAttribute(ATTR_EMPTYTEXT)) {
+            // "emptytext" changed from server
+            emptyText = uidl.getStringAttribute(ATTR_EMPTYTEXT);
+        }
+
+        suggestionPopup.setPagingEnabled(true);
+
+        allowNewItem = uidl.hasAttribute("allownewitem");
+
+        currentSuggestions.clear();
+        final UIDL options = uidl.getChildUIDL(0);
+        totalMatches = uidl.getIntAttribute("totalMatches");
+
+        String captions = emptyText;
+
+        for (final Iterator i = options.getChildIterator(); i.hasNext();) {
+            final UIDL optionUidl = (UIDL) i.next();
+            final FilterSelectSuggestion suggestion = new FilterSelectSuggestion(
+                    optionUidl);
+            currentSuggestions.add(suggestion);
+            if (optionUidl.hasAttribute("selected")) {
+                if (!filtering) {
+                    tb.setText(suggestion.getReplacementString());
+                }
+                currentSuggestion = suggestion;
+            }
+
+            // Collect captions so we can calculate minimum width for textarea
+            if (captions.length() > 0) {
+                captions += "|";
+            }
+            captions += suggestion.getReplacementString();
+        }
+
+        if (!filtering && uidl.hasVariable("selected")
+                && uidl.getStringArrayVariable("selected").length == 0) {
+            // select nulled
+            tb.setText(emptyText);
+            selectedOptionKey = null;
+            // TODO add class CLASSNAME_EMPTY
+        }
+
+        if (filtering
+                && lastFilter.toLowerCase().equals(
+                        uidl.getStringVariable("filter"))) {
+            suggestionPopup.showSuggestions(currentSuggestions, currentPage,
+                    totalMatches);
+            filtering = false;
+        }
+
+        // Calculate minumum textarea width
+        final int minw = minWidth(captions);
+        final Element spacer = DOM.createDiv();
+        DOM.setStyleAttribute(spacer, "width", minw + "px");
+        DOM.setStyleAttribute(spacer, "height", "0");
+        DOM.setStyleAttribute(spacer, "overflow", "hidden");
+        DOM.appendChild(panel.getElement(), spacer);
+
+    }
+
+    public void onSuggestionSelected(FilterSelectSuggestion suggestion) {
+        currentSuggestion = suggestion;
+        String newKey;
+        if (suggestion.key.equals("")) {
+            // "nullselection"
+            newKey = "";
+        } else {
+            // normal selection
+            newKey = String.valueOf(suggestion.getOptionKey());
+        }
+        String text = suggestion.getReplacementString();
+        tb.setText(text.equals("") ? emptyText : text);
+        // TODO add/remove class CLASSNAME_EMPTY
+        setSelectedItemIcon(suggestion.getIconUri());
+        if (!newKey.equals(selectedOptionKey)) {
+            selectedOptionKey = newKey;
+            client.updateVariable(paintableId, "selected",
+                    new String[] { selectedOptionKey }, immediate);
+            // currentPage = -1; // forget the page
+        }
+        suggestionPopup.hide();
+    }
+
+    private void setSelectedItemIcon(String iconUri) {
+        if (iconUri == null) {
+            selectedItemIcon.setVisible(false);
+        } else {
+            selectedItemIcon.setUrl(iconUri);
+            selectedItemIcon.setVisible(true);
+        }
+    }
+
+    public void onKeyDown(Widget sender, char keyCode, int modifiers) {
+        if (enabled && suggestionPopup.isAttached()) {
+            switch (keyCode) {
+            case KeyboardListener.KEY_DOWN:
+                suggestionPopup.selectNextItem();
+                DOM.eventPreventDefault(DOM.eventGetCurrentEvent());
+                break;
+            case KeyboardListener.KEY_UP:
+                suggestionPopup.selectPrevItem();
+                DOM.eventPreventDefault(DOM.eventGetCurrentEvent());
+                break;
+            case KeyboardListener.KEY_PAGEDOWN:
+                if (hasNextPage()) {
+                    filterOptions(currentPage + 1, lastFilter);
+                }
+                break;
+            case KeyboardListener.KEY_PAGEUP:
+                if (currentPage > 0) {
+                    filterOptions(currentPage - 1, lastFilter);
+                }
+                break;
+            case KeyboardListener.KEY_ENTER:
+            case KeyboardListener.KEY_TAB:
+                suggestionPopup.menu.doSelectedItemAction();
+                break;
+            }
+        }
+    }
+
+    public void onKeyPress(Widget sender, char keyCode, int modifiers) {
+
+    }
+
+    public void onKeyUp(Widget sender, char keyCode, int modifiers) {
+        if (enabled) {
+            switch (keyCode) {
+            case KeyboardListener.KEY_ENTER:
+            case KeyboardListener.KEY_TAB:
+                ; // NOP
+                break;
+            case KeyboardListener.KEY_DOWN:
+            case KeyboardListener.KEY_UP:
+            case KeyboardListener.KEY_PAGEDOWN:
+            case KeyboardListener.KEY_PAGEUP:
+                if (suggestionPopup.isAttached()) {
+                    break;
+                } else {
+                    // open popup as from gadget
+                    filterOptions(-1, "");
+                    lastFilter = "";
+                    tb.selectAll();
+                    break;
+                }
+            default:
+                filterOptions(currentPage);
+                break;
+            }
+        }
+    }
+
+    /**
+     * Listener for popupopener
+     */
+    public void onClick(Widget sender) {
+        if (enabled) {
+            // ask suggestionPopup if it was just closed, we are using GWT
+            // Popup's
+            // auto close feature
+            if (!suggestionPopup.isJustClosed()) {
+                filterOptions(-1, "");
+                lastFilter = "";
+            }
+            DOM.eventPreventDefault(DOM.eventGetCurrentEvent());
+            tb.setFocus(true);
+            tb.selectAll();
+
+        }
+    }
+
+    /*
+     * Calculate minumum width for FilterSelect textarea
+     */
+    private native int minWidth(String captions)
+    /*-{
+        if(!captions || captions.length <= 0)
+               return 0;
+        captions = captions.split("|");
+        var d = $wnd.document.createElement("div");
+        var html = "";
+        for(var i=0; i < captions.length; i++) {
+               html += "<div>" + captions[i] + "</div>";
+               // TODO apply same CSS classname as in suggestionmenu
+        }
+        d.style.position = "absolute";
+        d.style.top = "0";
+        d.style.left = "0";
+        d.style.visibility = "hidden";
+        d.innerHTML = html;
+        $wnd.document.body.appendChild(d);
+        var w = d.offsetWidth;
+        $wnd.document.body.removeChild(d);
+        return w;
+    }-*/;
+
+    public void onFocus(Widget sender) {
+        if (emptyText.equals(tb.getText())) {
+            tb.setText("");
+            // TODO remove class CLASSNAME_EMPTY
+        }
+    }
+
+    public void onLostFocus(Widget sender) {
+        if (suggestionPopup.isJustClosed()) {
+            suggestionPopup.menu.doSelectedItemAction();
+        }
+        if ("".equals(tb.getText())) {
+            tb.setText(emptyText);
+            // TODO add CLASSNAME_EMPTY
+        }
+    }
+
+    public void focus() {
+        tb.setFocus(true);
+    }
+}
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/IForm.java b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/IForm.java
new file mode 100644 (file)
index 0000000..14ab4d3
--- /dev/null
@@ -0,0 +1,149 @@
+/* \r
+@ITMillApache2LicenseForJavaFiles@\r
+ */\r
+\r
+package com.itmill.toolkit.terminal.gwt.client.ui;\r
+\r
+import com.google.gwt.user.client.DOM;\r
+import com.google.gwt.user.client.Element;\r
+import com.google.gwt.user.client.ui.ComplexPanel;\r
+import com.google.gwt.user.client.ui.Widget;\r
+import com.itmill.toolkit.terminal.gwt.client.ApplicationConnection;\r
+import com.itmill.toolkit.terminal.gwt.client.Container;\r
+import com.itmill.toolkit.terminal.gwt.client.ContainerResizedListener;\r
+import com.itmill.toolkit.terminal.gwt.client.ErrorMessage;\r
+import com.itmill.toolkit.terminal.gwt.client.Paintable;\r
+import com.itmill.toolkit.terminal.gwt.client.UIDL;\r
+import com.itmill.toolkit.terminal.gwt.client.Util;\r
+\r
+public class IForm extends ComplexPanel implements Paintable,\r
+        ContainerResizedListener {\r
+\r
+    public static final String CLASSNAME = "i-form";\r
+\r
+    private Container lo;\r
+    private Element legend = DOM.createLegend();\r
+    private Element caption = DOM.createSpan();\r
+    private Element errorIndicatorElement = DOM.createDiv();\r
+    private Element desc = DOM.createDiv();\r
+    private Icon icon;\r
+    private ErrorMessage errorMessage = new ErrorMessage();\r
+\r
+    private Element fieldContainer = DOM.createDiv();\r
+\r
+    private Element footerContainer = DOM.createDiv();\r
+\r
+    private Container footer;\r
+\r
+    public IForm() {\r
+        setElement(DOM.createFieldSet());\r
+        setStyleName(CLASSNAME);\r
+        DOM.appendChild(getElement(), legend);\r
+        DOM.appendChild(legend, caption);\r
+        DOM.setElementProperty(errorIndicatorElement, "className",\r
+                "i-errorindicator");\r
+        DOM.setStyleAttribute(errorIndicatorElement, "display", "none");\r
+        DOM.setInnerText(errorIndicatorElement, " "); // needed for IE\r
+        DOM.setElementProperty(desc, "className", "i-form-description");\r
+        DOM.appendChild(getElement(), desc);\r
+        DOM.appendChild(getElement(), fieldContainer);\r
+        errorMessage.setVisible(false);\r
+        errorMessage.setStyleName(CLASSNAME + "-errormessage");\r
+        DOM.appendChild(getElement(), errorMessage.getElement());\r
+        DOM.appendChild(getElement(), footerContainer);\r
+    }\r
+\r
+    public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {\r
+        if (client.updateComponent(this, uidl, false)) {\r
+            return;\r
+        }\r
+\r
+        boolean legendEmpty = true;\r
+        if (uidl.hasAttribute("caption")) {\r
+            DOM.setInnerText(caption, uidl.getStringAttribute("caption"));\r
+            legendEmpty = false;\r
+        } else {\r
+            DOM.setInnerText(caption, "");\r
+        }\r
+        if (uidl.hasAttribute("icon")) {\r
+            if (icon == null) {\r
+                icon = new Icon(client);\r
+                DOM.insertChild(legend, icon.getElement(), 0);\r
+            }\r
+            icon.setUri(uidl.getStringAttribute("icon"));\r
+            legendEmpty = false;\r
+        } else {\r
+            if (icon != null) {\r
+                DOM.removeChild(legend, icon.getElement());\r
+            }\r
+        }\r
+        if (legendEmpty) {\r
+            DOM.setStyleAttribute(legend, "display", "none");\r
+        } else {\r
+            DOM.setStyleAttribute(legend, "display", "");\r
+        }\r
+\r
+        if (uidl.hasAttribute("error")) {\r
+            final UIDL errorUidl = uidl.getErrors();\r
+            errorMessage.updateFromUIDL(errorUidl);\r
+            errorMessage.setVisible(true);\r
+\r
+        } else {\r
+            errorMessage.setVisible(false);\r
+        }\r
+\r
+        if (uidl.hasAttribute("description")) {\r
+            DOM.setInnerHTML(desc, uidl.getStringAttribute("description"));\r
+        } else {\r
+            DOM.setInnerHTML(desc, "");\r
+        }\r
+\r
+        iLayout();\r
+\r
+        final UIDL layoutUidl = uidl.getChildUIDL(0);\r
+        Container newLo = (Container) client.getPaintable(layoutUidl);\r
+        if (lo == null) {\r
+            lo = newLo;\r
+            add((Widget) lo, fieldContainer);\r
+        } else if (lo != newLo) {\r
+            client.unregisterPaintable(lo);\r
+            remove((Widget) lo);\r
+            lo = newLo;\r
+            add((Widget) lo, fieldContainer);\r
+        }\r
+        lo.updateFromUIDL(layoutUidl, client);\r
+\r
+        if (uidl.getChildCount() > 1) {\r
+            // render footer\r
+            Container newFooter = (Container) client.getPaintable(uidl\r
+                    .getChildUIDL(1));\r
+            if (footer == null) {\r
+                add((Widget) newFooter, footerContainer);\r
+                footer = newFooter;\r
+            } else if (newFooter != footer) {\r
+                remove((Widget) footer);\r
+                client.unregisterPaintable(footer);\r
+                add((Widget) newFooter, footerContainer);\r
+            }\r
+            footer = newFooter;\r
+            footer.updateFromUIDL(uidl.getChildUIDL(1), client);\r
+        } else {\r
+            if (footer != null) {\r
+                remove((Widget) footer);\r
+                client.unregisterPaintable(footer);\r
+            }\r
+        }\r
+    }\r
+\r
+    public void iLayout() {\r
+        // fix contained components container size as they may have relative\r
+        // widths\r
+        DOM.setStyleAttribute(fieldContainer, "width", "");\r
+        DOM.setStyleAttribute(footerContainer, "width", "");\r
+        int width = DOM.getElementPropertyInt(desc, "offsetWidth");\r
+        DOM.setStyleAttribute(fieldContainer, "width", width + "px");\r
+        DOM.setStyleAttribute(footerContainer, "width", width + "px");\r
+        Util.runDescendentsLayout(this);\r
+    }\r
+\r
+}\r
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/IFormLayout.java b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/IFormLayout.java
new file mode 100644 (file)
index 0000000..a476e26
--- /dev/null
@@ -0,0 +1,299 @@
+/* 
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.itmill.toolkit.terminal.gwt.client.ui;
+
+import java.util.HashMap;
+import java.util.Iterator;
+
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.Element;
+import com.google.gwt.user.client.Event;
+import com.google.gwt.user.client.ui.FlexTable;
+import com.google.gwt.user.client.ui.HTML;
+import com.google.gwt.user.client.ui.Widget;
+import com.itmill.toolkit.terminal.gwt.client.ApplicationConnection;
+import com.itmill.toolkit.terminal.gwt.client.Container;
+import com.itmill.toolkit.terminal.gwt.client.Paintable;
+import com.itmill.toolkit.terminal.gwt.client.StyleConstants;
+import com.itmill.toolkit.terminal.gwt.client.UIDL;
+import com.itmill.toolkit.terminal.gwt.client.Util;
+
+/**
+ * Two col Layout that places caption on left col and field on right col
+ */
+public class IFormLayout extends FlexTable implements Container {
+
+    private final static String CLASSNAME = "i-formlayout";
+
+    HashMap componentToCaption = new HashMap();
+    private ApplicationConnection client;
+    private HashMap componentToError = new HashMap();
+
+    public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+        this.client = client;
+
+        if (client.updateComponent(this, uidl, false)) {
+            return;
+        }
+
+        final MarginInfo margins = new MarginInfo(uidl
+                .getIntAttribute("margins"));
+
+        Element margin = getElement();
+        setStyleName(margin, CLASSNAME + "-" + StyleConstants.MARGIN_TOP,
+                margins.hasTop());
+        setStyleName(margin, CLASSNAME + "-" + StyleConstants.MARGIN_RIGHT,
+                margins.hasRight());
+        setStyleName(margin, CLASSNAME + "-" + StyleConstants.MARGIN_BOTTOM,
+                margins.hasBottom());
+        setStyleName(margin, CLASSNAME + "-" + StyleConstants.MARGIN_LEFT,
+                margins.hasLeft());
+
+        setStyleName(margin, CLASSNAME + "-" + "spacing", uidl
+                .hasAttribute("spacing"));
+
+        int i = 0;
+        for (final Iterator it = uidl.getChildIterator(); it.hasNext(); i++) {
+            prepareCell(i, 1);
+            final UIDL childUidl = (UIDL) it.next();
+            final Paintable p = client.getPaintable(childUidl);
+            Caption caption = (Caption) componentToCaption.get(p);
+            if (caption == null) {
+                caption = new Caption(p, client);
+                componentToCaption.put(p, caption);
+            }
+            ErrorFlag error = (ErrorFlag) componentToError.get(p);
+            if (error == null) {
+                error = new ErrorFlag();
+                componentToError.put(p, error);
+            }
+            prepareCell(i, 2);
+            final Paintable oldComponent = (Paintable) getWidget(i, 2);
+            if (oldComponent == null) {
+                setWidget(i, 2, (Widget) p);
+            } else if (oldComponent != p) {
+                client.unregisterPaintable(oldComponent);
+                setWidget(i, 2, (Widget) p);
+            }
+            getCellFormatter().setStyleName(i, 2, CLASSNAME + "-contentcell");
+            getCellFormatter().setStyleName(i, 0, CLASSNAME + "-captioncell");
+            setWidget(i, 0, caption);
+
+            getCellFormatter().setStyleName(i, 1, CLASSNAME + "-errorcell");
+            setWidget(i, 1, error);
+
+            p.updateFromUIDL(childUidl, client);
+
+            String rowstyles = CLASSNAME + "-row";
+            if (i == 0) {
+                rowstyles += " " + CLASSNAME + "-firstrow";
+            }
+            if (!it.hasNext()) {
+                rowstyles += " " + CLASSNAME + "-lastrow";
+            }
+
+            getRowFormatter().setStyleName(i, rowstyles);
+
+        }
+
+        while (getRowCount() > i) {
+            final Paintable p = (Paintable) getWidget(i, 2);
+            client.unregisterPaintable(p);
+            componentToCaption.remove(p);
+            removeRow(i);
+        }
+    }
+
+    public boolean hasChildComponent(Widget component) {
+        return componentToCaption.containsKey(component);
+    }
+
+    public void replaceChildComponent(Widget oldComponent, Widget newComponent) {
+        int i;
+        for (i = 0; i < getRowCount(); i++) {
+            if (oldComponent == getWidget(i, 1)) {
+                final Caption newCap = new Caption((Paintable) newComponent,
+                        client);
+                setWidget(i, 0, newCap);
+                setWidget(i, 1, newComponent);
+                client.unregisterPaintable((Paintable) oldComponent);
+                break;
+            }
+        }
+    }
+
+    public void updateCaption(Paintable component, UIDL uidl) {
+        final Caption c = (Caption) componentToCaption.get(component);
+        if (c != null) {
+            c.updateCaption(uidl);
+        }
+        final ErrorFlag e = (ErrorFlag) componentToError.get(component);
+        if (e != null) {
+            e.updateFromUIDL(uidl, component);
+        }
+    }
+
+    public class Caption extends HTML {
+
+        public static final String CLASSNAME = "i-caption";
+
+        private final Paintable owner;
+
+        private Element requiredFieldIndicator;
+
+        private Icon icon;
+
+        private Element captionText;
+
+        private final ApplicationConnection client;
+
+        /**
+         * 
+         * @param component
+         *                optional owner of caption. If not set, getOwner will
+         *                return null
+         * @param client
+         */
+        public Caption(Paintable component, ApplicationConnection client) {
+            super();
+            this.client = client;
+            owner = component;
+            setStyleName(CLASSNAME);
+        }
+
+        public void updateCaption(UIDL uidl) {
+            setVisible(!uidl.getBooleanAttribute("invisible"));
+
+            setStyleName(getElement(), "i-disabled", uidl
+                    .hasAttribute("disabled"));
+
+            boolean isEmpty = true;
+
+            if (uidl.hasAttribute("icon")) {
+                if (icon == null) {
+                    icon = new Icon(client);
+
+                    DOM.insertChild(getElement(), icon.getElement(), 0);
+                }
+                icon.setUri(uidl.getStringAttribute("icon"));
+                isEmpty = false;
+            } else {
+                if (icon != null) {
+                    DOM.removeChild(getElement(), icon.getElement());
+                    icon = null;
+                }
+
+            }
+
+            if (uidl.hasAttribute("caption")) {
+                if (captionText == null) {
+                    captionText = DOM.createSpan();
+                    DOM.insertChild(getElement(), captionText, icon == null ? 0
+                            : 1);
+                }
+                String c = uidl.getStringAttribute("caption");
+                if (c == null) {
+                    c = "";
+                } else {
+                    isEmpty = false;
+                }
+                DOM.setInnerText(captionText, c);
+            } else {
+                // TODO should span also be removed
+            }
+
+            if (uidl.hasAttribute("description")) {
+                if (captionText != null) {
+                    addStyleDependentName("hasdescription");
+                } else {
+                    removeStyleDependentName("hasdescription");
+                }
+            }
+
+            if (uidl.getBooleanAttribute("required")) {
+                if (requiredFieldIndicator == null) {
+                    requiredFieldIndicator = DOM.createSpan();
+                    DOM.setInnerText(requiredFieldIndicator, "*");
+                    DOM.setElementProperty(requiredFieldIndicator, "className",
+                            "i-required-field-indicator");
+                    DOM.appendChild(getElement(), requiredFieldIndicator);
+                }
+            } else {
+                if (requiredFieldIndicator != null) {
+                    DOM.removeChild(getElement(), requiredFieldIndicator);
+                    requiredFieldIndicator = null;
+                }
+            }
+
+            // Workaround for IE weirdness, sometimes returns bad height in some
+            // circumstances when Caption is empty. See #1444
+            // IE7 bugs more often. I wonder what happens when IE8 arrives...
+            if (Util.isIE()) {
+                if (isEmpty) {
+                    setHeight("0px");
+                    DOM.setStyleAttribute(getElement(), "overflow", "hidden");
+                } else {
+                    setHeight("");
+                    DOM.setStyleAttribute(getElement(), "overflow", "");
+                }
+
+            }
+
+        }
+
+        /**
+         * Returns Paintable for which this Caption belongs to.
+         * 
+         * @return owner Widget
+         */
+        public Paintable getOwner() {
+            return owner;
+        }
+
+        public void onBrowserEvent(Event event) {
+            super.onBrowserEvent(event);
+            if (client != null) {
+                client.handleTooltipEvent(event, owner);
+            }
+        }
+    }
+
+    private class ErrorFlag extends HTML {
+        private static final String CLASSNAME = ".i-form-layout-error-indicator";
+        Element errorIndicatorElement;
+        private Paintable owner;
+
+        public ErrorFlag() {
+            setStyleName(CLASSNAME);
+        }
+
+        public void updateFromUIDL(UIDL uidl, Paintable component) {
+            owner = component;
+            if (uidl.hasAttribute("error")) {
+                if (errorIndicatorElement == null) {
+                    errorIndicatorElement = DOM.createDiv();
+                    if (Util.isIE()) {
+                        DOM.setInnerHTML(errorIndicatorElement, "&nbsp;");
+                    }
+                    DOM.setElementProperty(errorIndicatorElement, "className",
+                            "i-errorindicator");
+                    DOM.appendChild(getElement(), errorIndicatorElement);
+                }
+
+            } else if (errorIndicatorElement != null) {
+                DOM.removeChild(getElement(), errorIndicatorElement);
+                errorIndicatorElement = null;
+            }
+        }
+
+        public void onBrowserEvent(Event event) {
+            super.onBrowserEvent(event);
+            if (owner != null) {
+                client.handleTooltipEvent(event, owner);
+            }
+        }
+
+    }
+}
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/IGridLayout.java b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/IGridLayout.java
new file mode 100644 (file)
index 0000000..68bb539
--- /dev/null
@@ -0,0 +1,296 @@
+/* 
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.itmill.toolkit.terminal.gwt.client.ui;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+
+import com.google.gwt.user.client.Command;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.DeferredCommand;
+import com.google.gwt.user.client.Element;
+import com.google.gwt.user.client.ui.FlexTable;
+import com.google.gwt.user.client.ui.HasHorizontalAlignment;
+import com.google.gwt.user.client.ui.HasVerticalAlignment;
+import com.google.gwt.user.client.ui.SimplePanel;
+import com.google.gwt.user.client.ui.Widget;
+import com.google.gwt.user.client.ui.HasHorizontalAlignment.HorizontalAlignmentConstant;
+import com.google.gwt.user.client.ui.HasVerticalAlignment.VerticalAlignmentConstant;
+import com.itmill.toolkit.terminal.gwt.client.ApplicationConnection;
+import com.itmill.toolkit.terminal.gwt.client.BrowserInfo;
+import com.itmill.toolkit.terminal.gwt.client.CaptionWrapper;
+import com.itmill.toolkit.terminal.gwt.client.Container;
+import com.itmill.toolkit.terminal.gwt.client.ContainerResizedListener;
+import com.itmill.toolkit.terminal.gwt.client.Paintable;
+import com.itmill.toolkit.terminal.gwt.client.StyleConstants;
+import com.itmill.toolkit.terminal.gwt.client.UIDL;
+import com.itmill.toolkit.terminal.gwt.client.Util;
+
+public class IGridLayout extends SimplePanel implements Paintable, Container,
+        ContainerResizedListener {
+
+    public static final String CLASSNAME = "i-gridlayout";
+
+    private Grid grid = new Grid();
+
+    private boolean needsLayout = false;
+
+    private boolean needsFF2Hack = BrowserInfo.get().isFF2();
+
+    private Element margin = DOM.createDiv();
+
+    private Element meterElement;
+
+    private String width;
+
+    public IGridLayout() {
+        super();
+        DOM.appendChild(getElement(), margin);
+        DOM.setStyleAttribute(getElement(), "overflow", "hidden");
+        setStyleName(CLASSNAME);
+        setWidget(grid);
+    }
+
+    protected Element getContainerElement() {
+        return margin;
+    }
+
+    public void setWidth(String width) {
+        this.width = width;
+        if (width != null && !width.equals("")) {
+            needsLayout = true;
+        } else {
+            needsLayout = false;
+            grid.setWidth("");
+        }
+    }
+
+    public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+
+        if (client.updateComponent(this, uidl, false)) {
+            return;
+        }
+        final MarginInfo margins = new MarginInfo(uidl
+                .getIntAttribute("margins"));
+
+        setStyleName(margin, CLASSNAME + "-" + StyleConstants.MARGIN_TOP,
+                margins.hasTop());
+        setStyleName(margin, CLASSNAME + "-" + StyleConstants.MARGIN_RIGHT,
+                margins.hasRight());
+        setStyleName(margin, CLASSNAME + "-" + StyleConstants.MARGIN_BOTTOM,
+                margins.hasBottom());
+        setStyleName(margin, CLASSNAME + "-" + StyleConstants.MARGIN_LEFT,
+                margins.hasLeft());
+
+        setStyleName(margin, CLASSNAME + "-" + "spacing", uidl
+                .hasAttribute("spacing"));
+        iLayout();
+        grid.updateFromUIDL(uidl, client);
+    }
+
+    public boolean hasChildComponent(Widget component) {
+        return grid.hasChildComponent(component);
+    }
+
+    public void replaceChildComponent(Widget oldComponent, Widget newComponent) {
+        grid.replaceChildComponent(oldComponent, newComponent);
+    }
+
+    public void updateCaption(Paintable component, UIDL uidl) {
+        grid.updateCaption(component, uidl);
+    }
+
+    public class Grid extends FlexTable implements Paintable, Container {
+
+        /** Widget to captionwrapper map */
+        private final HashMap widgetToCaptionWrapper = new HashMap();
+
+        public Grid() {
+            super();
+            setStyleName(CLASSNAME + "-grid");
+        }
+
+        public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+
+            int row = 0, column = 0;
+
+            final ArrayList oldWidgetWrappers = new ArrayList();
+            for (final Iterator iterator = iterator(); iterator.hasNext();) {
+                oldWidgetWrappers.add(iterator.next());
+            }
+            clear();
+
+            final int[] alignments = uidl.getIntArrayAttribute("alignments");
+            int alignmentIndex = 0;
+
+            for (final Iterator i = uidl.getChildIterator(); i.hasNext();) {
+                final UIDL r = (UIDL) i.next();
+                if ("gr".equals(r.getTag())) {
+                    column = 0;
+                    for (final Iterator j = r.getChildIterator(); j.hasNext();) {
+                        final UIDL c = (UIDL) j.next();
+                        if ("gc".equals(c.getTag())) {
+                            prepareCell(row, column);
+
+                            // Set cell width
+                            int w;
+                            if (c.hasAttribute("w")) {
+                                w = c.getIntAttribute("w");
+                            } else {
+                                w = 1;
+                            }
+
+                            FlexCellFormatter formatter = (FlexCellFormatter) getCellFormatter();
+
+                            // set col span
+                            formatter.setColSpan(row, column, w);
+
+                            String styleNames = CLASSNAME + "-cell";
+                            if (column == 0) {
+                                styleNames += " " + CLASSNAME + "-firstcol";
+                            }
+                            if (row == 0) {
+                                styleNames += " " + CLASSNAME + "-firstrow";
+                            }
+                            formatter.setStyleName(row, column, styleNames);
+
+                            // Set cell height
+                            int h;
+                            if (c.hasAttribute("h")) {
+                                h = c.getIntAttribute("h");
+                            } else {
+                                h = 1;
+                            }
+                            ((FlexCellFormatter) getCellFormatter())
+                                    .setRowSpan(row, column, h);
+
+                            final UIDL u = c.getChildUIDL(0);
+                            if (u != null) {
+
+                                AlignmentInfo alignmentInfo = new AlignmentInfo(
+                                        alignments[alignmentIndex++]);
+
+                                VerticalAlignmentConstant va;
+                                if (alignmentInfo.isBottom()) {
+                                    va = HasVerticalAlignment.ALIGN_BOTTOM;
+                                } else if (alignmentInfo.isTop()) {
+                                    va = HasVerticalAlignment.ALIGN_TOP;
+                                } else {
+                                    va = HasVerticalAlignment.ALIGN_MIDDLE;
+                                }
+
+                                HorizontalAlignmentConstant ha;
+
+                                if (alignmentInfo.isLeft()) {
+                                    ha = HasHorizontalAlignment.ALIGN_LEFT;
+                                } else if (alignmentInfo.isHorizontalCenter()) {
+                                    ha = HasHorizontalAlignment.ALIGN_CENTER;
+                                } else {
+                                    ha = HasHorizontalAlignment.ALIGN_RIGHT;
+                                }
+
+                                formatter.setAlignment(row, column, ha, va);
+
+                                final Paintable child = client.getPaintable(u);
+                                CaptionWrapper wr;
+                                if (widgetToCaptionWrapper.containsKey(child)) {
+                                    wr = (CaptionWrapper) widgetToCaptionWrapper
+                                            .get(child);
+                                    oldWidgetWrappers.remove(wr);
+                                } else {
+                                    wr = new CaptionWrapper(child, client);
+                                    widgetToCaptionWrapper.put(child, wr);
+                                }
+
+                                setWidget(row, column, wr);
+
+                                DOM.setStyleAttribute(wr.getElement(),
+                                        "textAlign", alignmentInfo
+                                                .getHorizontalAlignment());
+
+                                if (!u.getBooleanAttribute("cached")) {
+                                    child.updateFromUIDL(u, client);
+                                }
+                            }
+                            column += w;
+                        }
+                    }
+                    row++;
+                }
+            }
+
+            // loop oldWidgetWrappers that where not re-attached and unregister
+            // them
+            for (final Iterator it = oldWidgetWrappers.iterator(); it.hasNext();) {
+                final CaptionWrapper w = (CaptionWrapper) it.next();
+                client.unregisterPaintable(w.getPaintable());
+                widgetToCaptionWrapper.remove(w.getPaintable());
+            }
+            // fix rendering bug on FF2 (#1838)
+            if (needsFF2Hack) {
+                DeferredCommand.addCommand(new Command() {
+                    public void execute() {
+                        Element firstcell = getCellFormatter().getElement(0, 0);
+                        if (firstcell != null) {
+                            String styleAttribute = DOM.getStyleAttribute(
+                                    firstcell, "verticalAlign");
+                            DOM.setStyleAttribute(firstcell, "verticalAlign",
+                                    "");
+                            int elementPropertyInt = DOM.getElementPropertyInt(
+                                    firstcell, "offsetWidth");
+                            DOM.setStyleAttribute(firstcell, "verticalAlign",
+                                    styleAttribute);
+                            if (elementPropertyInt > 0) {
+                                needsFF2Hack = false;
+                            }
+                        }
+                    }
+                });
+            }
+        }
+
+        public boolean hasChildComponent(Widget component) {
+            if (widgetToCaptionWrapper.containsKey(component)) {
+                return true;
+            }
+            return false;
+        }
+
+        public void replaceChildComponent(Widget oldComponent,
+                Widget newComponent) {
+            // TODO Auto-generated method stub
+
+        }
+
+        public void updateCaption(Paintable component, UIDL uidl) {
+            final CaptionWrapper wrapper = (CaptionWrapper) widgetToCaptionWrapper
+                    .get(component);
+            wrapper.updateCaption(uidl);
+        }
+
+    }
+
+    public void iLayout() {
+        if (needsLayout) {
+            super.setWidth(width);
+            if (meterElement == null) {
+                meterElement = DOM.createDiv();
+                DOM.setStyleAttribute(meterElement, "overflow", "hidden");
+                DOM.setStyleAttribute(meterElement, "height", "0");
+                DOM.appendChild(getContainerElement(), meterElement);
+            }
+            int contentWidth = DOM.getElementPropertyInt(meterElement,
+                    "offsetWidth");
+            int offsetWidth = getOffsetWidth();
+
+            grid.setWidth((offsetWidth - (offsetWidth - contentWidth)) + "px");
+        } else {
+            grid.setWidth("");
+        }
+        Util.runDescendentsLayout(this);
+    }
+
+}
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/IHorizontalExpandLayout.java b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/IHorizontalExpandLayout.java
new file mode 100644 (file)
index 0000000..80ea9b9
--- /dev/null
@@ -0,0 +1,13 @@
+/* 
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.itmill.toolkit.terminal.gwt.client.ui;
+
+public class IHorizontalExpandLayout extends IExpandLayout {
+
+    public IHorizontalExpandLayout() {
+        super(IExpandLayout.ORIENTATION_HORIZONTAL);
+    }
+
+}
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/ILabel.java b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/ILabel.java
new file mode 100644 (file)
index 0000000..15e1eab
--- /dev/null
@@ -0,0 +1,49 @@
+/* 
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.itmill.toolkit.terminal.gwt.client.ui;
+
+import com.google.gwt.user.client.ui.HTML;
+import com.itmill.toolkit.terminal.gwt.client.ApplicationConnection;
+import com.itmill.toolkit.terminal.gwt.client.Paintable;
+import com.itmill.toolkit.terminal.gwt.client.UIDL;
+
+public class ILabel extends HTML implements Paintable {
+
+    public static final String CLASSNAME = "i-label";
+
+    public ILabel() {
+        super();
+        setStyleName(CLASSNAME);
+    }
+
+    public ILabel(String text) {
+        super(text);
+        setStyleName(CLASSNAME);
+    }
+
+    public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+
+        if (client.updateComponent(this, uidl, true)) {
+            return;
+        }
+
+        final String mode = uidl.getStringAttribute("mode");
+        if (mode == null || "text".equals(mode)) {
+            setText(uidl.getChildString(0));
+        } else if ("pre".equals(mode)) {
+            setHTML(uidl.getChildrenAsXML());
+        } else if ("uidl".equals(mode)) {
+            setHTML(uidl.getChildrenAsXML());
+        } else if ("xhtml".equals(mode)) {
+            setHTML(uidl.getChildUIDL(0).getChildUIDL(0).getChildString(0));
+        } else if ("xml".equals(mode)) {
+            setHTML(uidl.getChildUIDL(0).getChildString(0));
+        } else if ("raw".equals(mode)) {
+            setHTML(uidl.getChildUIDL(0).getChildString(0));
+        } else {
+            setText("");
+        }
+    }
+}
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/ILink.java b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/ILink.java
new file mode 100644 (file)
index 0000000..e7977c9
--- /dev/null
@@ -0,0 +1,193 @@
+/* 
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.itmill.toolkit.terminal.gwt.client.ui;
+
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.Element;
+import com.google.gwt.user.client.Event;
+import com.google.gwt.user.client.Window;
+import com.google.gwt.user.client.ui.ClickListener;
+import com.google.gwt.user.client.ui.HTML;
+import com.google.gwt.user.client.ui.Widget;
+import com.itmill.toolkit.terminal.gwt.client.ApplicationConnection;
+import com.itmill.toolkit.terminal.gwt.client.ErrorMessage;
+import com.itmill.toolkit.terminal.gwt.client.Paintable;
+import com.itmill.toolkit.terminal.gwt.client.UIDL;
+
+public class ILink extends HTML implements Paintable, ClickListener {
+
+    public static final String CLASSNAME = "i-link";
+
+    private static final int BORDER_STYLE_DEFAULT = 0;
+    private static final int BORDER_STYLE_MINIMAL = 1;
+    private static final int BORDER_STYLE_NONE = 2;
+
+    private String src;
+
+    private String target;
+
+    private int borderStyle = BORDER_STYLE_DEFAULT;
+
+    private boolean enabled;
+
+    private boolean readonly;
+
+    private int targetWidth;
+
+    private int targetHeight;
+
+    private Element errorIndicatorElement;
+
+    private final Element captionElement = DOM.createSpan();
+
+    private ErrorMessage errorMessage;
+
+    private Icon icon;
+
+    public ILink() {
+        super();
+        DOM.appendChild(getElement(), captionElement);
+        addClickListener(this);
+        setStyleName(CLASSNAME);
+    }
+
+    public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+
+        // Ensure correct implementation,
+        // but don't let container manage caption etc.
+        if (client.updateComponent(this, uidl, false)) {
+            return;
+        }
+
+        enabled = uidl.hasAttribute("disabled") ? false : true;
+        readonly = uidl.hasAttribute("readonly") ? true : false;
+
+        if (uidl.hasAttribute("name")) {
+            target = uidl.getStringAttribute("name");
+        }
+        if (uidl.hasAttribute("src")) {
+            src = client.translateToolkitUri(uidl.getStringAttribute("src"));
+        }
+
+        if (uidl.hasAttribute("border")) {
+            if ("none".equals(uidl.getStringAttribute("border"))) {
+                borderStyle = BORDER_STYLE_NONE;
+            } else {
+                borderStyle = BORDER_STYLE_MINIMAL;
+            }
+        } else {
+            borderStyle = BORDER_STYLE_DEFAULT;
+        }
+
+        targetHeight = uidl.hasAttribute("height") ? uidl
+                .getIntAttribute("targetHeight") : -1;
+        targetWidth = uidl.hasAttribute("width") ? uidl
+                .getIntAttribute("targetHeidth") : -1;
+
+        // Set link caption
+        DOM.setInnerText(captionElement, uidl.getStringAttribute("caption"));
+
+        // handle error
+        if (uidl.hasAttribute("error")) {
+            final UIDL errorUidl = uidl.getErrors();
+            if (errorIndicatorElement == null) {
+                errorIndicatorElement = DOM.createDiv();
+                DOM.setElementProperty(errorIndicatorElement, "className",
+                        "i-errorindicator");
+                DOM.sinkEvents(errorIndicatorElement, Event.MOUSEEVENTS);
+                sinkEvents(Event.MOUSEEVENTS);
+            }
+            DOM.insertChild(getElement(), errorIndicatorElement, 0);
+            if (errorMessage == null) {
+                errorMessage = new ErrorMessage();
+            }
+            errorMessage.updateFromUIDL(errorUidl);
+
+        } else if (errorIndicatorElement != null) {
+            DOM.setStyleAttribute(errorIndicatorElement, "display", "none");
+        }
+
+        if (uidl.hasAttribute("icon")) {
+            if (icon == null) {
+                icon = new Icon(client);
+                DOM.insertChild(getElement(), icon.getElement(), 0);
+            }
+            icon.setUri(uidl.getStringAttribute("icon"));
+        }
+
+        // handle description
+        if (uidl.hasAttribute("description")) {
+            setTitle(uidl.getStringAttribute("description"));
+        }
+
+    }
+
+    public void onClick(Widget sender) {
+        if (enabled && !readonly) {
+            if (target == null) {
+                target = "_self";
+            }
+            String features;
+            switch (borderStyle) {
+            case BORDER_STYLE_NONE:
+                features = "menubar=no,location=no,status=no";
+                break;
+            case BORDER_STYLE_MINIMAL:
+                features = "menubar=yes,location=no,status=no";
+                break;
+            default:
+                features = "";
+                break;
+            }
+
+            if (targetWidth > 0) {
+                features += (features.length() > 0 ? "," : "") + "width="
+                        + targetWidth;
+            }
+            if (targetHeight > 0) {
+                features += (features.length() > 0 ? "," : "") + "height="
+                        + targetHeight;
+            }
+
+            Window.open(src, target, features);
+        }
+    }
+
+    public void onBrowserEvent(Event event) {
+        final Element target = DOM.eventGetTarget(event);
+        if (errorIndicatorElement != null
+                && DOM.compare(target, errorIndicatorElement)) {
+            switch (DOM.eventGetType(event)) {
+            case Event.ONMOUSEOVER:
+                showErrorMessage();
+                break;
+            case Event.ONMOUSEOUT:
+                hideErrorMessage();
+                break;
+            case Event.ONCLICK:
+                ApplicationConnection.getConsole().log(
+                        DOM.getInnerHTML(errorMessage.getElement()));
+                return;
+            default:
+                break;
+            }
+        }
+        if (DOM.compare(target, captionElement)
+                || (icon != null && DOM.compare(target, icon.getElement()))) {
+            super.onBrowserEvent(event);
+        }
+    }
+
+    private void hideErrorMessage() {
+        errorMessage.hide();
+    }
+
+    private void showErrorMessage() {
+        if (errorMessage != null) {
+            errorMessage.showAt(errorIndicatorElement);
+        }
+    }
+
+}
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/IListSelect.java b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/IListSelect.java
new file mode 100644 (file)
index 0000000..ccc149a
--- /dev/null
@@ -0,0 +1,126 @@
+/* 
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.itmill.toolkit.terminal.gwt.client.ui;
+
+import java.util.Iterator;
+import java.util.Vector;
+
+import com.google.gwt.user.client.Event;
+import com.google.gwt.user.client.ui.ListBox;
+import com.google.gwt.user.client.ui.Widget;
+import com.itmill.toolkit.terminal.gwt.client.ApplicationConnection;
+import com.itmill.toolkit.terminal.gwt.client.Paintable;
+import com.itmill.toolkit.terminal.gwt.client.Tooltip;
+import com.itmill.toolkit.terminal.gwt.client.UIDL;
+
+public class IListSelect extends IOptionGroupBase {
+
+    public static final String CLASSNAME = "i-select";
+
+    private static final int VISIBLE_COUNT = 10;
+
+    protected TooltipListBox select;
+
+    private int lastSelectedIndex = -1;
+
+    public IListSelect() {
+        super(new TooltipListBox(true), CLASSNAME);
+        select = (TooltipListBox) optionsContainer;
+        select.setSelect(this);
+        select.addChangeListener(this);
+        select.addClickListener(this);
+        select.setStyleName(CLASSNAME + "-select");
+        select.setVisibleItemCount(VISIBLE_COUNT);
+    }
+
+    protected void buildOptions(UIDL uidl) {
+        select.setClient(client);
+        select.setMultipleSelect(isMultiselect());
+        select.setEnabled(!isDisabled() && !isReadonly());
+        select.clear();
+        if (!isMultiselect() && isNullSelectionAllowed()
+                && !isNullSelectionItemAvailable()) {
+            // can't unselect last item in singleselect mode
+            select.addItem("", null);
+        }
+        for (final Iterator i = uidl.getChildIterator(); i.hasNext();) {
+            final UIDL optionUidl = (UIDL) i.next();
+            select.addItem(optionUidl.getStringAttribute("caption"), optionUidl
+                    .getStringAttribute("key"));
+            if (optionUidl.hasAttribute("selected")) {
+                select.setItemSelected(select.getItemCount() - 1, true);
+            }
+        }
+        if (getRows() > 0) {
+            select.setVisibleItemCount(getRows());
+        }
+    }
+
+    protected Object[] getSelectedItems() {
+        final Vector selectedItemKeys = new Vector();
+        for (int i = 0; i < select.getItemCount(); i++) {
+            if (select.isItemSelected(i)) {
+                selectedItemKeys.add(select.getValue(i));
+            }
+        }
+        return selectedItemKeys.toArray();
+    }
+
+    public void onChange(Widget sender) {
+        final int si = select.getSelectedIndex();
+        if (si == -1 && !isNullSelectionAllowed()) {
+            select.setSelectedIndex(lastSelectedIndex);
+        } else {
+            lastSelectedIndex = si;
+            if (isMultiselect()) {
+                client.updateVariable(id, "selected", getSelectedItems(),
+                        isImmediate());
+            } else {
+                client.updateVariable(id, "selected", new String[] { ""
+                        + getSelectedItem() }, isImmediate());
+            }
+        }
+    }
+
+    public void setHeight(String height) {
+        select.setHeight(height);
+        super.setHeight(height);
+    }
+
+    public void setWidth(String width) {
+        select.setWidth(width);
+        super.setWidth(width);
+    }
+
+}
+
+/**
+ * Extended ListBox to listen tooltip events and forward them to generic
+ * handler.
+ */
+class TooltipListBox extends ListBox {
+    private ApplicationConnection client;
+    private Paintable pntbl;
+
+    TooltipListBox(boolean isMultiselect) {
+        super(isMultiselect);
+        sinkEvents(Tooltip.TOOLTIP_EVENTS);
+    }
+
+    public void setClient(ApplicationConnection client) {
+        this.client = client;
+    }
+
+    public void setSelect(Paintable s) {
+        pntbl = s;
+    }
+
+    public void onBrowserEvent(Event event) {
+        super.onBrowserEvent(event);
+        if (client != null) {
+            client.handleTooltipEvent(event, pntbl);
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/IMenuBar.java b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/IMenuBar.java
new file mode 100644 (file)
index 0000000..58d6f6f
--- /dev/null
@@ -0,0 +1,152 @@
+package com.itmill.toolkit.terminal.gwt.client.ui;
+
+import java.util.Iterator;
+import java.util.Stack;
+
+import com.google.gwt.user.client.Command;
+import com.google.gwt.user.client.ui.MenuBar;
+import com.google.gwt.user.client.ui.MenuItem;
+import com.itmill.toolkit.terminal.gwt.client.ApplicationConnection;
+import com.itmill.toolkit.terminal.gwt.client.Paintable;
+import com.itmill.toolkit.terminal.gwt.client.UIDL;
+
+public class IMenuBar extends MenuBar implements Paintable {
+
+    /** Set the CSS class name to allow styling. */
+    public static final String CLASSNAME = "i-menubar";
+
+    /** Component identifier in UIDL communications. */
+    protected String uidlId;
+
+    /** Reference to the server connection object. */
+    protected ApplicationConnection client;
+
+    /** A host reference for the Command objects */
+    protected final IMenuBar hostReference = this;
+
+    /**
+     * The constructor should first call super() to initialize the component and
+     * then handle any initialization relevant to IT Mill Toolkit.
+     */
+    public IMenuBar() {
+        // The superclass has a lot of relevant initialization
+        super();
+
+        // This method call of the Paintable interface sets the component
+        // style name in DOM tree
+        setStyleName(CLASSNAME);
+    }
+
+    /**
+     * This method must be implemented to update the client-side component from
+     * UIDL data received from server.
+     * 
+     * This method is called when the page is loaded for the first time, and
+     * every time UI changes in the component are received from the server.
+     */
+    public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+        // This call should be made first. Ensure correct implementation,
+        // and let the containing layout manage caption, etc.
+        if (client.updateComponent(this, uidl, true)) {
+            return;
+        }
+
+        // Save reference to server connection object to be able to send
+        // user interaction later
+        this.client = client;
+
+        // Save the UIDL identifier for the component
+        uidlId = uidl.getId();
+
+        // Empty the menu every time it receives new information
+        if (!this.getItems().isEmpty()) {
+            this.clearItems();
+        }
+
+        /* Get tree received from server and actualize it in the GWT-MenuBar */
+
+        // For GWT 1.5
+        // this.setAnimationEnabled(uidl.getBooleanAttribute("animationEnabled"));
+        UIDL items = uidl.getChildUIDL(1);
+        Iterator itr = items.getChildIterator();
+        Stack iteratorStack = new Stack();
+        Stack menuStack = new Stack();
+        MenuBar currentMenu = this;
+
+        // Construct an empty command to be used when the item has no command
+        // associated
+        Command emptyCommand = new Command() {
+            public void execute() {
+            }
+        };
+
+        while (itr.hasNext()) {
+            UIDL item = (UIDL) itr.next();
+            MenuItem menuItem = null; // For receiving the item
+
+            String itemText = item.getStringAttribute("text");
+            final int itemId = item.getIntAttribute("id");
+
+            boolean itemHasCommand = item.getBooleanAttribute("command");
+
+            // Construct html from the text and the optional icon
+            if (!item.hasAttribute("icon")) {
+                itemText = "<p>" + itemText + "</p>";
+            } else {
+                itemText = "<p>"
+                        + "<img src=\""
+                        + client.translateToolkitUri(item
+                                .getStringAttribute("icon")) + "\"</img>"
+                        + itemText + "</p>";
+            }
+
+            // Check if we need to attach a command to this item
+            if (itemHasCommand) {
+                // Construct a command that fires onMenuClick(int) with the
+                // item's id-number
+                Command normalCommand = new Command() {
+                    public void execute() {
+                        hostReference.onMenuClick(itemId);
+                    }
+                };
+
+                menuItem = currentMenu.addItem(itemText, true, normalCommand);
+
+            } else {
+                menuItem = currentMenu.addItem(itemText, true, emptyCommand);
+            }
+
+            if (item.getChildCount() > 0) {
+                menuStack.push(currentMenu);
+                iteratorStack.push(itr);
+                itr = item.getChildIterator();
+                currentMenu = new MenuBar(true);
+                menuItem.setSubMenu(currentMenu);
+            }
+
+            if (!itr.hasNext() && !iteratorStack.empty()) {
+                itr = (Iterator) iteratorStack.pop();
+                currentMenu = (MenuBar) menuStack.pop();
+            }
+        }// while
+
+    }// updateFromUIDL
+
+    /**
+     * This is called by the items in the menu and it communicates the
+     * information to the server
+     * 
+     * @param clickedItemId
+     *                id of the item that was clicked
+     */
+    public void onMenuClick(int clickedItemId) {
+        // Updating the state to the server can not be done before
+        // the server connection is known, i.e., before updateFromUIDL()
+        // has been called.
+        if (uidlId != null && client != null) {
+            // Communicate the user interaction parameters to server. This call
+            // will initiate an AJAX request to the server.
+            client.updateVariable(uidlId, "clickedId", clickedItemId, true);
+        }
+    }
+}// class IMenuBar
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/INativeSelect.java b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/INativeSelect.java
new file mode 100644 (file)
index 0000000..b011928
--- /dev/null
@@ -0,0 +1,77 @@
+/* 
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.itmill.toolkit.terminal.gwt.client.ui;
+
+import java.util.Iterator;
+import java.util.Vector;
+
+import com.google.gwt.user.client.ui.Widget;
+import com.itmill.toolkit.terminal.gwt.client.UIDL;
+
+public class INativeSelect extends IOptionGroupBase implements Field {
+
+    public static final String CLASSNAME = "i-select";
+
+    protected TooltipListBox select;
+
+    public INativeSelect() {
+        super(new TooltipListBox(false), CLASSNAME);
+        select = (TooltipListBox) optionsContainer;
+        select.setSelect(this);
+        select.setVisibleItemCount(1);
+        select.addChangeListener(this);
+        select.setStyleName(CLASSNAME + "-select");
+
+    }
+
+    protected void buildOptions(UIDL uidl) {
+        select.setClient(client);
+        select.setEnabled(!isDisabled() && !isReadonly());
+        select.clear();
+        if (isNullSelectionAllowed() && !isNullSelectionItemAvailable()) {
+            // can't unselect last item in singleselect mode
+            select.addItem("", null);
+        }
+        for (final Iterator i = uidl.getChildIterator(); i.hasNext();) {
+            final UIDL optionUidl = (UIDL) i.next();
+            select.addItem(optionUidl.getStringAttribute("caption"), optionUidl
+                    .getStringAttribute("key"));
+            if (optionUidl.hasAttribute("selected")) {
+                select.setItemSelected(select.getItemCount() - 1, true);
+            }
+        }
+    }
+
+    protected Object[] getSelectedItems() {
+        final Vector selectedItemKeys = new Vector();
+        for (int i = 0; i < select.getItemCount(); i++) {
+            if (select.isItemSelected(i)) {
+                selectedItemKeys.add(select.getValue(i));
+            }
+        }
+        return selectedItemKeys.toArray();
+    }
+
+    public void onChange(Widget sender) {
+        if (select.isMultipleSelect()) {
+            client.updateVariable(id, "selected", getSelectedItems(),
+                    isImmediate());
+        } else {
+            client.updateVariable(id, "selected", new String[] { ""
+                    + getSelectedItem() }, isImmediate());
+        }
+    }
+
+    public void setHeight(String height) {
+        select.setHeight(height);
+        super.setHeight(height);
+    }
+
+    public void setWidth(String width) {
+        select.setWidth(width);
+        super.setWidth(width);
+    }
+
+}
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/IOptionGroup.java b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/IOptionGroup.java
new file mode 100644 (file)
index 0000000..5120d91
--- /dev/null
@@ -0,0 +1,78 @@
+/* \r
+@ITMillApache2LicenseForJavaFiles@\r
+ */\r
+\r
+package com.itmill.toolkit.terminal.gwt.client.ui;\r
+\r
+import java.util.HashMap;\r
+import java.util.Iterator;\r
+import java.util.Map;\r
+\r
+import com.google.gwt.user.client.ui.CheckBox;\r
+import com.google.gwt.user.client.ui.Panel;\r
+import com.google.gwt.user.client.ui.RadioButton;\r
+import com.google.gwt.user.client.ui.Widget;\r
+import com.itmill.toolkit.terminal.gwt.client.UIDL;\r
+\r
+public class IOptionGroup extends IOptionGroupBase {\r
+\r
+    public static final String CLASSNAME = "i-select-optiongroup";\r
+\r
+    private final Panel panel;\r
+\r
+    private final Map optionsToKeys;\r
+\r
+    public IOptionGroup() {\r
+        super(CLASSNAME);\r
+        panel = (Panel) optionsContainer;\r
+        optionsToKeys = new HashMap();\r
+    }\r
+\r
+    /*\r
+     * Return true if no elements were changed, false otherwise.\r
+     */\r
+    protected void buildOptions(UIDL uidl) {\r
+        panel.clear();\r
+        for (final Iterator it = uidl.getChildIterator(); it.hasNext();) {\r
+            final UIDL opUidl = (UIDL) it.next();\r
+            CheckBox op;\r
+            if (isMultiselect()) {\r
+                op = new ICheckBox();\r
+                op.setText(opUidl.getStringAttribute("caption"));\r
+            } else {\r
+                op = new RadioButton(id, opUidl.getStringAttribute("caption"));\r
+                op.setStyleName("i-radiobutton");\r
+            }\r
+            op.addStyleName(CLASSNAME_OPTION);\r
+            op.setChecked(opUidl.getBooleanAttribute("selected"));\r
+            op.setEnabled(!opUidl.getBooleanAttribute("disabled")\r
+                    && !isReadonly() && !isDisabled());\r
+            op.addClickListener(this);\r
+            optionsToKeys.put(op, opUidl.getStringAttribute("key"));\r
+            panel.add(op);\r
+        }\r
+    }\r
+\r
+    protected Object[] getSelectedItems() {\r
+        return selectedKeys.toArray();\r
+    }\r
+\r
+    public void onClick(Widget sender) {\r
+        super.onClick(sender);\r
+        if (sender instanceof CheckBox) {\r
+            final boolean selected = ((CheckBox) sender).isChecked();\r
+            final String key = (String) optionsToKeys.get(sender);\r
+            if (!isMultiselect()) {\r
+                selectedKeys.clear();\r
+            }\r
+            if (selected) {\r
+                selectedKeys.add(key);\r
+            } else {\r
+                selectedKeys.remove(key);\r
+            }\r
+            client.updateVariable(id, "selected", getSelectedItems(),\r
+                    isImmediate());\r
+        }\r
+    }\r
+\r
+}\r
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/IOptionGroupBase.java b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/IOptionGroupBase.java
new file mode 100644 (file)
index 0000000..04ac2ce
--- /dev/null
@@ -0,0 +1,223 @@
+/* \r
+@ITMillApache2LicenseForJavaFiles@\r
+ */\r
+\r
+package com.itmill.toolkit.terminal.gwt.client.ui;\r
+\r
+import java.util.Set;\r
+\r
+import com.google.gwt.user.client.ui.ChangeListener;\r
+import com.google.gwt.user.client.ui.ClickListener;\r
+import com.google.gwt.user.client.ui.Composite;\r
+import com.google.gwt.user.client.ui.FlowPanel;\r
+import com.google.gwt.user.client.ui.KeyboardListener;\r
+import com.google.gwt.user.client.ui.Panel;\r
+import com.google.gwt.user.client.ui.Widget;\r
+import com.itmill.toolkit.terminal.gwt.client.ApplicationConnection;\r
+import com.itmill.toolkit.terminal.gwt.client.Paintable;\r
+import com.itmill.toolkit.terminal.gwt.client.UIDL;\r
+\r
+abstract class IOptionGroupBase extends Composite implements Paintable, Field,\r
+        ClickListener, ChangeListener, KeyboardListener {\r
+\r
+    public static final String CLASSNAME_OPTION = "i-select-option";\r
+\r
+    protected ApplicationConnection client;\r
+\r
+    protected String id;\r
+\r
+    protected Set selectedKeys;\r
+\r
+    private boolean immediate;\r
+\r
+    private boolean multiselect;\r
+\r
+    private boolean disabled;\r
+\r
+    private boolean readonly;\r
+\r
+    private int cols = 0;\r
+\r
+    private int rows = 0;\r
+\r
+    private boolean nullSelectionAllowed = true;\r
+\r
+    private boolean nullSelectionItemAvailable = false;\r
+\r
+    /**\r
+     * Widget holding the different options (e.g. ListBox or Panel for radio\r
+     * buttons) (optional, fallbacks to container Panel)\r
+     */\r
+    protected Widget optionsContainer;\r
+\r
+    /**\r
+     * Panel containing the component\r
+     */\r
+    private final Panel container;\r
+\r
+    private ITextField newItemField;\r
+\r
+    private IButton newItemButton;\r
+\r
+    public IOptionGroupBase(String classname) {\r
+        container = new FlowPanel();\r
+        initWidget(container);\r
+        optionsContainer = container;\r
+        container.setStyleName(classname);\r
+        immediate = false;\r
+        multiselect = false;\r
+    }\r
+\r
+    /*\r
+     * Call this if you wish to specify your own container for the option\r
+     * elements (e.g. SELECT)\r
+     */\r
+    public IOptionGroupBase(Widget w, String classname) {\r
+        this(classname);\r
+        optionsContainer = w;\r
+        container.add(optionsContainer);\r
+    }\r
+\r
+    protected boolean isImmediate() {\r
+        return immediate;\r
+    }\r
+\r
+    protected boolean isMultiselect() {\r
+        return multiselect;\r
+    }\r
+\r
+    protected boolean isDisabled() {\r
+        return disabled;\r
+    }\r
+\r
+    protected boolean isReadonly() {\r
+        return readonly;\r
+    }\r
+\r
+    protected boolean isNullSelectionAllowed() {\r
+        return nullSelectionAllowed;\r
+    }\r
+\r
+    protected boolean isNullSelectionItemAvailable() {\r
+        return nullSelectionItemAvailable;\r
+    }\r
+\r
+    /**\r
+     * @return "cols" specified in uidl, 0 if not specified\r
+     */\r
+    protected int getColumns() {\r
+        return cols;\r
+    }\r
+\r
+    /**\r
+     * @return "rows" specified in uidl, 0 if not specified\r
+     */\r
+\r
+    protected int getRows() {\r
+        return rows;\r
+    }\r
+\r
+    public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {\r
+        this.client = client;\r
+        id = uidl.getId();\r
+\r
+        if (client.updateComponent(this, uidl, true)) {\r
+            return;\r
+        }\r
+\r
+        selectedKeys = uidl.getStringArrayVariableAsSet("selected");\r
+\r
+        readonly = uidl.getBooleanAttribute("readonly");\r
+        disabled = uidl.getBooleanAttribute("disabled");\r
+        multiselect = "multi".equals(uidl.getStringAttribute("selectmode"));\r
+        immediate = uidl.getBooleanAttribute("immediate");\r
+        nullSelectionAllowed = uidl.getBooleanAttribute("nullselect");\r
+        nullSelectionItemAvailable = uidl.getBooleanAttribute("nullselectitem");\r
+\r
+        cols = uidl.getIntAttribute("cols");\r
+        rows = uidl.getIntAttribute("rows");\r
+\r
+        final UIDL ops = uidl.getChildUIDL(0);\r
+\r
+        if (getColumns() > 0) {\r
+            container.setWidth(getColumns() + "em");\r
+            if (container != optionsContainer) {\r
+                optionsContainer.setWidth("100%");\r
+            }\r
+        }\r
+\r
+        buildOptions(ops);\r
+\r
+        if (uidl.getBooleanAttribute("allownewitem")) {\r
+            if (newItemField == null) {\r
+                newItemButton = new IButton();\r
+                newItemButton.setText("+");\r
+                newItemButton.setWidth("1.5em");\r
+                newItemButton.addClickListener(this);\r
+                newItemField = new ITextField();\r
+                newItemField.addKeyboardListener(this);\r
+                // newItemField.setColumns(16);\r
+                if (getColumns() > 0) {\r
+                    newItemField.setWidth((getColumns() - 2) + "em");\r
+                }\r
+            }\r
+            newItemField.setEnabled(!disabled && !readonly);\r
+            newItemButton.setEnabled(!disabled && !readonly);\r
+\r
+            if (newItemField == null || newItemField.getParent() != container) {\r
+                container.add(newItemField);\r
+                container.add(newItemButton);\r
+            }\r
+        } else if (newItemField != null) {\r
+            container.remove(newItemField);\r
+            container.remove(newItemButton);\r
+        }\r
+\r
+    }\r
+\r
+    public void onClick(Widget sender) {\r
+        if (sender == newItemButton && !newItemField.getText().equals("")) {\r
+            client.updateVariable(id, "newitem", newItemField.getText(), true);\r
+            newItemField.setText("");\r
+        }\r
+    }\r
+\r
+    public void onChange(Widget sender) {\r
+        if (multiselect) {\r
+            client\r
+                    .updateVariable(id, "selected", getSelectedItems(),\r
+                            immediate);\r
+        } else {\r
+            client.updateVariable(id, "selected", new String[] { ""\r
+                    + getSelectedItem() }, immediate);\r
+        }\r
+    }\r
+\r
+    public void onKeyPress(Widget sender, char keyCode, int modifiers) {\r
+        if (sender == newItemField && keyCode == KeyboardListener.KEY_ENTER) {\r
+            newItemButton.click();\r
+        }\r
+    }\r
+\r
+    public void onKeyUp(Widget sender, char keyCode, int modifiers) {\r
+        // Ignore, subclasses may override\r
+    }\r
+\r
+    public void onKeyDown(Widget sender, char keyCode, int modifiers) {\r
+        // Ignore, subclasses may override\r
+    }\r
+\r
+    protected abstract void buildOptions(UIDL uidl);\r
+\r
+    protected abstract Object[] getSelectedItems();\r
+\r
+    protected Object getSelectedItem() {\r
+        final Object[] sel = getSelectedItems();\r
+        if (sel.length > 0) {\r
+            return sel[0];\r
+        } else {\r
+            return null;\r
+        }\r
+    }\r
+\r
+}\r
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/IOrderedLayout.java b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/IOrderedLayout.java
new file mode 100644 (file)
index 0000000..6720574
--- /dev/null
@@ -0,0 +1,502 @@
+/* 
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.itmill.toolkit.terminal.gwt.client.ui;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.Element;
+import com.google.gwt.user.client.ui.ComplexPanel;
+import com.google.gwt.user.client.ui.UIObject;
+import com.google.gwt.user.client.ui.Widget;
+import com.itmill.toolkit.terminal.gwt.client.ApplicationConnection;
+import com.itmill.toolkit.terminal.gwt.client.Caption;
+import com.itmill.toolkit.terminal.gwt.client.Container;
+import com.itmill.toolkit.terminal.gwt.client.Paintable;
+import com.itmill.toolkit.terminal.gwt.client.StyleConstants;
+import com.itmill.toolkit.terminal.gwt.client.UIDL;
+import com.itmill.toolkit.terminal.gwt.client.Util;
+
+/**
+ * Abstract base class for ordered layouts. Use either vertical or horizontal
+ * subclass.
+ * 
+ * @author IT Mill Ltd
+ */
+public abstract class IOrderedLayout extends ComplexPanel implements Container {
+
+    public static final String CLASSNAME = "i-orderedlayout";
+
+    public static final int ORIENTATION_VERTICAL = 0;
+    public static final int ORIENTATION_HORIZONTAL = 1;
+
+    int orientationMode = ORIENTATION_VERTICAL;
+
+    protected HashMap componentToCaption = new HashMap();
+
+    protected ApplicationConnection client;
+
+    /**
+     * Contains reference to Element where Paintables are wrapped. Normally a TR
+     * or a TBODY element.
+     */
+    protected Element childContainer;
+
+    /*
+     * Elements that provides the Layout interface implementation.
+     */
+    protected Element root;
+    protected Element margin;
+
+    private boolean hasComponentSpacing;
+
+    private MarginInfo margins = new MarginInfo(0);
+
+    public IOrderedLayout(int orientation) {
+        orientationMode = orientation;
+        constructDOM();
+        setStyleName(CLASSNAME);
+    }
+
+    protected void constructDOM() {
+        root = DOM.createDiv();
+        margin = DOM.createDiv();
+        DOM.appendChild(root, margin);
+        if (orientationMode == ORIENTATION_HORIZONTAL) {
+            final String structure = "<table cellspacing=\"0\" cellpadding=\"0\"><tbody><tr></tr></tbody></table>";
+            DOM.setInnerHTML(margin, structure);
+            childContainer = DOM.getFirstChild(DOM.getFirstChild(DOM
+                    .getFirstChild(margin)));
+        } else {
+            childContainer = margin;
+        }
+        setElement(root);
+    }
+
+    public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+
+        this.client = client;
+
+        // Ensure correct implementation
+        if (client.updateComponent(this, uidl, false)) {
+            return;
+        }
+
+        // Handle layout margins
+        if (margins.getBitMask() != uidl.getIntAttribute("margins")) {
+            handleMargins(uidl);
+        }
+
+        //
+        hasComponentSpacing = uidl.getBooleanAttribute("spacing");
+
+        // Update contained components
+
+        final ArrayList uidlWidgets = new ArrayList();
+        for (final Iterator it = uidl.getChildIterator(); it.hasNext();) {
+            final UIDL uidlForChild = (UIDL) it.next();
+            final Paintable child = client.getPaintable(uidlForChild);
+            uidlWidgets.add(child);
+        }
+
+        final ArrayList oldWidgets = getPaintables();
+
+        final Iterator oldIt = oldWidgets.iterator();
+        final Iterator newIt = uidlWidgets.iterator();
+        final Iterator newUidl = uidl.getChildIterator();
+
+        final ArrayList paintedWidgets = new ArrayList();
+
+        Widget oldChild = null;
+        while (newIt.hasNext()) {
+            final Widget child = (Widget) newIt.next();
+            final UIDL childUidl = (UIDL) newUidl.next();
+
+            if (oldChild == null && oldIt.hasNext()) {
+                // search for next old Paintable which still exists in layout
+                // and delete others
+                while (oldIt.hasNext()) {
+                    oldChild = (Widget) oldIt.next();
+                    // now oldChild is an instance of Paintable
+                    if (paintedWidgets.contains(oldChild)) {
+                        continue;
+                    } else if (uidlWidgets.contains(oldChild)) {
+                        break;
+                    } else {
+                        removePaintable((Paintable) oldChild);
+                        oldChild = null;
+                    }
+                }
+            }
+            if (oldChild == null) {
+                // we are adding components to layout
+                add(child);
+            } else if (child == oldChild) {
+                // child already attached and updated
+                oldChild = null;
+            } else if (hasChildComponent(child)) {
+                // current child has been moved, re-insert before current
+                // oldChild
+                // TODO this might be optimized by moving only container element
+                // to correct position
+                removeCaption(child);
+                int index = getPaintableIndex(oldChild);
+                if (componentToCaption.containsKey(oldChild)) {
+                    index--;
+                }
+                remove(child);
+                this.insert(child, index);
+            } else {
+                // insert new child before old one
+                final int index = getPaintableIndex(oldChild); // TODO this
+                // returns wrong
+                // value if
+                // captions are
+                // used
+                insert(child, index);
+            }
+            ((Paintable) child).updateFromUIDL(childUidl, client);
+            paintedWidgets.add(child);
+        }
+        // remove possibly remaining old Paintable object which were not updated
+        while (oldIt.hasNext()) {
+            oldChild = (Widget) oldIt.next();
+            final Paintable p = (Paintable) oldChild;
+            if (!uidlWidgets.contains(p)) {
+                removePaintable(p);
+            }
+        }
+
+        // Handle component alignments
+        handleAlignments(uidl);
+    }
+
+    /**
+     * Retuns a list of Paintables currently rendered in layout
+     * 
+     * @return list of Paintable objects
+     */
+    protected ArrayList getPaintables() {
+        final ArrayList al = new ArrayList();
+        final Iterator it = iterator();
+        while (it.hasNext()) {
+            final Widget w = (Widget) it.next();
+            if (w instanceof Paintable) {
+                al.add(w);
+            }
+        }
+        return al;
+    }
+
+    /**
+     * Removes Paintable from DOM and its reference from ApplicationConnection.
+     * 
+     * Also removes Paintable's Caption if one exists
+     * 
+     * @param p
+     *                Paintable to be removed
+     */
+    public boolean removePaintable(Paintable p) {
+        final Caption c = (Caption) componentToCaption.get(p);
+        if (c != null) {
+            componentToCaption.remove(c);
+            remove(c);
+        }
+        client.unregisterPaintable(p);
+        return remove((Widget) p);
+    }
+
+    /*
+     * (non-Javadoc)
+     * 
+     * @see com.itmill.toolkit.terminal.gwt.client.Layout#replaceChildComponent(com.google.gwt.user.client.ui.Widget,
+     *      com.google.gwt.user.client.ui.Widget)
+     */
+    public void replaceChildComponent(Widget from, Widget to) {
+        client.unregisterPaintable((Paintable) from);
+        final Caption c = (Caption) componentToCaption.get(from);
+        if (c != null) {
+            remove(c);
+            componentToCaption.remove(c);
+        }
+        final int index = getPaintableIndex(from);
+        if (index >= 0) {
+            remove(index);
+            insert(to, index);
+        }
+    }
+
+    protected void insert(Widget w, int beforeIndex) {
+        if (w instanceof Caption) {
+            final Caption c = (Caption) w;
+            // captions go into same container element as their
+            // owners
+            final Element container = getWidgetWrapperFor((Widget) c.getOwner())
+                    .getContainerElement();
+            final Element captionContainer = DOM.createDiv();
+            DOM.insertChild(container, captionContainer, 0);
+            insert(w, captionContainer, beforeIndex, false);
+        } else {
+            WidgetWrapper wr = new WidgetWrapper();
+            DOM.insertChild(childContainer, wr.getElement(), beforeIndex);
+            insert(w, wr.getContainerElement(), beforeIndex, false);
+        }
+    }
+
+    public boolean hasChildComponent(Widget component) {
+        return getPaintableIndex(component) >= 0;
+    }
+
+    public void updateCaption(Paintable component, UIDL uidl) {
+
+        Caption c = (Caption) componentToCaption.get(component);
+
+        if (Caption.isNeeded(uidl)) {
+            if (c == null) {
+                final int index = getPaintableIndex((Widget) component);
+                c = new Caption(component, client);
+                insert(c, index);
+                componentToCaption.put(component, c);
+            }
+            c.updateCaption(uidl);
+        } else {
+            if (c != null) {
+                remove(c);
+                componentToCaption.remove(component);
+            }
+        }
+    }
+
+    public void removeCaption(Widget w) {
+        final Caption c = (Caption) componentToCaption.get(w);
+        if (c != null) {
+            this.remove(c);
+            componentToCaption.remove(w);
+        }
+    }
+
+    public void add(Widget w) {
+        WidgetWrapper wr = new WidgetWrapper();
+        DOM.appendChild(childContainer, wr.getElement());
+        super.add(w, wr.getContainerElement());
+    }
+
+    public boolean remove(int index) {
+        return remove(getWidget(index));
+    }
+
+    public boolean remove(Widget w) {
+        final Element wrapper = getWidgetWrapperFor(w).getElement();
+        final boolean removed = super.remove(w);
+        if (removed) {
+            if (!(w instanceof Caption)) {
+                DOM.removeChild(childContainer, wrapper);
+            }
+            return true;
+        }
+        return false;
+    }
+
+    public Widget getWidget(int index) {
+        return getChildren().get(index);
+    }
+
+    public int getWidgetCount() {
+        return getChildren().size();
+    }
+
+    public int getWidgetIndex(Widget child) {
+        return getChildren().indexOf(child);
+    }
+
+    public int getPaintableCount() {
+        int size = 0;
+        for (Iterator it = getChildren().iterator(); it.hasNext();) {
+            Widget w = (Widget) it.next();
+            if (!(w instanceof Caption)) {
+                size++;
+            }
+        }
+        return size;
+    }
+
+    public int getPaintableIndex(Widget child) {
+        int i = 0;
+        for (Iterator it = getChildren().iterator(); it.hasNext();) {
+            Widget w = (Widget) it.next();
+            if (w == child) {
+                return i;
+            } else if (!(w instanceof Caption)) {
+                i++;
+            }
+        }
+        return -1;
+    }
+
+    protected void handleMargins(UIDL uidl) {
+        margins = new MarginInfo(uidl.getIntAttribute("margins"));
+        setStyleName(margin, CLASSNAME + "-" + StyleConstants.MARGIN_TOP,
+                margins.hasTop());
+        setStyleName(margin, CLASSNAME + "-" + StyleConstants.MARGIN_RIGHT,
+                margins.hasRight());
+        setStyleName(margin, CLASSNAME + "-" + StyleConstants.MARGIN_BOTTOM,
+                margins.hasBottom());
+        setStyleName(margin, CLASSNAME + "-" + StyleConstants.MARGIN_LEFT,
+                margins.hasLeft());
+    }
+
+    protected void handleAlignments(UIDL uidl) {
+        // Component alignments as a comma separated list.
+        // See com.itmill.toolkit.terminal.gwt.client.ui.AlignmentInfo.java for
+        // possible values.
+        final int[] alignments = uidl.getIntArrayAttribute("alignments");
+        int alignmentIndex = 0;
+        // Insert alignment attributes
+        final Iterator it = getPaintables().iterator();
+        boolean first = true;
+        while (it.hasNext()) {
+
+            // Calculate alignment info
+            final AlignmentInfo ai = new AlignmentInfo(
+                    alignments[alignmentIndex++]);
+
+            final WidgetWrapper wr = getWidgetWrapperFor((Widget) it.next());
+            wr.setAlignment(ai.getVerticalAlignment(), ai
+                    .getHorizontalAlignment());
+
+            // Handle spacing in this loop as well
+            if (first) {
+                wr.setSpacingEnabled(false);
+                first = false;
+            } else {
+                wr.setSpacingEnabled(hasComponentSpacing);
+            }
+        }
+    }
+
+    /**
+     * WidgetWrapper classe. Helper classe for spacing and alignment handling.
+     * 
+     */
+    class WidgetWrapper extends UIObject {
+
+        Element td;
+
+        public WidgetWrapper() {
+            if (orientationMode == ORIENTATION_VERTICAL) {
+                setElement(DOM.createDiv());
+                // Apply 'hasLayout' for IE (needed to get accurate dimension
+                // calculations)
+                if (Util.isIE()) {
+                    DOM.setStyleAttribute(getElement(), "zoom", "1");
+                }
+            } else {
+                setElement(DOM.createTD());
+            }
+        }
+
+        public WidgetWrapper(Element element) {
+            if (DOM.getElementProperty(element, "className").equals("i_align")) {
+                td = element;
+                setElement(DOM.getParent(DOM.getParent(DOM.getParent(DOM
+                        .getParent(td)))));
+            } else {
+                setElement(element);
+            }
+        }
+
+        Element getContainerElement() {
+            if (td != null) {
+                return td;
+            } else {
+                return getElement();
+            }
+        }
+
+        void setAlignment(String verticalAlignment, String horizontalAlignment) {
+
+            // Set vertical alignment
+            if (Util.isIE()) {
+                DOM.setElementAttribute(getElement(), "vAlign",
+                        verticalAlignment);
+            } else {
+                DOM.setStyleAttribute(getElement(), "verticalAlign",
+                        verticalAlignment);
+            }
+
+            // Set horizontal alignment
+            if (Util.isIE()) {
+                DOM.setElementAttribute(getElement(), "align",
+                        horizontalAlignment);
+            } else {
+                // use one-cell table to implement horizontal alignments, only
+                // for values other than "left" (which is default)
+                // build one cell table
+                if (!horizontalAlignment.equals("left")) {
+                    if (td == null) {
+                        final Element table = DOM.createTable();
+                        final Element tBody = DOM.createTBody();
+                        final Element tr = DOM.createTR();
+                        td = DOM.createTD();
+                        DOM.appendChild(table, tBody);
+                        DOM.appendChild(tBody, tr);
+                        DOM.appendChild(tr, td);
+                        DOM.setElementAttribute(table, "cellpadding", "0");
+                        DOM.setElementAttribute(table, "cellspacing", "0");
+                        DOM.setStyleAttribute(table, "width", "100%");
+                        // use className for identification
+                        DOM.setElementProperty(td, "className", "i_align");
+                        // move possible content to cell
+                        while (DOM.getChildCount(getElement()) > 0) {
+                            Element content = DOM.getFirstChild(getElement());
+                            if (content != null) {
+                                DOM.removeChild(getElement(), content);
+                                DOM.appendChild(td, content);
+                            }
+                        }
+                        DOM.appendChild(getElement(), table);
+                    }
+                    DOM.setElementAttribute(td, "align", horizontalAlignment);
+                } else if (td != null) {
+                    // Move content to main container
+                    while (DOM.getChildCount(td) > 0) {
+                        Element content = DOM.getFirstChild(td);
+                        if (content != null) {
+                            DOM.removeChild(td, content);
+                            DOM.appendChild(getElement(), content);
+                        }
+                    }
+                    // Remove unneeded table element
+                    DOM.removeChild(getElement(), DOM
+                            .getFirstChild(getElement()));
+                }
+            }
+        }
+
+        void setSpacingEnabled(boolean b) {
+            setStyleName(
+                    getElement(),
+                    CLASSNAME
+                            + "-"
+                            + (orientationMode == ORIENTATION_HORIZONTAL ? StyleConstants.HORIZONTAL_SPACING
+                                    : StyleConstants.VERTICAL_SPACING), b);
+        }
+
+    }
+
+    /**
+     * Returns given widgets WidgetWrapper
+     * 
+     * @param child
+     * @return
+     */
+    public WidgetWrapper getWidgetWrapperFor(Widget child) {
+        final Element containerElement = DOM.getParent(child.getElement());
+        return new WidgetWrapper(containerElement);
+    }
+
+}
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/IOrderedLayoutHorizontal.java b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/IOrderedLayoutHorizontal.java
new file mode 100644 (file)
index 0000000..105103e
--- /dev/null
@@ -0,0 +1,81 @@
+/* \r
+@ITMillApache2LicenseForJavaFiles@\r
+ */\r
+\r
+package com.itmill.toolkit.terminal.gwt.client.ui;\r
+\r
+import com.google.gwt.user.client.DOM;\r
+import com.itmill.toolkit.terminal.gwt.client.ContainerResizedListener;\r
+import com.itmill.toolkit.terminal.gwt.client.UIDL;\r
+import com.itmill.toolkit.terminal.gwt.client.Util;\r
+\r
+public class IOrderedLayoutHorizontal extends IOrderedLayout implements\r
+        ContainerResizedListener {\r
+\r
+    private String height;\r
+    private boolean relativeHeight;\r
+    private int marginHeight = 0;\r
+\r
+    public IOrderedLayoutHorizontal() {\r
+        super(ORIENTATION_HORIZONTAL);\r
+    }\r
+\r
+    public void setHeight(String newHeight) {\r
+        super.setHeight(newHeight);\r
+        if (newHeight != null && !newHeight.equals("")) {\r
+            if (!newHeight.equals(height)) {\r
+                height = newHeight;\r
+                if (newHeight.indexOf("%") > 0) {\r
+                    relativeHeight = true;\r
+                    DOM.setStyleAttribute(getElement(), "overflow", "hidden");\r
+                } else {\r
+                    relativeHeight = false;\r
+                    DOM.setStyleAttribute(getElement(), "overflow", "");\r
+                }\r
+                setInternalHeight();\r
+            }\r
+        } else {\r
+            if (newHeight != null) {\r
+                // clear existing height values\r
+                DOM.setStyleAttribute(getElement(), "overflow", "");\r
+                DOM.setStyleAttribute(DOM.getFirstChild(margin), "height", "");\r
+\r
+                newHeight = null;\r
+                relativeHeight = false;\r
+            }\r
+        }\r
+    }\r
+\r
+    protected void handleMargins(UIDL uidl) {\r
+        super.handleMargins(uidl);\r
+        if (height != null) {\r
+            marginHeight = -1;\r
+            setInternalHeight();\r
+        }\r
+    }\r
+\r
+    private void setInternalHeight() {\r
+        int availSpace = DOM\r
+                .getElementPropertyInt(getElement(), "clientHeight");\r
+        if (marginHeight < 0) {\r
+            DOM.setStyleAttribute(margin, "height", height);\r
+            int tmp = DOM.getElementPropertyInt(margin, "offsetHeight");\r
+            marginHeight = tmp\r
+                    - DOM.getElementPropertyInt(getElement(), "clientHeight");\r
+            DOM.setStyleAttribute(margin, "height", "");\r
+        }\r
+\r
+        availSpace -= marginHeight;\r
+\r
+        DOM.setStyleAttribute(DOM.getFirstChild(margin), "height", availSpace\r
+                + "px");\r
+\r
+    }\r
+\r
+    public void iLayout() {\r
+        if (relativeHeight) {\r
+            setInternalHeight();\r
+        }\r
+        Util.runDescendentsLayout(this);\r
+    }\r
+}\r
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/IOrderedLayoutVertical.java b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/IOrderedLayoutVertical.java
new file mode 100644 (file)
index 0000000..1214c85
--- /dev/null
@@ -0,0 +1,12 @@
+/* \r
+@ITMillApache2LicenseForJavaFiles@\r
+ */\r
+\r
+package com.itmill.toolkit.terminal.gwt.client.ui;\r
+\r
+public class IOrderedLayoutVertical extends IOrderedLayout {\r
+\r
+    public IOrderedLayoutVertical() {\r
+        super(ORIENTATION_VERTICAL);\r
+    }\r
+}\r
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/IPanel.java b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/IPanel.java
new file mode 100644 (file)
index 0000000..b4ce031
--- /dev/null
@@ -0,0 +1,356 @@
+/* 
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.itmill.toolkit.terminal.gwt.client.ui;
+
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.Element;
+import com.google.gwt.user.client.Event;
+import com.google.gwt.user.client.ui.SimplePanel;
+import com.google.gwt.user.client.ui.Widget;
+import com.itmill.toolkit.terminal.gwt.client.ApplicationConnection;
+import com.itmill.toolkit.terminal.gwt.client.BrowserInfo;
+import com.itmill.toolkit.terminal.gwt.client.ContainerResizedListener;
+import com.itmill.toolkit.terminal.gwt.client.ErrorMessage;
+import com.itmill.toolkit.terminal.gwt.client.Paintable;
+import com.itmill.toolkit.terminal.gwt.client.UIDL;
+import com.itmill.toolkit.terminal.gwt.client.Util;
+
+public class IPanel extends SimplePanel implements Paintable,
+        ContainerResizedListener {
+
+    public static final String CLASSNAME = "i-panel";
+
+    ApplicationConnection client;
+
+    String id;
+
+    private final Element captionNode = DOM.createDiv();
+
+    private final Element captionText = DOM.createSpan();
+
+    private Icon icon;
+
+    private final Element bottomDecoration = DOM.createDiv();
+
+    private final Element contentNode = DOM.createDiv();
+
+    private Element errorIndicatorElement;
+
+    private ErrorMessage errorMessage;
+
+    private String height;
+
+    private Paintable layout;
+
+    ShortcutActionHandler shortcutHandler;
+
+    private String width;
+
+    private Element geckoCaptionMeter;
+
+    public IPanel() {
+        super();
+        DOM.appendChild(getElement(), captionNode);
+        DOM.appendChild(captionNode, captionText);
+        DOM.appendChild(getElement(), contentNode);
+        DOM.appendChild(getElement(), bottomDecoration);
+        setStyleName(CLASSNAME);
+        DOM
+                .setElementProperty(captionNode, "className", CLASSNAME
+                        + "-caption");
+        DOM
+                .setElementProperty(contentNode, "className", CLASSNAME
+                        + "-content");
+        DOM.setElementProperty(bottomDecoration, "className", CLASSNAME
+                + "-deco");
+        DOM.sinkEvents(getElement(), Event.ONKEYDOWN);
+    }
+
+    protected Element getContainerElement() {
+        return contentNode;
+    }
+
+    private void setCaption(String text) {
+        DOM.setInnerText(captionText, text);
+    }
+
+    public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+        // Ensure correct implementation
+        if (client.updateComponent(this, uidl, false)) {
+            return;
+        }
+
+        this.client = client;
+        id = uidl.getId();
+
+        // Panel size. Height needs to be saved for later use
+        height = uidl.hasVariable("height") ? uidl.getStringVariable("height")
+                : null;
+        setWidth(uidl.hasVariable("width") ? uidl.getStringVariable("width")
+                : "");
+
+        // Restore default stylenames
+        DOM
+                .setElementProperty(captionNode, "className", CLASSNAME
+                        + "-caption");
+        DOM
+                .setElementProperty(contentNode, "className", CLASSNAME
+                        + "-content");
+        DOM.setElementProperty(bottomDecoration, "className", CLASSNAME
+                + "-deco");
+
+        // Handle caption displaying
+        boolean hasCaption = false;
+        if (uidl.hasAttribute("caption")
+                && !uidl.getStringAttribute("caption").equals("")) {
+            setCaption(uidl.getStringAttribute("caption"));
+            hasCaption = true;
+        } else {
+            setCaption("");
+            DOM.setElementProperty(captionNode, "className", CLASSNAME
+                    + "-nocaption");
+        }
+
+        setIconUri(uidl, client);
+
+        handleDescription(uidl);
+
+        handleError(uidl);
+
+        // Add proper stylenames for all elements. This way we can prevent
+        // unwanted CSS selector inheritance.
+        if (uidl.hasAttribute("style")) {
+            final String[] styles = uidl.getStringAttribute("style").split(" ");
+            final String captionBaseClass = CLASSNAME
+                    + (hasCaption ? "-caption" : "-nocaption");
+            final String contentBaseClass = CLASSNAME + "-content";
+            final String decoBaseClass = CLASSNAME + "-deco";
+            String captionClass = captionBaseClass;
+            String contentClass = contentBaseClass;
+            String decoClass = decoBaseClass;
+            for (int i = 0; i < styles.length; i++) {
+                captionClass += " " + captionBaseClass + "-" + styles[i];
+                contentClass += " " + contentBaseClass + "-" + styles[i];
+                decoClass += " " + decoBaseClass + "-" + styles[i];
+            }
+            DOM.setElementProperty(captionNode, "className", captionClass);
+            DOM.setElementProperty(contentNode, "className", contentClass);
+            DOM.setElementProperty(bottomDecoration, "className", decoClass);
+        }
+
+        // Height adjustment
+        iLayout(false);
+
+        // Render content
+        final UIDL layoutUidl = uidl.getChildUIDL(0);
+        final Paintable newLayout = client.getPaintable(layoutUidl);
+        if (newLayout != layout) {
+            if (layout != null) {
+                client.unregisterPaintable(layout);
+            }
+            setWidget((Widget) newLayout);
+            layout = newLayout;
+        }
+        (layout).updateFromUIDL(layoutUidl, client);
+
+        // We may have actions attached to this panel
+        if (uidl.getChildCount() > 1) {
+            final int cnt = uidl.getChildCount();
+            for (int i = 1; i < cnt; i++) {
+                UIDL childUidl = uidl.getChildUIDL(i);
+                if (childUidl.getTag().equals("actions")) {
+                    if (shortcutHandler == null) {
+                        shortcutHandler = new ShortcutActionHandler(id, client);
+                    }
+                    shortcutHandler.updateActionMap(childUidl);
+                }
+            }
+        }
+
+    }
+
+    private void handleError(UIDL uidl) {
+        if (uidl.hasAttribute("error")) {
+            final UIDL errorUidl = uidl.getErrors();
+            if (errorIndicatorElement == null) {
+                errorIndicatorElement = DOM.createDiv();
+                DOM.setElementProperty(errorIndicatorElement, "className",
+                        "i-errorindicator");
+                DOM.sinkEvents(errorIndicatorElement, Event.MOUSEEVENTS);
+                sinkEvents(Event.MOUSEEVENTS);
+            }
+            DOM.insertBefore(captionNode, errorIndicatorElement, captionText);
+            if (errorMessage == null) {
+                errorMessage = new ErrorMessage();
+            }
+            errorMessage.updateFromUIDL(errorUidl);
+
+        } else if (errorIndicatorElement != null) {
+            DOM.removeChild(captionNode, errorIndicatorElement);
+            errorIndicatorElement = null;
+        }
+    }
+
+    private void handleDescription(UIDL uidl) {
+        DOM.setElementProperty(captionText, "title", uidl
+                .hasAttribute("description") ? uidl
+                .getStringAttribute("description") : "");
+    }
+
+    private void setIconUri(UIDL uidl, ApplicationConnection client) {
+        final String iconUri = uidl.hasAttribute("icon") ? uidl
+                .getStringAttribute("icon") : null;
+        if (iconUri == null) {
+            if (icon != null) {
+                DOM.removeChild(captionNode, icon.getElement());
+                icon = null;
+            }
+        } else {
+            if (icon == null) {
+                icon = new Icon(client);
+                DOM.insertChild(captionNode, icon.getElement(), 0);
+            }
+            icon.setUri(iconUri);
+        }
+    }
+
+    public void iLayout() {
+        iLayout(true);
+    }
+
+    public void iLayout(boolean runGeckoFix) {
+        if (height != null && height != "") {
+            final boolean hasChildren = getWidget() != null;
+            Element contentEl = null;
+            String origPositioning = null;
+            // save scroll position
+            int scrollTop = DOM.getElementPropertyInt(contentNode, "scrollTop");
+            int scrollLeft = DOM.getElementPropertyInt(contentNode,
+                    "scrollLeft");
+            if (hasChildren) {
+                // Remove children temporary form normal flow to detect proper
+                // size
+                contentEl = getWidget().getElement();
+                origPositioning = DOM.getStyleAttribute(contentEl, "position");
+                DOM.setStyleAttribute(contentEl, "position", "absolute");
+            }
+
+            // Set defaults
+            DOM.setStyleAttribute(contentNode, "overflow", "hidden");
+            DOM.setStyleAttribute(contentNode, "height", "");
+
+            // Calculate target height
+            super.setHeight(height);
+            final int targetHeight = getOffsetHeight();
+
+            // Calculate used height
+            super.setHeight("");
+            final int usedHeight = DOM.getElementPropertyInt(bottomDecoration,
+                    "offsetTop")
+                    + DOM.getElementPropertyInt(bottomDecoration,
+                            "offsetHeight")
+                    - DOM.getElementPropertyInt(getElement(), "offsetTop");
+
+            // Calculate content area height (don't allow negative values)
+            int h = targetHeight - usedHeight;
+            if (h < 0) {
+                h = 0;
+            }
+
+            // Set proper values for content element
+            DOM.setStyleAttribute(contentNode, "height", h + "px");
+            DOM.setStyleAttribute(contentNode, "overflow", "auto");
+
+            // Restore content to flow
+            if (hasChildren) {
+                ApplicationConnection.getConsole().log(
+                        "positioning:" + origPositioning);
+                DOM.setStyleAttribute(contentEl, "position", origPositioning);
+            }
+            // restore scroll position
+            DOM.setElementPropertyInt(contentNode, "scrollTop", scrollTop);
+            DOM.setElementPropertyInt(contentNode, "scrollLeft", scrollLeft);
+
+        } else {
+            DOM.setStyleAttribute(contentNode, "height", "");
+        }
+
+        if (runGeckoFix && BrowserInfo.get().isGecko()) {
+            // workaround for #1764
+            if (width == null || width.equals("")) {
+                if (geckoCaptionMeter == null) {
+                    geckoCaptionMeter = DOM.createDiv();
+                    DOM.appendChild(captionNode, geckoCaptionMeter);
+                }
+                int captionWidth = DOM.getElementPropertyInt(captionText,
+                        "offsetWidth");
+                int availWidth = DOM.getElementPropertyInt(geckoCaptionMeter,
+                        "offsetWidth");
+                if (captionWidth == availWidth) {
+                    /*
+                     * Caption width defines panel width -> Gecko based browsers
+                     * somehow fails to float things right, without the
+                     * "noncode" below
+                     */
+                    setWidth(getOffsetWidth() + "px");
+                } else {
+                    DOM.setStyleAttribute(captionNode, "width", "");
+                }
+            }
+        }
+        Util.runDescendentsLayout(this);
+    }
+
+    public void onBrowserEvent(Event event) {
+        final Element target = DOM.eventGetTarget(event);
+        final int type = DOM.eventGetType(event);
+        if (type == Event.ONKEYDOWN && shortcutHandler != null) {
+            shortcutHandler.handleKeyboardEvent(event);
+            return;
+        }
+        if (errorIndicatorElement != null
+                && DOM.compare(target, errorIndicatorElement)) {
+            switch (type) {
+            case Event.ONMOUSEOVER:
+                if (errorMessage != null) {
+                    errorMessage.showAt(errorIndicatorElement);
+                }
+                break;
+            case Event.ONMOUSEOUT:
+                if (errorMessage != null) {
+                    errorMessage.hide();
+                }
+                break;
+            case Event.ONCLICK:
+                ApplicationConnection.getConsole().log(
+                        DOM.getInnerHTML(errorMessage.getElement()));
+                return;
+            default:
+                break;
+            }
+        }
+    }
+
+    /**
+     * Panel handles dimensions by itself.
+     */
+    public void setHeight(String height) {
+        // NOP
+    }
+
+    /**
+     * Panel handles dimensions by itself.
+     */
+    public void setWidth(String width) {
+        this.width = width;
+        // Let browser handle 100% width (DIV element takes all size by
+        // default).
+        // This way we can specify borders for Panel's outer element.
+        if (!width.equals("100%")) {
+            super.setWidth(width);
+        }
+    }
+
+}
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/IPasswordField.java b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/IPasswordField.java
new file mode 100644 (file)
index 0000000..0690edf
--- /dev/null
@@ -0,0 +1,21 @@
+/* \r
+@ITMillApache2LicenseForJavaFiles@\r
+ */\r
+\r
+package com.itmill.toolkit.terminal.gwt.client.ui;\r
+\r
+import com.google.gwt.user.client.DOM;\r
+\r
+/**\r
+ * This class represents a password field.\r
+ * \r
+ * @author IT Mill Ltd.\r
+ * \r
+ */\r
+public class IPasswordField extends ITextField {\r
+\r
+    public IPasswordField() {\r
+        super(DOM.createInputPassword());\r
+    }\r
+\r
+}\r
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/IPopupCalendar.java b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/IPopupCalendar.java
new file mode 100644 (file)
index 0000000..307519c
--- /dev/null
@@ -0,0 +1,124 @@
+/* \r
+@ITMillApache2LicenseForJavaFiles@\r
+ */\r
+\r
+package com.itmill.toolkit.terminal.gwt.client.ui;\r
+\r
+import com.google.gwt.user.client.DOM;\r
+import com.google.gwt.user.client.Timer;\r
+import com.google.gwt.user.client.Window;\r
+import com.google.gwt.user.client.ui.Button;\r
+import com.google.gwt.user.client.ui.ClickListener;\r
+import com.google.gwt.user.client.ui.PopupListener;\r
+import com.google.gwt.user.client.ui.PopupPanel;\r
+import com.google.gwt.user.client.ui.Widget;\r
+import com.google.gwt.user.client.ui.PopupPanel.PositionCallback;\r
+import com.itmill.toolkit.terminal.gwt.client.ApplicationConnection;\r
+import com.itmill.toolkit.terminal.gwt.client.Paintable;\r
+import com.itmill.toolkit.terminal.gwt.client.UIDL;\r
+\r
+public class IPopupCalendar extends ITextualDate implements Paintable, Field,\r
+        ClickListener, PopupListener {\r
+\r
+    private final Button calendarToggle;\r
+\r
+    private final CalendarPanel calendar;\r
+\r
+    private final ToolkitOverlay popup;\r
+    private boolean open = false;\r
+\r
+    public IPopupCalendar() {\r
+        super();\r
+\r
+        calendarToggle = new Button();\r
+        calendarToggle.setStyleName(CLASSNAME + "-button");\r
+        calendarToggle.setText("...");\r
+        calendarToggle.addClickListener(this);\r
+        add(calendarToggle);\r
+\r
+        calendar = new CalendarPanel(this);\r
+        popup = new ToolkitOverlay(true, true);\r
+        popup.setStyleName(IDateField.CLASSNAME + "-popup");\r
+        popup.setWidget(calendar);\r
+        popup.addPopupListener(this);\r
+\r
+        DOM.setElementProperty(calendar.getElement(), "id",\r
+                "PID_TOOLKIT_POPUPCAL");\r
+\r
+    }\r
+\r
+    public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {\r
+        super.updateFromUIDL(uidl, client);\r
+        if (date != null) {\r
+            calendar.updateCalendar();\r
+        }\r
+        calendarToggle.setEnabled(enabled);\r
+    }\r
+\r
+    public void onClick(Widget sender) {\r
+        if (sender == calendarToggle && !open) {\r
+            open = true;\r
+            calendar.updateCalendar();\r
+            // clear previous values\r
+            popup.setWidth("");\r
+            popup.setHeight("");\r
+            popup.setPopupPositionAndShow(new PositionCallback() {\r
+                public void setPosition(int offsetWidth, int offsetHeight) {\r
+                    final int w = offsetWidth;\r
+                    final int h = offsetHeight;\r
+                    int t = calendarToggle.getAbsoluteTop();\r
+                    int l = calendarToggle.getAbsoluteLeft();\r
+                    if (l + w > Window.getClientWidth()\r
+                            + Window.getScrollLeft()) {\r
+                        l = Window.getClientWidth() + Window.getScrollLeft()\r
+                                - w;\r
+                    }\r
+                    if (t + h > Window.getClientHeight()\r
+                            + Window.getScrollTop()) {\r
+                        t = Window.getClientHeight() + Window.getScrollTop()\r
+                                - h - calendarToggle.getOffsetHeight() - 30;\r
+                        l += calendarToggle.getOffsetWidth();\r
+                    }\r
+                    popup.setPopupPosition(l, t\r
+                            + calendarToggle.getOffsetHeight() + 2);\r
+\r
+                    // fix size\r
+                    popup.setWidth(w + "px");\r
+                    popup.setHeight(h + "px");\r
+                    setFocus(true);\r
+                }\r
+            });\r
+        }\r
+    }\r
+\r
+    public void onPopupClosed(PopupPanel sender, boolean autoClosed) {\r
+        if (sender == popup) {\r
+            buildDate();\r
+            // Sigh.\r
+            Timer t = new Timer() {\r
+                public void run() {\r
+                    open = false;\r
+                }\r
+            };\r
+            t.schedule(100);\r
+        }\r
+    }\r
+\r
+    /**\r
+     * Sets focus to Calendar panel.\r
+     * \r
+     * @param focus\r
+     */\r
+    public void setFocus(boolean focus) {\r
+        calendar.setFocus(focus);\r
+    }\r
+\r
+    protected int getFieldExtraWidth() {\r
+        if (fieldExtraWidth < 0) {\r
+            fieldExtraWidth = super.getFieldExtraWidth();\r
+            fieldExtraWidth += calendarToggle.getOffsetWidth();\r
+        }\r
+        return fieldExtraWidth;\r
+    }\r
+\r
+}\r
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/IProgressIndicator.java b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/IProgressIndicator.java
new file mode 100644 (file)
index 0000000..c7c062c
--- /dev/null
@@ -0,0 +1,82 @@
+/* 
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.itmill.toolkit.terminal.gwt.client.ui;
+
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.Element;
+import com.google.gwt.user.client.Timer;
+import com.google.gwt.user.client.ui.Widget;
+import com.itmill.toolkit.terminal.gwt.client.ApplicationConnection;
+import com.itmill.toolkit.terminal.gwt.client.Paintable;
+import com.itmill.toolkit.terminal.gwt.client.UIDL;
+
+public class IProgressIndicator extends Widget implements Paintable {
+
+    private static final String CLASSNAME = "i-progressindicator";
+    Element wrapper = DOM.createDiv();
+    Element indicator = DOM.createDiv();
+    private ApplicationConnection client;
+    private final Poller poller;
+    private boolean indeterminate = false;
+
+    public IProgressIndicator() {
+        setElement(wrapper);
+        setStyleName(CLASSNAME);
+        DOM.appendChild(wrapper, indicator);
+        poller = new Poller();
+    }
+
+    public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+        if (client.updateComponent(this, uidl, true)) {
+            return;
+        }
+
+        poller.cancel();
+        this.client = client;
+        if (client.updateComponent(this, uidl, true)) {
+            return;
+        }
+
+        indeterminate = uidl.getBooleanAttribute("indeterminate");
+
+        String style = CLASSNAME;
+        if (uidl.getBooleanAttribute("disabled")) {
+            style += "-disabled";
+        }
+
+        if (indeterminate) {
+            this.setStyleName(style + "-indeterminate");
+        } else {
+            setStyleName(style);
+            try {
+                final float f = Float.parseFloat(uidl
+                        .getStringAttribute("state"));
+                final int size = Math.round(100 * f);
+                DOM.setStyleAttribute(indicator, "width", size + "%");
+            } catch (final Exception e) {
+            }
+        }
+
+        if (!uidl.getBooleanAttribute("disabled")) {
+            poller.scheduleRepeating(uidl.getIntAttribute("pollinginterval"));
+        }
+    }
+
+    public void setVisible(boolean visible) {
+        super.setVisible(visible);
+        if (!visible) {
+            poller.cancel();
+        }
+    }
+
+    class Poller extends Timer {
+
+        public void run() {
+            client.sendPendingVariableChanges();
+        }
+
+    }
+
+}
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/IScrollTable.java b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/IScrollTable.java
new file mode 100644 (file)
index 0000000..d15eacc
--- /dev/null
@@ -0,0 +1,2246 @@
+/* 
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.itmill.toolkit.terminal.gwt.client.ui;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+import java.util.Vector;
+
+import com.google.gwt.user.client.Command;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.DeferredCommand;
+import com.google.gwt.user.client.Element;
+import com.google.gwt.user.client.Event;
+import com.google.gwt.user.client.Timer;
+import com.google.gwt.user.client.Window;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.Panel;
+import com.google.gwt.user.client.ui.RootPanel;
+import com.google.gwt.user.client.ui.ScrollListener;
+import com.google.gwt.user.client.ui.ScrollPanel;
+import com.google.gwt.user.client.ui.Widget;
+import com.itmill.toolkit.terminal.gwt.client.ApplicationConnection;
+import com.itmill.toolkit.terminal.gwt.client.ContainerResizedListener;
+import com.itmill.toolkit.terminal.gwt.client.Paintable;
+import com.itmill.toolkit.terminal.gwt.client.UIDL;
+import com.itmill.toolkit.terminal.gwt.client.Util;
+import com.itmill.toolkit.terminal.gwt.client.ui.IScrollTable.IScrollTableBody.IScrollTableRow;
+
+/**
+ * IScrollTable
+ * 
+ * IScrollTable is a FlowPanel having two widgets in it: * TableHead component *
+ * ScrollPanel
+ * 
+ * TableHead contains table's header and widgets + logic for resizing,
+ * reordering and hiding columns.
+ * 
+ * ScrollPanel contains IScrollTableBody object which handles content. To save
+ * some bandwidth and to improve clients responsiveness with loads of data, in
+ * IScrollTableBody all rows are not necessary rendered. There are "spacers" in
+ * IScrollTableBody to use the exact same space as non-rendered rows would use.
+ * This way we can use seamlessly traditional scrollbars and scrolling to fetch
+ * more rows instead of "paging".
+ * 
+ * In IScrollTable we listen to scroll events. On horizontal scrolling we also
+ * update TableHeads scroll position which has its scrollbars hidden. On
+ * vertical scroll events we will check if we are reaching the end of area where
+ * we have rows rendered and
+ * 
+ * TODO implement unregistering for child components in Cells
+ */
+public class IScrollTable extends Composite implements Table, ScrollListener,
+        ContainerResizedListener {
+
+    public static final String CLASSNAME = "i-table";
+    /**
+     * multiple of pagelenght which component will cache when requesting more
+     * rows
+     */
+    private static final double CACHE_RATE = 2;
+    /**
+     * fraction of pageLenght which can be scrolled without making new request
+     */
+    private static final double CACHE_REACT_RATE = 1.5;
+
+    public static final char ALIGN_CENTER = 'c';
+    public static final char ALIGN_LEFT = 'b';
+    public static final char ALIGN_RIGHT = 'e';
+    private int firstRowInViewPort = 0;
+    private int pageLength = 15;
+
+    private boolean showRowHeaders = false;
+
+    private String[] columnOrder;
+
+    private ApplicationConnection client;
+    private String paintableId;
+
+    private boolean immediate;
+
+    private int selectMode = Table.SELECT_MODE_NONE;
+
+    private final HashSet selectedRowKeys = new HashSet();
+
+    private boolean initializedAndAttached = false;
+
+    private final TableHead tHead = new TableHead();
+
+    private final ScrollPanel bodyContainer = new ScrollPanel();
+
+    private int totalRows;
+
+    private Set collapsedColumns;
+
+    private final RowRequestHandler rowRequestHandler;
+    private IScrollTableBody tBody;
+    private String width;
+    private String height;
+    private int firstvisible = 0;
+    private boolean sortAscending;
+    private String sortColumn;
+    private boolean columnReordering;
+
+    /**
+     * This map contains captions and icon urls for actions like: * "33_c" ->
+     * "Edit" * "33_i" -> "http://dom.com/edit.png"
+     */
+    private final HashMap actionMap = new HashMap();
+    private String[] visibleColOrder;
+    private boolean initialContentReceived = false;
+    private Element scrollPositionElement;
+    private final FlowPanel panel;
+    private boolean enabled;
+    private boolean showColHeaders;
+
+    /** flag to indicate that table body has changed */
+    private boolean isNewBody = true;
+
+    /**
+     * Stores old height for IE, that sometimes fails to return correct height
+     * for container element. Then this value is used as a fallback.
+     */
+    private int oldAvailPixels;
+
+    public IScrollTable() {
+
+        bodyContainer.addScrollListener(this);
+        bodyContainer.setStyleName(CLASSNAME + "-body");
+
+        panel = new FlowPanel();
+        panel.setStyleName(CLASSNAME);
+        panel.add(tHead);
+        panel.add(bodyContainer);
+
+        rowRequestHandler = new RowRequestHandler();
+
+        initWidget(panel);
+    }
+
+    public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+        if (client.updateComponent(this, uidl, true)) {
+            return;
+        }
+
+        enabled = !uidl.hasAttribute("disabled");
+
+        this.client = client;
+        paintableId = uidl.getStringAttribute("id");
+        immediate = uidl.getBooleanAttribute("immediate");
+        final int newTotalRows = uidl.getIntAttribute("totalrows");
+        if (newTotalRows != totalRows) {
+            totalRows = newTotalRows;
+            if (tBody != null) {
+                initializedAndAttached = false;
+                initialContentReceived = false;
+                isNewBody = true;
+            }
+        }
+
+        pageLength = uidl.getIntAttribute("pagelength");
+        if (pageLength == 0) {
+            pageLength = totalRows;
+        }
+        firstvisible = uidl.hasVariable("firstvisible") ? uidl
+                .getIntVariable("firstvisible") : 0;
+
+        showRowHeaders = uidl.getBooleanAttribute("rowheaders");
+        showColHeaders = uidl.getBooleanAttribute("colheaders");
+
+        if (uidl.hasAttribute("width")) {
+            width = uidl.getStringAttribute("width");
+        }
+        if (uidl.hasAttribute("height")) {
+            height = uidl.getStringAttribute("height");
+        }
+
+        if (uidl.hasVariable("sortascending")) {
+            sortAscending = uidl.getBooleanVariable("sortascending");
+            sortColumn = uidl.getStringVariable("sortcolumn");
+        }
+
+        if (uidl.hasVariable("selected")) {
+            final Set selectedKeys = uidl
+                    .getStringArrayVariableAsSet("selected");
+            selectedRowKeys.clear();
+            for (final Iterator it = selectedKeys.iterator(); it.hasNext();) {
+                selectedRowKeys.add(it.next());
+            }
+        }
+
+        if (uidl.hasAttribute("selectmode")) {
+            if (uidl.getBooleanAttribute("readonly")) {
+                selectMode = Table.SELECT_MODE_NONE;
+            } else if (uidl.getStringAttribute("selectmode").equals("multi")) {
+                selectMode = Table.SELECT_MODE_MULTI;
+            } else if (uidl.getStringAttribute("selectmode").equals("single")) {
+                selectMode = Table.SELECT_MODE_SINGLE;
+            } else {
+                selectMode = Table.SELECT_MODE_NONE;
+            }
+        }
+
+        if (uidl.hasVariable("columnorder")) {
+            columnReordering = true;
+            columnOrder = uidl.getStringArrayVariable("columnorder");
+        }
+
+        if (uidl.hasVariable("collapsedcolumns")) {
+            tHead.setColumnCollapsingAllowed(true);
+            collapsedColumns = uidl
+                    .getStringArrayVariableAsSet("collapsedcolumns");
+        } else {
+            tHead.setColumnCollapsingAllowed(false);
+        }
+
+        UIDL rowData = null;
+        for (final Iterator it = uidl.getChildIterator(); it.hasNext();) {
+            final UIDL c = (UIDL) it.next();
+            if (c.getTag().equals("rows")) {
+                rowData = c;
+            } else if (c.getTag().equals("actions")) {
+                updateActionMap(c);
+            } else if (c.getTag().equals("visiblecolumns")) {
+                tHead.updateCellsFromUIDL(c);
+            }
+        }
+        updateHeader(uidl.getStringArrayAttribute("vcolorder"));
+
+        if (initializedAndAttached) {
+            updateBody(rowData, uidl.getIntAttribute("firstrow"), uidl
+                    .getIntAttribute("rows"));
+        } else {
+            if (tBody != null) {
+                tBody.removeFromParent();
+                client.unregisterChildPaintables(tBody);
+            }
+            tBody = new IScrollTableBody();
+
+            tBody.renderInitialRows(rowData, uidl.getIntAttribute("firstrow"),
+                    uidl.getIntAttribute("rows"));
+            bodyContainer.add(tBody);
+            initialContentReceived = true;
+            if (isAttached()) {
+                sizeInit();
+            }
+        }
+        hideScrollPositionAnnotation();
+    }
+
+    private void updateActionMap(UIDL c) {
+        final Iterator it = c.getChildIterator();
+        while (it.hasNext()) {
+            final UIDL action = (UIDL) it.next();
+            final String key = action.getStringAttribute("key");
+            final String caption = action.getStringAttribute("caption");
+            actionMap.put(key + "_c", caption);
+            if (action.hasAttribute("icon")) {
+                // TODO need some uri handling ??
+                actionMap.put(key + "_i", client.translateToolkitUri(action
+                        .getStringAttribute("icon")));
+            }
+        }
+
+    }
+
+    public String getActionCaption(String actionKey) {
+        return (String) actionMap.get(actionKey + "_c");
+    }
+
+    public String getActionIcon(String actionKey) {
+        return (String) actionMap.get(actionKey + "_i");
+    }
+
+    private void updateHeader(String[] strings) {
+        if (strings == null) {
+            return;
+        }
+
+        int visibleCols = strings.length;
+        int colIndex = 0;
+        if (showRowHeaders) {
+            tHead.enableColumn("0", colIndex);
+            visibleCols++;
+            visibleColOrder = new String[visibleCols];
+            visibleColOrder[colIndex] = "0";
+            colIndex++;
+        } else {
+            visibleColOrder = new String[visibleCols];
+            tHead.removeCell("0");
+        }
+
+        int i;
+        for (i = 0; i < strings.length; i++) {
+            final String cid = strings[i];
+            visibleColOrder[colIndex] = cid;
+            tHead.enableColumn(cid, colIndex);
+            colIndex++;
+        }
+
+        tHead.setVisible(showColHeaders);
+
+    }
+
+    /**
+     * @param uidl
+     *                which contains row data
+     * @param firstRow
+     *                first row in data set
+     * @param reqRows
+     *                amount of rows in data set
+     */
+    private void updateBody(UIDL uidl, int firstRow, int reqRows) {
+        if (uidl == null || reqRows < 1) {
+            // container is empty, remove possibly existing rows
+            if (firstRow < 0) {
+                while (tBody.getLastRendered() > tBody.firstRendered) {
+                    tBody.unlinkRow(false);
+                }
+                tBody.unlinkRow(false);
+            }
+            return;
+        }
+
+        tBody.renderRows(uidl, firstRow, reqRows);
+
+        final int optimalFirstRow = (int) (firstRowInViewPort - pageLength
+                * CACHE_RATE);
+        boolean cont = true;
+        while (cont && tBody.getLastRendered() > optimalFirstRow
+                && tBody.getFirstRendered() < optimalFirstRow) {
+            // client.console.log("removing row from start");
+            cont = tBody.unlinkRow(true);
+        }
+        final int optimalLastRow = (int) (firstRowInViewPort + pageLength + pageLength
+                * CACHE_RATE);
+        cont = true;
+        while (cont && tBody.getLastRendered() > optimalLastRow) {
+            // client.console.log("removing row from the end");
+            cont = tBody.unlinkRow(false);
+        }
+        tBody.fixSpacers();
+
+    }
+
+    /**
+     * Gives correct column index for given column key ("cid" in UIDL).
+     * 
+     * @param colKey
+     * @return column index of visible columns, -1 if column not visible
+     */
+    private int getColIndexByKey(String colKey) {
+        // return 0 if asked for rowHeaders
+        if ("0".equals(colKey)) {
+            return 0;
+        }
+        for (int i = 0; i < visibleColOrder.length; i++) {
+            if (visibleColOrder[i].equals(colKey)) {
+                return i;
+            }
+        }
+        return -1;
+    }
+
+    private boolean isCollapsedColumn(String colKey) {
+        if (collapsedColumns == null) {
+            return false;
+        }
+        if (collapsedColumns.contains(colKey)) {
+            return true;
+        }
+        return false;
+    }
+
+    private String getColKeyByIndex(int index) {
+        return tHead.getHeaderCell(index).getColKey();
+    }
+
+    private void setColWidth(int colIndex, int w) {
+        final HeaderCell cell = tHead.getHeaderCell(colIndex);
+        cell.setWidth(w);
+        tBody.setColWidth(colIndex, w);
+    }
+
+    private int getColWidth(String colKey) {
+        return tHead.getHeaderCell(colKey).getWidth();
+    }
+
+    private IScrollTableRow getRenderedRowByKey(String key) {
+        final Iterator it = tBody.iterator();
+        IScrollTableRow r = null;
+        while (it.hasNext()) {
+            r = (IScrollTableRow) it.next();
+            if (r.getKey().equals(key)) {
+                return r;
+            }
+        }
+        return null;
+    }
+
+    private void reOrderColumn(String columnKey, int newIndex) {
+
+        final int oldIndex = getColIndexByKey(columnKey);
+
+        // Change header order
+        tHead.moveCell(oldIndex, newIndex);
+
+        // Change body order
+        tBody.moveCol(oldIndex, newIndex);
+
+        /*
+         * Build new columnOrder and update it to server Note that columnOrder
+         * also contains collapsed columns so we cannot directly build it from
+         * cells vector Loop the old columnOrder and append in order to new
+         * array unless on moved columnKey. On new index also put the moved key
+         * i == index on columnOrder, j == index on newOrder
+         */
+        final String oldKeyOnNewIndex = visibleColOrder[newIndex];
+        if (showRowHeaders) {
+            newIndex--; // columnOrder don't have rowHeader
+        }
+        // add back hidden rows,
+        for (int i = 0; i < columnOrder.length; i++) {
+            if (columnOrder[i].equals(oldKeyOnNewIndex)) {
+                break; // break loop at target
+            }
+            if (isCollapsedColumn(columnOrder[i])) {
+                newIndex++;
+            }
+        }
+        // finally we can build the new columnOrder for server
+        final String[] newOrder = new String[columnOrder.length];
+        for (int i = 0, j = 0; j < newOrder.length; i++) {
+            if (j == newIndex) {
+                newOrder[j] = columnKey;
+                j++;
+            }
+            if (i == columnOrder.length) {
+                break;
+            }
+            if (columnOrder[i].equals(columnKey)) {
+                continue;
+            }
+            newOrder[j] = columnOrder[i];
+            j++;
+        }
+        columnOrder = newOrder;
+        // also update visibleColumnOrder
+        int i = showRowHeaders ? 1 : 0;
+        for (int j = 0; j < newOrder.length; j++) {
+            final String cid = newOrder[j];
+            if (!isCollapsedColumn(cid)) {
+                visibleColOrder[i++] = cid;
+            }
+        }
+        client.updateVariable(paintableId, "columnorder", columnOrder, false);
+    }
+
+    protected void onAttach() {
+        super.onAttach();
+        if (initialContentReceived) {
+            sizeInit();
+        }
+    }
+
+    protected void onDetach() {
+        rowRequestHandler.cancel();
+        super.onDetach();
+        // ensure that scrollPosElement will be detached
+        if (scrollPositionElement != null) {
+            final Element parent = DOM.getParent(scrollPositionElement);
+            if (parent != null) {
+                DOM.removeChild(parent, scrollPositionElement);
+            }
+        }
+    }
+
+    /**
+     * Run only once when component is attached and received its initial
+     * content. This function : * Syncs headers and bodys "natural widths and
+     * saves the values. * Sets proper width and height * Makes deferred request
+     * to get some cache rows
+     */
+    private void sizeInit() {
+        /*
+         * We will use browsers table rendering algorithm to find proper column
+         * widths. If content and header take less space than available, we will
+         * divide extra space relatively to each column which has not width set.
+         * 
+         * Overflow pixels are added to last column.
+         * 
+         */
+
+        Iterator headCells = tHead.iterator();
+        int i = 0;
+        int totalExplicitColumnsWidths = 0;
+        int total = 0;
+
+        final int[] widths = new int[tHead.visibleCells.size()];
+
+        if (width == null) {
+            // if this is a re-init, remove old manually fixed size
+            bodyContainer.setWidth("");
+            tHead.setWidth("");
+            super.setWidth("");
+        }
+
+        tHead.enableBrowserIntelligence();
+        // first loop: collect natural widths
+        while (headCells.hasNext()) {
+            final HeaderCell hCell = (HeaderCell) headCells.next();
+            int w = hCell.getWidth();
+            if (w > 0) {
+                // server has defined column width explicitly
+                totalExplicitColumnsWidths += w;
+            } else {
+                final int hw = hCell.getOffsetWidth();
+                final int cw = tBody.getColWidth(i);
+                w = (hw > cw ? hw : cw) + IScrollTableBody.CELL_EXTRA_WIDTH;
+            }
+            widths[i] = w;
+            total += w;
+            i++;
+        }
+
+        tHead.disableBrowserIntelligence();
+
+        if (height == null) {
+            bodyContainer.setHeight((tBody.getRowHeight() * pageLength) + "px");
+        } else {
+            mySetHeight(height);
+            iLayout();
+        }
+
+        if (width == null) {
+            int w = total;
+            w += getScrollbarWidth();
+            bodyContainer.setWidth(w + "px");
+            tHead.setWidth(w + "px");
+            super.setWidth(w + "px");
+        } else {
+            if (width.indexOf("px") > 0) {
+                bodyContainer.setWidth(width);
+                tHead.setWidth(width);
+                super.setWidth(width);
+            } else if (width.indexOf("%") > 0) {
+                if (!width.equals("100%")) {
+                    super.setWidth(width);
+                }
+                // contained blocks are relatively to container element
+                bodyContainer.setWidth("100%");
+                tHead.setWidth("100%");
+
+            }
+        }
+
+        int availW = tBody.getAvailableWidth();
+        // Hey IE, are you really sure about this?
+        availW = tBody.getAvailableWidth();
+
+        if (availW > total) {
+            // natural size is smaller than available space
+            final int extraSpace = availW - total;
+            final int totalWidthR = total - totalExplicitColumnsWidths;
+            if (totalWidthR > 0) {
+                // now we will share this sum relatively to those without
+                // explicit width
+                headCells = tHead.iterator();
+                i = 0;
+                HeaderCell hCell;
+                while (headCells.hasNext()) {
+                    hCell = (HeaderCell) headCells.next();
+                    if (hCell.getWidth() == -1) {
+                        int w = widths[i];
+                        final int newSpace = extraSpace * w / totalWidthR;
+                        w += newSpace;
+                        widths[i] = w;
+                    }
+                    i++;
+                }
+            }
+        } else {
+            // bodys size will be more than available and scrollbar will appear
+        }
+
+        // last loop: set possibly modified values or reset if new tBody
+        i = 0;
+        headCells = tHead.iterator();
+        while (headCells.hasNext()) {
+            final HeaderCell hCell = (HeaderCell) headCells.next();
+            if (isNewBody || hCell.getWidth() == -1) {
+                final int w = widths[i];
+                setColWidth(i, w);
+            }
+            i++;
+        }
+
+        isNewBody = false;
+
+        if (firstvisible > 0) {
+            // Deferred due some Firefox oddities. IE & Safari could survive
+            // without
+            DeferredCommand.addCommand(new Command() {
+                public void execute() {
+                    bodyContainer.setScrollPosition(firstvisible
+                            * tBody.getRowHeight());
+                    firstRowInViewPort = firstvisible;
+                }
+            });
+        }
+
+        if (enabled) {
+            // Do we need cache rows
+            if (tBody.getLastRendered() + 1 < firstRowInViewPort + pageLength
+                    + CACHE_REACT_RATE * pageLength) {
+                DeferredCommand.addCommand(new Command() {
+                    public void execute() {
+                        if (totalRows - 1 > tBody.getLastRendered()) {
+                            // fetch cache rows
+                            rowRequestHandler.setReqFirstRow(tBody
+                                    .getLastRendered() + 1);
+                            rowRequestHandler
+                                    .setReqRows((int) (pageLength * CACHE_RATE));
+                            rowRequestHandler.deferRowFetch(1);
+                        }
+                    }
+                });
+            }
+        }
+        initializedAndAttached = true;
+    }
+
+    public void iLayout() {
+        if (height != null) {
+            if (height.equals("100%")) {
+                /*
+                 * We define height in pixels with 100% not to include borders
+                 * which is what users usually want. So recalculate pixels via
+                 * setHeight.
+                 */
+                mySetHeight(height);
+            }
+
+            int contentH = (DOM.getElementPropertyInt(getElement(),
+                    "clientHeight") - tHead.getOffsetHeight());
+            if (contentH < 0) {
+                contentH = 0;
+            }
+            bodyContainer.setHeight(contentH + "px");
+        }
+    }
+
+    private int getScrollbarWidth() {
+        return bodyContainer.getOffsetWidth()
+                - DOM.getElementPropertyInt(bodyContainer.getElement(),
+                        "clientWidth");
+    }
+
+    /**
+     * This method has logic which rows needs to be requested from server when
+     * user scrolls
+     */
+    public void onScroll(Widget widget, int scrollLeft, int scrollTop) {
+        if (!initializedAndAttached) {
+            return;
+        }
+        if (!enabled) {
+            bodyContainer.setScrollPosition(firstRowInViewPort
+                    * tBody.getRowHeight());
+            return;
+        }
+
+        rowRequestHandler.cancel();
+
+        // fix headers horizontal scrolling
+        tHead.setHorizontalScrollPosition(scrollLeft);
+
+        firstRowInViewPort = (int) Math.ceil(scrollTop
+                / (double) tBody.getRowHeight());
+        // ApplicationConnection.getConsole().log(
+        // "At scrolltop: " + scrollTop + " At row " + firstRowInViewPort);
+
+        int postLimit = (int) (firstRowInViewPort + pageLength + pageLength
+                * CACHE_REACT_RATE);
+        if (postLimit > totalRows - 1) {
+            postLimit = totalRows - 1;
+        }
+        int preLimit = (int) (firstRowInViewPort - pageLength
+                * CACHE_REACT_RATE);
+        if (preLimit < 0) {
+            preLimit = 0;
+        }
+        final int lastRendered = tBody.getLastRendered();
+        final int firstRendered = tBody.getFirstRendered();
+
+        if (postLimit <= lastRendered && preLimit >= firstRendered) {
+            client.updateVariable(paintableId, "firstvisible",
+                    firstRowInViewPort, false);
+            return; // scrolled withing "non-react area"
+        }
+
+        if (firstRowInViewPort - pageLength * CACHE_RATE > lastRendered
+                || firstRowInViewPort + pageLength + pageLength * CACHE_RATE < firstRendered) {
+            // need a totally new set
+            // ApplicationConnection.getConsole().log(
+            // "Table: need a totally new set");
+            rowRequestHandler
+                    .setReqFirstRow((int) (firstRowInViewPort - pageLength
+                            * CACHE_RATE));
+            int last = firstRowInViewPort + (int) CACHE_RATE * pageLength
+                    + pageLength;
+            if (last > totalRows) {
+                last = totalRows - 1;
+            }
+            rowRequestHandler.setReqRows(last
+                    - rowRequestHandler.getReqFirstRow() + 1);
+            rowRequestHandler.deferRowFetch();
+            return;
+        }
+        if (preLimit < firstRendered) {
+            // need some rows to the beginning of the rendered area
+            // ApplicationConnection
+            // .getConsole()
+            // .log(
+            // "Table: need some rows to the beginning of the rendered area");
+            rowRequestHandler
+                    .setReqFirstRow((int) (firstRowInViewPort - pageLength
+                            * CACHE_RATE));
+            rowRequestHandler.setReqRows(firstRendered
+                    - rowRequestHandler.getReqFirstRow());
+            rowRequestHandler.deferRowFetch();
+
+            return;
+        }
+        if (postLimit > lastRendered) {
+            // need some rows to the end of the rendered area
+            // ApplicationConnection.getConsole().log(
+            // "need some rows to the end of the rendered area");
+            rowRequestHandler.setReqFirstRow(lastRendered + 1);
+            rowRequestHandler.setReqRows((int) ((firstRowInViewPort
+                    + pageLength + pageLength * CACHE_RATE) - lastRendered));
+            rowRequestHandler.deferRowFetch();
+        }
+
+    }
+
+    private void announceScrollPosition() {
+        if (scrollPositionElement == null) {
+            scrollPositionElement = DOM.createDiv();
+            DOM.setElementProperty(scrollPositionElement, "className",
+                    "i-table-scrollposition");
+            DOM.appendChild(getElement(), scrollPositionElement);
+        }
+
+        DOM.setStyleAttribute(scrollPositionElement, "position", "absolute");
+        DOM.setStyleAttribute(scrollPositionElement, "marginLeft", (DOM
+                .getElementPropertyInt(getElement(), "offsetWidth") / 2 - 80)
+                + "px");
+        DOM.setStyleAttribute(scrollPositionElement, "marginTop", -(DOM
+                .getElementPropertyInt(getElement(), "offsetHeight") - 2)
+                + "px");
+
+        // indexes go from 1-totalRows, as rowheaders in index-mode indicate
+        int last = (firstRowInViewPort + (bodyContainer.getOffsetHeight() / tBody
+                .getRowHeight()));
+        if (last > totalRows) {
+            last = totalRows;
+        }
+        DOM.setInnerHTML(scrollPositionElement, "<span>"
+                + (firstRowInViewPort + 1) + " &ndash; " + last + "..."
+                + "</span>");
+        DOM.setStyleAttribute(scrollPositionElement, "display", "block");
+    }
+
+    private void hideScrollPositionAnnotation() {
+        if (scrollPositionElement != null) {
+            DOM.setStyleAttribute(scrollPositionElement, "display", "none");
+        }
+    }
+
+    private class RowRequestHandler extends Timer {
+
+        private int reqFirstRow = 0;
+        private int reqRows = 0;
+
+        public void deferRowFetch() {
+            deferRowFetch(250);
+        }
+
+        public void deferRowFetch(int msec) {
+            if (reqRows > 0 && reqFirstRow < totalRows) {
+                schedule(msec);
+
+                // tell scroll position to user if currently "visible" rows are
+                // not rendered
+                if ((firstRowInViewPort + pageLength > tBody.getLastRendered())
+                        || (firstRowInViewPort < tBody.getFirstRendered())) {
+                    announceScrollPosition();
+                } else {
+                    hideScrollPositionAnnotation();
+                }
+            }
+        }
+
+        public void setReqFirstRow(int reqFirstRow) {
+            if (reqFirstRow < 0) {
+                reqFirstRow = 0;
+            } else if (reqFirstRow >= totalRows) {
+                reqFirstRow = totalRows - 1;
+            }
+            this.reqFirstRow = reqFirstRow;
+        }
+
+        public void setReqRows(int reqRows) {
+            this.reqRows = reqRows;
+        }
+
+        public void run() {
+            ApplicationConnection.getConsole().log(
+                    "Getting " + reqRows + " rows from " + reqFirstRow);
+
+            int firstToBeRendered = tBody.firstRendered;
+            if (reqFirstRow < firstToBeRendered) {
+                firstToBeRendered = reqFirstRow;
+            } else if (firstRowInViewPort - (int) (CACHE_RATE * pageLength) > firstToBeRendered) {
+                firstToBeRendered = firstRowInViewPort
+                        - (int) (CACHE_RATE * pageLength);
+                if (firstToBeRendered < 0) {
+                    firstToBeRendered = 0;
+                }
+            }
+
+            int lastToBeRendered = tBody.lastRendered;
+
+            if (reqFirstRow + reqRows - 1 > lastToBeRendered) {
+                lastToBeRendered = reqFirstRow + reqRows - 1;
+            } else if (firstRowInViewPort + pageLength + pageLength
+                    * CACHE_RATE < lastToBeRendered) {
+                lastToBeRendered = (firstRowInViewPort + pageLength + (int) (pageLength * CACHE_RATE));
+                if (lastToBeRendered >= totalRows) {
+                    lastToBeRendered = totalRows - 1;
+                }
+            }
+
+            client.updateVariable(paintableId, "firstToBeRendered",
+                    firstToBeRendered, false);
+
+            client.updateVariable(paintableId, "lastToBeRendered",
+                    lastToBeRendered, false);
+
+            client.updateVariable(paintableId, "firstvisible",
+                    firstRowInViewPort, false);
+            client.updateVariable(paintableId, "reqfirstrow", reqFirstRow,
+                    false);
+            client.updateVariable(paintableId, "reqrows", reqRows, true);
+        }
+
+        public int getReqFirstRow() {
+            return reqFirstRow;
+        }
+
+        public int getReqRows() {
+            return reqRows;
+        }
+
+        /**
+         * Sends request to refresh content at this position.
+         */
+        public void refreshContent() {
+            int first = (int) (firstRowInViewPort - pageLength * CACHE_RATE);
+            int reqRows = (int) (2 * pageLength * CACHE_RATE + pageLength);
+            if (first < 0) {
+                reqRows = reqRows + first;
+                first = 0;
+            }
+            setReqFirstRow(first);
+            setReqRows(reqRows);
+            run();
+        }
+    }
+
+    public class HeaderCell extends Widget {
+
+        private static final int DRAG_WIDGET_WIDTH = 4;
+
+        private static final int MINIMUM_COL_WIDTH = 20;
+
+        Element td = DOM.createTD();
+
+        Element captionContainer = DOM.createDiv();
+
+        Element colResizeWidget = DOM.createDiv();
+
+        Element floatingCopyOfHeaderCell;
+
+        private boolean sortable = false;
+        private final String cid;
+        private boolean dragging;
+
+        private int dragStartX;
+        private int colIndex;
+        private int originalWidth;
+
+        private boolean isResizing;
+
+        private int headerX;
+
+        private boolean moved;
+
+        private int closestSlot;
+
+        private int width = -1;
+
+        private char align = ALIGN_LEFT;
+
+        public void setSortable(boolean b) {
+            sortable = b;
+        }
+
+        public HeaderCell(String colId, String headerText) {
+            cid = colId;
+
+            DOM.setElementProperty(colResizeWidget, "className", CLASSNAME
+                    + "-resizer");
+            DOM.setStyleAttribute(colResizeWidget, "width", DRAG_WIDGET_WIDTH
+                    + "px");
+            DOM.sinkEvents(colResizeWidget, Event.MOUSEEVENTS);
+
+            setText(headerText);
+
+            DOM.appendChild(td, colResizeWidget);
+
+            DOM.setElementProperty(captionContainer, "className", CLASSNAME
+                    + "-caption-container");
+
+            // ensure no clipping initially (problem on column additions)
+            DOM.setStyleAttribute(captionContainer, "overflow", "visible");
+
+            DOM.sinkEvents(captionContainer, Event.MOUSEEVENTS);
+
+            DOM.appendChild(td, captionContainer);
+
+            DOM.sinkEvents(td, Event.MOUSEEVENTS);
+
+            setElement(td);
+        }
+
+        public void setWidth(int w) {
+            if (width == -1) {
+                // go to default mode, clip content if necessary
+                DOM.setStyleAttribute(captionContainer, "overflow", "");
+            }
+            width = w;
+            DOM.setStyleAttribute(captionContainer, "width", (w
+                    - DRAG_WIDGET_WIDTH - 4)
+                    + "px");
+            setWidth(w + "px");
+        }
+
+        public int getWidth() {
+            return width;
+        }
+
+        public void setText(String headerText) {
+            DOM.setInnerHTML(captionContainer, headerText);
+        }
+
+        public String getColKey() {
+            return cid;
+        }
+
+        private void setSorted(boolean sorted) {
+            if (sorted) {
+                if (sortAscending) {
+                    this.setStyleName(CLASSNAME + "-header-cell-asc");
+                } else {
+                    this.setStyleName(CLASSNAME + "-header-cell-desc");
+                }
+            } else {
+                this.setStyleName(CLASSNAME + "-header-cell");
+            }
+        }
+
+        /**
+         * Handle column reordering.
+         */
+        public void onBrowserEvent(Event event) {
+            if (enabled) {
+                if (isResizing
+                        || DOM.compare(DOM.eventGetTarget(event),
+                                colResizeWidget)) {
+                    onResizeEvent(event);
+                } else {
+                    handleCaptionEvent(event);
+                }
+            }
+        }
+
+        private void createFloatingCopy() {
+            floatingCopyOfHeaderCell = DOM.createDiv();
+            DOM.setInnerHTML(floatingCopyOfHeaderCell, DOM.getInnerHTML(td));
+            floatingCopyOfHeaderCell = DOM
+                    .getChild(floatingCopyOfHeaderCell, 1);
+            DOM.setElementProperty(floatingCopyOfHeaderCell, "className",
+                    CLASSNAME + "-header-drag");
+            updateFloatingCopysPosition(DOM.getAbsoluteLeft(td), DOM
+                    .getAbsoluteTop(td));
+            DOM.appendChild(RootPanel.get().getElement(),
+                    floatingCopyOfHeaderCell);
+        }
+
+        private void updateFloatingCopysPosition(int x, int y) {
+            x -= DOM.getElementPropertyInt(floatingCopyOfHeaderCell,
+                    "offsetWidth") / 2;
+            DOM.setStyleAttribute(floatingCopyOfHeaderCell, "left", x + "px");
+            if (y > 0) {
+                DOM.setStyleAttribute(floatingCopyOfHeaderCell, "top", (y + 7)
+                        + "px");
+            }
+        }
+
+        private void hideFloatingCopy() {
+            DOM.removeChild(RootPanel.get().getElement(),
+                    floatingCopyOfHeaderCell);
+            floatingCopyOfHeaderCell = null;
+        }
+
+        protected void handleCaptionEvent(Event event) {
+            switch (DOM.eventGetType(event)) {
+            case Event.ONMOUSEDOWN:
+                ApplicationConnection.getConsole().log(
+                        "HeaderCaption: mouse down");
+                if (columnReordering) {
+                    dragging = true;
+                    moved = false;
+                    colIndex = getColIndexByKey(cid);
+                    DOM.setCapture(getElement());
+                    headerX = tHead.getAbsoluteLeft();
+                    ApplicationConnection
+                            .getConsole()
+                            .log(
+                                    "HeaderCaption: Caption set to capture mouse events");
+                    DOM.eventPreventDefault(event); // prevent selecting text
+                }
+                break;
+            case Event.ONMOUSEUP:
+                ApplicationConnection.getConsole()
+                        .log("HeaderCaption: mouseUP");
+                if (columnReordering) {
+                    dragging = false;
+                    DOM.releaseCapture(getElement());
+                    ApplicationConnection.getConsole().log(
+                            "HeaderCaption: Stopped column reordering");
+                    if (moved) {
+                        hideFloatingCopy();
+                        tHead.removeSlotFocus();
+                        if (closestSlot != colIndex
+                                && closestSlot != (colIndex + 1)) {
+                            if (closestSlot > colIndex) {
+                                reOrderColumn(cid, closestSlot - 1);
+                            } else {
+                                reOrderColumn(cid, closestSlot);
+                            }
+                        }
+                    }
+                }
+
+                if (!moved) {
+                    // mouse event was a click to header -> sort column
+                    if (sortable) {
+                        if (sortColumn.equals(cid)) {
+                            // just toggle order
+                            client.updateVariable(paintableId, "sortascending",
+                                    !sortAscending, false);
+                        } else {
+                            // set table scrolled by this column
+                            client.updateVariable(paintableId, "sortcolumn",
+                                    cid, false);
+                        }
+                        // get also cache columns at the same request
+                        bodyContainer.setScrollPosition(0);
+                        firstvisible = 0;
+                        rowRequestHandler.setReqFirstRow(0);
+                        rowRequestHandler.setReqRows((int) (2 * pageLength
+                                * CACHE_RATE + pageLength));
+                        rowRequestHandler.deferRowFetch();
+                    }
+                    break;
+                }
+                break;
+            case Event.ONMOUSEMOVE:
+                if (dragging) {
+                    ApplicationConnection.getConsole().log(
+                            "HeaderCaption: Dragging column, optimal index...");
+                    if (!moved) {
+                        createFloatingCopy();
+                        moved = true;
+                    }
+                    final int x = DOM.eventGetClientX(event)
+                            + DOM.getElementPropertyInt(tHead.hTableWrapper,
+                                    "scrollLeft");
+                    int slotX = headerX;
+                    closestSlot = colIndex;
+                    int closestDistance = -1;
+                    int start = 0;
+                    if (showRowHeaders) {
+                        start++;
+                    }
+                    final int visibleCellCount = tHead.getVisibleCellCount();
+                    for (int i = start; i <= visibleCellCount; i++) {
+                        if (i > 0) {
+                            final String colKey = getColKeyByIndex(i - 1);
+                            slotX += getColWidth(colKey);
+                        }
+                        final int dist = Math.abs(x - slotX);
+                        if (closestDistance == -1 || dist < closestDistance) {
+                            closestDistance = dist;
+                            closestSlot = i;
+                        }
+                    }
+                    tHead.focusSlot(closestSlot);
+
+                    updateFloatingCopysPosition(DOM.eventGetClientX(event), -1);
+                    ApplicationConnection.getConsole().log("" + closestSlot);
+                }
+                break;
+            default:
+                break;
+            }
+        }
+
+        private void onResizeEvent(Event event) {
+            switch (DOM.eventGetType(event)) {
+            case Event.ONMOUSEDOWN:
+                isResizing = true;
+                DOM.setCapture(getElement());
+                dragStartX = DOM.eventGetClientX(event);
+                colIndex = getColIndexByKey(cid);
+                originalWidth = getWidth();
+                DOM.eventPreventDefault(event);
+                break;
+            case Event.ONMOUSEUP:
+                isResizing = false;
+                DOM.releaseCapture(getElement());
+                break;
+            case Event.ONMOUSEMOVE:
+                if (isResizing) {
+                    final int deltaX = DOM.eventGetClientX(event) - dragStartX;
+                    if (deltaX == 0) {
+                        return;
+                    }
+
+                    int newWidth = originalWidth + deltaX;
+                    if (newWidth < MINIMUM_COL_WIDTH) {
+                        newWidth = MINIMUM_COL_WIDTH;
+                    }
+                    setColWidth(colIndex, newWidth);
+                }
+                break;
+            default:
+                break;
+            }
+        }
+
+        public String getCaption() {
+            return DOM.getInnerText(captionContainer);
+        }
+
+        public boolean isEnabled() {
+            return getParent() != null;
+        }
+
+        public void setAlign(char c) {
+            if (align != c) {
+                switch (c) {
+                case ALIGN_CENTER:
+                    DOM.setStyleAttribute(captionContainer, "textAlign",
+                            "center");
+                    break;
+                case ALIGN_RIGHT:
+                    DOM.setStyleAttribute(captionContainer, "textAlign",
+                            "right");
+                    break;
+                default:
+                    DOM.setStyleAttribute(captionContainer, "textAlign", "");
+                    break;
+                }
+            }
+            align = c;
+        }
+
+        public char getAlign() {
+            return align;
+        }
+
+    }
+
+    /**
+     * HeaderCell that is header cell for row headers.
+     * 
+     * Reordering disabled and clicking on it resets sorting.
+     */
+    public class RowHeadersHeaderCell extends HeaderCell {
+
+        RowHeadersHeaderCell() {
+            super("0", "");
+        }
+
+        protected void handleCaptionEvent(Event event) {
+            // NOP: RowHeaders cannot be reordered
+            // TODO It'd be nice to reset sorting here
+        }
+    }
+
+    public class TableHead extends Panel implements ActionOwner {
+
+        private static final int WRAPPER_WIDTH = 9000;
+
+        Vector visibleCells = new Vector();
+
+        HashMap availableCells = new HashMap();
+
+        Element div = DOM.createDiv();
+        Element hTableWrapper = DOM.createDiv();
+        Element hTableContainer = DOM.createDiv();
+        Element table = DOM.createTable();
+        Element headerTableBody = DOM.createTBody();
+        Element tr = DOM.createTR();
+
+        private final Element columnSelector = DOM.createDiv();
+
+        private int focusedSlot = -1;
+
+        public TableHead() {
+            DOM.setStyleAttribute(hTableWrapper, "overflow", "hidden");
+            DOM.setElementProperty(hTableWrapper, "className", CLASSNAME
+                    + "-header");
+
+            // TODO move styles to CSS
+            DOM.setElementProperty(columnSelector, "className", CLASSNAME
+                    + "-column-selector");
+            DOM.setStyleAttribute(columnSelector, "display", "none");
+
+            DOM.appendChild(table, headerTableBody);
+            DOM.appendChild(headerTableBody, tr);
+            DOM.appendChild(hTableContainer, table);
+            DOM.appendChild(hTableWrapper, hTableContainer);
+            DOM.appendChild(div, hTableWrapper);
+            DOM.appendChild(div, columnSelector);
+            setElement(div);
+
+            setStyleName(CLASSNAME + "-header-wrap");
+
+            DOM.sinkEvents(columnSelector, Event.ONCLICK);
+
+            availableCells.put("0", new RowHeadersHeaderCell());
+        }
+
+        public void updateCellsFromUIDL(UIDL uidl) {
+            Iterator it = uidl.getChildIterator();
+            HashSet updated = new HashSet();
+            updated.add("0");
+            while (it.hasNext()) {
+                final UIDL col = (UIDL) it.next();
+                final String cid = col.getStringAttribute("cid");
+                updated.add(cid);
+                HeaderCell c = getHeaderCell(cid);
+                if (c == null) {
+                    c = new HeaderCell(cid, col.getStringAttribute("caption"));
+                    availableCells.put(cid, c);
+                    if (initializedAndAttached) {
+                        // we will need a column width recalculation
+                        initializedAndAttached = false;
+                        initialContentReceived = false;
+                        isNewBody = true;
+                    }
+                } else {
+                    c.setText(col.getStringAttribute("caption"));
+                }
+
+                if (col.hasAttribute("sortable")) {
+                    c.setSortable(true);
+                    if (cid.equals(sortColumn)) {
+                        c.setSorted(true);
+                    } else {
+                        c.setSorted(false);
+                    }
+                }
+                if (col.hasAttribute("align")) {
+                    c.setAlign(col.getStringAttribute("align").charAt(0));
+                }
+                if (col.hasAttribute("width")) {
+                    final String width = col.getStringAttribute("width");
+                    c.setWidth(Integer.parseInt(width));
+                }
+                // TODO icon
+            }
+            // check for orphaned header cells
+            it = availableCells.keySet().iterator();
+            while (it.hasNext()) {
+                String cid = (String) it.next();
+                if (!updated.contains(cid)) {
+                    removeCell(cid);
+                    it.remove();
+                }
+            }
+
+        }
+
+        public void enableColumn(String cid, int index) {
+            final HeaderCell c = getHeaderCell(cid);
+            if (!c.isEnabled() || getHeaderCell(index) != c) {
+                setHeaderCell(index, c);
+                if (c.getWidth() == -1) {
+                    if (initializedAndAttached) {
+                        // column is not drawn before,
+                        // we will need a column width recalculation
+                        initializedAndAttached = false;
+                        initialContentReceived = false;
+                        isNewBody = true;
+                    }
+                }
+            }
+        }
+
+        public int getVisibleCellCount() {
+            return visibleCells.size();
+        }
+
+        public void setHorizontalScrollPosition(int scrollLeft) {
+            DOM.setElementPropertyInt(hTableWrapper, "scrollLeft", scrollLeft);
+        }
+
+        public void setColumnCollapsingAllowed(boolean cc) {
+            if (cc) {
+                DOM.setStyleAttribute(columnSelector, "display", "block");
+            } else {
+                DOM.setStyleAttribute(columnSelector, "display", "none");
+            }
+        }
+
+        public void disableBrowserIntelligence() {
+            DOM.setStyleAttribute(hTableContainer, "width", WRAPPER_WIDTH
+                    + "px");
+        }
+
+        public void enableBrowserIntelligence() {
+            DOM.setStyleAttribute(hTableContainer, "width", "");
+        }
+
+        public void setHeaderCell(int index, HeaderCell cell) {
+            if (cell.isEnabled()) {
+                // we're moving the cell
+                DOM.removeChild(tr, cell.getElement());
+                orphan(cell);
+            }
+            if (index < visibleCells.size()) {
+                // insert to right slot
+                DOM.insertChild(tr, cell.getElement(), index);
+                adopt(cell);
+                visibleCells.insertElementAt(cell, index);
+
+            } else if (index == visibleCells.size()) {
+                // simply append
+                DOM.appendChild(tr, cell.getElement());
+                adopt(cell);
+                visibleCells.add(cell);
+            } else {
+                throw new RuntimeException(
+                        "Header cells must be appended in order");
+            }
+        }
+
+        public HeaderCell getHeaderCell(int index) {
+            if (index < visibleCells.size()) {
+                return (HeaderCell) visibleCells.get(index);
+            } else {
+                return null;
+            }
+        }
+
+        /**
+         * Get's HeaderCell by it's column Key.
+         * 
+         * Note that this returns HeaderCell even if it is currently collapsed.
+         * 
+         * @param cid
+         *                Column key of accessed HeaderCell
+         * @return HeaderCell
+         */
+        public HeaderCell getHeaderCell(String cid) {
+            return (HeaderCell) availableCells.get(cid);
+        }
+
+        public void moveCell(int oldIndex, int newIndex) {
+            final HeaderCell hCell = getHeaderCell(oldIndex);
+            final Element cell = hCell.getElement();
+
+            visibleCells.remove(oldIndex);
+            DOM.removeChild(tr, cell);
+
+            DOM.insertChild(tr, cell, newIndex);
+            visibleCells.insertElementAt(hCell, newIndex);
+        }
+
+        public Iterator iterator() {
+            return visibleCells.iterator();
+        }
+
+        public boolean remove(Widget w) {
+            if (visibleCells.contains(w)) {
+                visibleCells.remove(w);
+                orphan(w);
+                DOM.removeChild(DOM.getParent(w.getElement()), w.getElement());
+                return true;
+            }
+            return false;
+        }
+
+        public void removeCell(String colKey) {
+            final HeaderCell c = getHeaderCell(colKey);
+            remove(c);
+        }
+
+        private void focusSlot(int index) {
+            removeSlotFocus();
+            if (index > 0) {
+                DOM.setElementProperty(DOM.getFirstChild(DOM.getChild(tr,
+                        index - 1)), "className", CLASSNAME + "-resizer "
+                        + CLASSNAME + "-focus-slot-right");
+            } else {
+                DOM.setElementProperty(DOM.getFirstChild(DOM
+                        .getChild(tr, index)), "className", CLASSNAME
+                        + "-resizer " + CLASSNAME + "-focus-slot-left");
+            }
+            focusedSlot = index;
+        }
+
+        private void removeSlotFocus() {
+            if (focusedSlot < 0) {
+                return;
+            }
+            if (focusedSlot == 0) {
+                DOM.setElementProperty(DOM.getFirstChild(DOM.getChild(tr,
+                        focusedSlot)), "className", CLASSNAME + "-resizer");
+            } else if (focusedSlot > 0) {
+                DOM.setElementProperty(DOM.getFirstChild(DOM.getChild(tr,
+                        focusedSlot - 1)), "className", CLASSNAME + "-resizer");
+            }
+            focusedSlot = -1;
+        }
+
+        public void onBrowserEvent(Event event) {
+            if (enabled) {
+                if (DOM.compare(DOM.eventGetTarget(event), columnSelector)) {
+                    final int left = DOM.getAbsoluteLeft(columnSelector);
+                    final int top = DOM.getAbsoluteTop(columnSelector)
+                            + DOM.getElementPropertyInt(columnSelector,
+                                    "offsetHeight");
+                    client.getContextMenu().showAt(this, left, top);
+                }
+            }
+        }
+
+        class VisibleColumnAction extends Action {
+
+            String colKey;
+            private boolean collapsed;
+
+            public VisibleColumnAction(String colKey) {
+                super(IScrollTable.TableHead.this);
+                this.colKey = colKey;
+                caption = tHead.getHeaderCell(colKey).getCaption();
+            }
+
+            public void execute() {
+                client.getContextMenu().hide();
+                // toggle selected column
+                if (collapsedColumns.contains(colKey)) {
+                    collapsedColumns.remove(colKey);
+                } else {
+                    tHead.removeCell(colKey);
+                    collapsedColumns.add(colKey);
+                }
+
+                // update variable to server
+                client.updateVariable(paintableId, "collapsedcolumns",
+                        collapsedColumns.toArray(), false);
+                // let rowRequestHandler determine proper rows
+                rowRequestHandler.refreshContent();
+            }
+
+            public void setCollapsed(boolean b) {
+                collapsed = b;
+            }
+
+            /**
+             * Override default method to distinguish on/off columns
+             */
+            public String getHTML() {
+                final StringBuffer buf = new StringBuffer();
+                if (collapsed) {
+                    buf.append("<span class=\"i-off\">");
+                } else {
+                    buf.append("<span class=\"i-on\">");
+                }
+                buf.append(super.getHTML());
+                buf.append("</span>");
+
+                return buf.toString();
+            }
+
+        }
+
+        /*
+         * Returns columns as Action array for column select popup
+         */
+        public Action[] getActions() {
+            Object[] cols;
+            if (columnReordering) {
+                cols = columnOrder;
+            } else {
+                // if columnReordering is disabled, we need different way to get
+                // all available columns
+                cols = visibleColOrder;
+                cols = new Object[visibleColOrder.length
+                        + collapsedColumns.size()];
+                int i;
+                for (i = 0; i < visibleColOrder.length; i++) {
+                    cols[i] = visibleColOrder[i];
+                }
+                for (final Iterator it = collapsedColumns.iterator(); it
+                        .hasNext();) {
+                    cols[i++] = it.next();
+                }
+            }
+            final Action[] actions = new Action[cols.length];
+
+            for (int i = 0; i < cols.length; i++) {
+                final String cid = (String) cols[i];
+                final HeaderCell c = getHeaderCell(cid);
+                final VisibleColumnAction a = new VisibleColumnAction(c
+                        .getColKey());
+                a.setCaption(c.getCaption());
+                if (!c.isEnabled()) {
+                    a.setCollapsed(true);
+                }
+                actions[i] = a;
+            }
+            return actions;
+        }
+
+        public ApplicationConnection getClient() {
+            return client;
+        }
+
+        public String getPaintableId() {
+            return paintableId;
+        }
+
+        /**
+         * Returns column alignments for visible columns
+         */
+        public char[] getColumnAlignments() {
+            final Iterator it = visibleCells.iterator();
+            final char[] aligns = new char[visibleCells.size()];
+            int colIndex = 0;
+            while (it.hasNext()) {
+                aligns[colIndex++] = ((HeaderCell) it.next()).getAlign();
+            }
+            return aligns;
+        }
+
+    }
+
+    /**
+     * This Panel can only contain IScrollTableRow type of widgets. This
+     * "simulates" very large table, keeping spacers which take room of
+     * unrendered rows.
+     * 
+     */
+    public class IScrollTableBody extends Panel {
+
+        public static final int CELL_EXTRA_WIDTH = 20;
+
+        public static final int DEFAULT_ROW_HEIGHT = 24;
+
+        /**
+         * Amount of padding inside one table cell (this is reduced from the
+         * "cellContent" element's width). You may override this in your own
+         * widgetset.
+         */
+        public static final int CELL_CONTENT_PADDING = 8;
+
+        private int rowHeight = -1;
+
+        private final List renderedRows = new Vector();
+
+        private boolean initDone = false;
+
+        Element preSpacer = DOM.createDiv();
+        Element postSpacer = DOM.createDiv();
+
+        Element container = DOM.createDiv();
+
+        Element tBody = DOM.createTBody();
+        Element table = DOM.createTable();
+
+        private int firstRendered;
+
+        private int lastRendered;
+
+        private char[] aligns;
+
+        IScrollTableBody() {
+            constructDOM();
+            setElement(container);
+        }
+
+        private void constructDOM() {
+            DOM.setElementProperty(table, "className", CLASSNAME + "-table");
+            DOM.setElementProperty(preSpacer, "className", CLASSNAME
+                    + "-row-spacer");
+            DOM.setElementProperty(postSpacer, "className", CLASSNAME
+                    + "-row-spacer");
+
+            DOM.appendChild(table, tBody);
+            DOM.appendChild(container, preSpacer);
+            DOM.appendChild(container, table);
+            DOM.appendChild(container, postSpacer);
+
+        }
+
+        public int getAvailableWidth() {
+            return DOM.getElementPropertyInt(preSpacer, "offsetWidth");
+        }
+
+        public void renderInitialRows(UIDL rowData, int firstIndex, int rows) {
+            firstRendered = firstIndex;
+            lastRendered = firstIndex + rows - 1;
+            final Iterator it = rowData.getChildIterator();
+            aligns = tHead.getColumnAlignments();
+            while (it.hasNext()) {
+                final IScrollTableRow row = new IScrollTableRow((UIDL) it
+                        .next(), aligns);
+                addRow(row);
+            }
+            if (isAttached()) {
+                fixSpacers();
+            }
+        }
+
+        public void renderRows(UIDL rowData, int firstIndex, int rows) {
+            // FIXME REVIEW
+            aligns = tHead.getColumnAlignments();
+            final Iterator it = rowData.getChildIterator();
+            if (firstIndex == lastRendered + 1) {
+                while (it.hasNext()) {
+                    final IScrollTableRow row = createRow((UIDL) it.next());
+                    addRow(row);
+                    lastRendered++;
+                }
+                fixSpacers();
+            } else if (firstIndex + rows == firstRendered) {
+                final IScrollTableRow[] rowArray = new IScrollTableRow[rows];
+                int i = rows;
+                while (it.hasNext()) {
+                    i--;
+                    rowArray[i] = createRow((UIDL) it.next());
+                }
+                for (i = 0; i < rows; i++) {
+                    addRowBeforeFirstRendered(rowArray[i]);
+                    firstRendered--;
+                }
+            } else {
+                // completely new set of rows
+                while (lastRendered + 1 > firstRendered) {
+                    unlinkRow(false);
+                }
+                final IScrollTableRow row = createRow((UIDL) it.next());
+                firstRendered = firstIndex;
+                lastRendered = firstIndex - 1;
+                addRow(row);
+                lastRendered++;
+                setContainerHeight();
+                fixSpacers();
+                while (it.hasNext()) {
+                    addRow(createRow((UIDL) it.next()));
+                    lastRendered++;
+                }
+                fixSpacers();
+                DeferredCommand.addCommand(new Command() {
+                    public void execute() {
+                        // this may be a new set of rows due content change,
+                        // ensure we have proper cache rows
+                        int reactFirstRow = (int) (firstRowInViewPort - pageLength
+                                * CACHE_REACT_RATE);
+                        int reactLastRow = (int) (firstRowInViewPort
+                                + pageLength + pageLength * CACHE_REACT_RATE);
+                        if (reactFirstRow < 0) {
+                            reactFirstRow = 0;
+                        }
+                        if (reactLastRow > totalRows) {
+                            reactLastRow = totalRows - 1;
+                        }
+                        if (reactFirstRow < firstRendered
+                                || reactLastRow > lastRendered) {
+                            // re-fetch full cache area
+                            reactFirstRow = (int) (firstRowInViewPort - pageLength
+                                    * CACHE_RATE);
+                            reactLastRow = (int) (firstRowInViewPort
+                                    + pageLength + pageLength * CACHE_RATE);
+                            if (reactFirstRow < 0) {
+                                reactFirstRow = 0;
+                            }
+                            if (reactLastRow > totalRows) {
+                                reactLastRow = totalRows - 1;
+                            }
+                            // fetch some lines before
+                            rowRequestHandler.setReqFirstRow(reactFirstRow);
+                            rowRequestHandler.setReqRows((int) (2 * pageLength
+                                    * CACHE_RATE + pageLength));
+                            rowRequestHandler.deferRowFetch(1);
+                        }
+                    }
+                });
+            }
+        }
+
+        /**
+         * This method is used to instantiate new rows for this table. It
+         * automatically sets correct widths to rows cells and assigns correct
+         * client reference for child widgets.
+         * 
+         * This method can be called only after table has been initialized
+         * 
+         * @param uidl
+         */
+        private IScrollTableRow createRow(UIDL uidl) {
+            final IScrollTableRow row = new IScrollTableRow(uidl, aligns);
+            final int cells = DOM.getChildCount(row.getElement());
+            for (int i = 0; i < cells; i++) {
+                final Element cell = DOM.getChild(row.getElement(), i);
+                final int w = IScrollTable.this
+                        .getColWidth(getColKeyByIndex(i));
+                DOM.setStyleAttribute(DOM.getFirstChild(cell), "width",
+                        (w - CELL_CONTENT_PADDING) + "px");
+                DOM.setStyleAttribute(cell, "width", w + "px");
+            }
+            return row;
+        }
+
+        private void addRowBeforeFirstRendered(IScrollTableRow row) {
+            IScrollTableRow first = null;
+            if (renderedRows.size() > 0) {
+                first = (IScrollTableRow) renderedRows.get(0);
+            }
+            if (first != null && first.getStyleName().indexOf("-odd") == -1) {
+                row.setStyleName(CLASSNAME + "-row-odd");
+            }
+            if (row.isSelected()) {
+                row.addStyleName("i-selected");
+            }
+            DOM.insertChild(tBody, row.getElement(), 0);
+            adopt(row);
+            renderedRows.add(0, row);
+        }
+
+        private void addRow(IScrollTableRow row) {
+            IScrollTableRow last = null;
+            if (renderedRows.size() > 0) {
+                last = (IScrollTableRow) renderedRows
+                        .get(renderedRows.size() - 1);
+            }
+            if (last != null && last.getStyleName().indexOf("-odd") == -1) {
+                row.setStyleName(CLASSNAME + "-row-odd");
+            }
+            if (row.isSelected()) {
+                row.addStyleName("i-selected");
+            }
+            DOM.appendChild(tBody, row.getElement());
+            adopt(row);
+            renderedRows.add(row);
+        }
+
+        public Iterator iterator() {
+            return renderedRows.iterator();
+        }
+
+        /**
+         * @return false if couldn't remove row
+         */
+        public boolean unlinkRow(boolean fromBeginning) {
+            if (lastRendered - firstRendered < 0) {
+                return false;
+            }
+            int index;
+            if (fromBeginning) {
+                index = 0;
+                firstRendered++;
+            } else {
+                index = renderedRows.size() - 1;
+                lastRendered--;
+            }
+            final IScrollTableRow toBeRemoved = (IScrollTableRow) renderedRows
+                    .get(index);
+            client.unregisterChildPaintables(toBeRemoved);
+            DOM.removeChild(tBody, toBeRemoved.getElement());
+            orphan(toBeRemoved);
+            renderedRows.remove(index);
+            fixSpacers();
+            return true;
+        }
+
+        public boolean remove(Widget w) {
+            throw new UnsupportedOperationException();
+        }
+
+        protected void onAttach() {
+            super.onAttach();
+            setContainerHeight();
+        }
+
+        /**
+         * Fix container blocks height according to totalRows to avoid
+         * "bouncing" when scrolling
+         */
+        private void setContainerHeight() {
+            fixSpacers();
+            DOM.setStyleAttribute(container, "height", totalRows
+                    * getRowHeight() + "px");
+        }
+
+        private void fixSpacers() {
+            int prepx = getRowHeight() * firstRendered;
+            if (prepx < 0) {
+                prepx = 0;
+            }
+            DOM.setStyleAttribute(preSpacer, "height", prepx + "px");
+            int postpx = getRowHeight() * (totalRows - 1 - lastRendered);
+            if (postpx < 0) {
+                postpx = 0;
+            }
+            DOM.setStyleAttribute(postSpacer, "height", postpx + "px");
+        }
+
+        public int getRowHeight() {
+            if (initDone) {
+                return rowHeight;
+            } else {
+                if (DOM.getChildCount(tBody) > 0) {
+                    rowHeight = DOM
+                            .getElementPropertyInt(tBody, "offsetHeight")
+                            / DOM.getChildCount(tBody);
+                } else {
+                    return DEFAULT_ROW_HEIGHT;
+                }
+                initDone = true;
+                return rowHeight;
+            }
+        }
+
+        public int getColWidth(int i) {
+            if (initDone) {
+                final Element e = DOM.getChild(DOM.getChild(tBody, 0), i);
+                return DOM.getElementPropertyInt(e, "offsetWidth");
+            } else {
+                return 0;
+            }
+        }
+
+        public void setColWidth(int colIndex, int w) {
+            final int rows = DOM.getChildCount(tBody);
+            for (int i = 0; i < rows; i++) {
+                final Element cell = DOM.getChild(DOM.getChild(tBody, i),
+                        colIndex);
+                DOM.setStyleAttribute(DOM.getFirstChild(cell), "width",
+                        (w - CELL_CONTENT_PADDING) + "px");
+                DOM.setStyleAttribute(cell, "width", w + "px");
+            }
+        }
+
+        public int getLastRendered() {
+            return lastRendered;
+        }
+
+        public int getFirstRendered() {
+            return firstRendered;
+        }
+
+        public void moveCol(int oldIndex, int newIndex) {
+
+            // loop all rows and move given index to its new place
+            final Iterator rows = iterator();
+            while (rows.hasNext()) {
+                final IScrollTableRow row = (IScrollTableRow) rows.next();
+
+                final Element td = DOM.getChild(row.getElement(), oldIndex);
+                DOM.removeChild(row.getElement(), td);
+
+                DOM.insertChild(row.getElement(), td, newIndex);
+
+            }
+
+        }
+
+        public class IScrollTableRow extends Panel implements ActionOwner {
+
+            Vector childWidgets = new Vector();
+            private boolean selected = false;
+            private final int rowKey;
+
+            private String[] actionKeys = null;
+
+            private IScrollTableRow(int rowKey) {
+                this.rowKey = rowKey;
+                setElement(DOM.createElement("tr"));
+                DOM.sinkEvents(getElement(), Event.ONCLICK);
+                attachContextMenuEvent(getElement());
+                setStyleName(CLASSNAME + "-row");
+            }
+
+            protected void onDetach() {
+                Util.removeContextMenuEvent(getElement());
+                super.onDetach();
+            }
+
+            /**
+             * Attaches context menu event handler to given element. Attached
+             * handler fires showContextMenu function.
+             * 
+             * @param el
+             *                element where to attach contenxt menu event
+             */
+            private native void attachContextMenuEvent(Element el)
+            /*-{
+               var row = this;
+               el.oncontextmenu = function(e) {
+                       if(!e)
+                               e = $wnd.event;
+                       row.@com.itmill.toolkit.terminal.gwt.client.ui.IScrollTable.IScrollTableBody.IScrollTableRow::showContextMenu(Lcom/google/gwt/user/client/Event;)(e);
+                       return false;
+               };
+            }-*/;
+
+            public String getKey() {
+                return String.valueOf(rowKey);
+            }
+
+            public IScrollTableRow(UIDL uidl, char[] aligns) {
+                this(uidl.getIntAttribute("key"));
+
+                tHead.getColumnAlignments();
+                int col = 0;
+                // row header
+                if (showRowHeaders) {
+                    addCell(uidl.getStringAttribute("caption"), aligns[col++],
+                            "");
+                }
+
+                if (uidl.hasAttribute("al")) {
+                    actionKeys = uidl.getStringArrayAttribute("al");
+                }
+
+                final Iterator cells = uidl.getChildIterator();
+                while (cells.hasNext()) {
+                    final Object cell = cells.next();
+                    if (cell instanceof String) {
+                        String style = "";
+                        if (uidl.hasAttribute("style-" + col)) {
+                            style = uidl.getStringAttribute("style-" + col);
+                        }
+                        addCell(cell.toString(), aligns[col++], style);
+                    } else {
+                        final Paintable cellContent = client
+                                .getPaintable((UIDL) cell);
+                        (cellContent).updateFromUIDL((UIDL) cell, client);
+                        String style = "";
+                        if (uidl.hasAttribute("style")) {
+                            style = uidl.getStringAttribute("style");
+                        }
+                        addCell((Widget) cellContent, aligns[col++], style);
+                    }
+                }
+                if (uidl.hasAttribute("selected") && !isSelected()) {
+                    toggleSelection();
+                }
+            }
+
+            public void addCell(String text, char align, String style) {
+                // String only content is optimized by not using Label widget
+                final Element td = DOM.createTD();
+                final Element container = DOM.createDiv();
+                String className = CLASSNAME + "-cell-content";
+                if (style != null && !style.equals("")) {
+                    className += " " + CLASSNAME + "-cell-content-" + style;
+                }
+
+                DOM.setElementProperty(container, "className", className);
+                DOM.setInnerHTML(container, text);
+                if (align != ALIGN_LEFT) {
+                    switch (align) {
+                    case ALIGN_CENTER:
+                        DOM.setStyleAttribute(container, "textAlign", "center");
+                        break;
+                    case ALIGN_RIGHT:
+                    default:
+                        DOM.setStyleAttribute(container, "textAlign", "right");
+                        break;
+                    }
+                }
+                DOM.appendChild(td, container);
+                DOM.appendChild(getElement(), td);
+            }
+
+            public void addCell(Widget w, char align, String style) {
+                final Element td = DOM.createTD();
+                final Element container = DOM.createDiv();
+                String className = CLASSNAME + "-cell-content";
+                if (style != null && !style.equals("")) {
+                    className += " " + CLASSNAME + "-cell-content-" + style;
+                }
+                DOM.setElementProperty(container, "className", className);
+                // TODO most components work with this, but not all (e.g.
+                // Select)
+                // Old comment: make widget cells respect align.
+                // text-align:center for IE, margin: auto for others
+                if (align != ALIGN_LEFT) {
+                    switch (align) {
+                    case ALIGN_CENTER:
+                        DOM.setStyleAttribute(container, "textAlign", "center");
+                        break;
+                    case ALIGN_RIGHT:
+                    default:
+                        DOM.setStyleAttribute(container, "textAlign", "right");
+                        break;
+                    }
+                }
+                DOM.appendChild(td, container);
+                DOM.appendChild(getElement(), td);
+                DOM.appendChild(container, w.getElement());
+                adopt(w);
+                childWidgets.add(w);
+            }
+
+            public Iterator iterator() {
+                return childWidgets.iterator();
+            }
+
+            public boolean remove(Widget w) {
+                // TODO Auto-generated method stub
+                return false;
+            }
+
+            /*
+             * React on click that occur on content cells only
+             */
+            public void onBrowserEvent(Event event) {
+                switch (DOM.eventGetType(event)) {
+                case Event.ONCLICK:
+                    final Element tdOrTr = DOM.getParent(DOM
+                            .eventGetTarget(event));
+                    if (DOM.compare(getElement(), tdOrTr)
+                            || DOM.compare(getElement(), DOM.getParent(tdOrTr))) {
+                        if (selectMode > Table.SELECT_MODE_NONE) {
+                            toggleSelection();
+                            client.updateVariable(paintableId, "selected",
+                                    selectedRowKeys.toArray(), immediate);
+                        }
+                    }
+                    break;
+
+                default:
+                    break;
+                }
+                super.onBrowserEvent(event);
+            }
+
+            public void showContextMenu(Event event) {
+                ApplicationConnection.getConsole().log("Context menu");
+                if (enabled && actionKeys != null) {
+                    int left = DOM.eventGetClientX(event);
+                    int top = DOM.eventGetClientY(event);
+                    top += Window.getScrollTop();
+                    left += Window.getScrollLeft();
+                    client.getContextMenu().showAt(this, left, top);
+                }
+            }
+
+            public boolean isSelected() {
+                return selected;
+            }
+
+            private void toggleSelection() {
+                selected = !selected;
+                if (selected) {
+                    if (selectMode == Table.SELECT_MODE_SINGLE) {
+                        deselectAll();
+                    }
+                    selectedRowKeys.add(String.valueOf(rowKey));
+                    addStyleName("i-selected");
+                } else {
+                    selectedRowKeys.remove(String.valueOf(rowKey));
+                    removeStyleName("i-selected");
+                }
+            }
+
+            /*
+             * (non-Javadoc)
+             * 
+             * @see com.itmill.toolkit.terminal.gwt.client.ui.IActionOwner#getActions()
+             */
+            public Action[] getActions() {
+                if (actionKeys == null) {
+                    return new Action[] {};
+                }
+                final Action[] actions = new Action[actionKeys.length];
+                for (int i = 0; i < actions.length; i++) {
+                    final String actionKey = actionKeys[i];
+                    final TreeAction a = new TreeAction(this, String
+                            .valueOf(rowKey), actionKey);
+                    a.setCaption(getActionCaption(actionKey));
+                    a.setIconUrl(getActionIcon(actionKey));
+                    actions[i] = a;
+                }
+                return actions;
+            }
+
+            public ApplicationConnection getClient() {
+                return client;
+            }
+
+            public String getPaintableId() {
+                return paintableId;
+            }
+        }
+    }
+
+    public void deselectAll() {
+        final Object[] keys = selectedRowKeys.toArray();
+        for (int i = 0; i < keys.length; i++) {
+            final IScrollTableRow row = getRenderedRowByKey((String) keys[i]);
+            if (row != null && row.isSelected()) {
+                row.toggleSelection();
+            }
+        }
+        // still ensure all selects are removed from (not necessary rendered)
+        selectedRowKeys.clear();
+
+    }
+
+    public void add(Widget w) {
+        throw new UnsupportedOperationException(
+                "ITable can contain only rows created by itself.");
+    }
+
+    public void clear() {
+        panel.clear();
+    }
+
+    public Iterator iterator() {
+        return panel.iterator();
+    }
+
+    public boolean remove(Widget w) {
+        return panel.remove(w);
+    }
+
+    public void mySetHeight(String height) {
+        // workaround very common 100% height problem - extract borders
+        if (height.equals("100%")) {
+            final int borders = getBorderSpace();
+            final Element parentElem = DOM.getParent(getElement());
+
+            // put table away from flow for a moment
+            DOM.setStyleAttribute(getElement(), "position", "absolute");
+            // get containers natural space for table
+            int availPixels = DOM.getElementPropertyInt(parentElem,
+                    "offsetHeight");
+            if (Util.isIE()) {
+                if (availPixels == 0) {
+                    // In complex layouts IE sometimes rather randomly returns 0
+                    // although container really has height. Use old value if
+                    // one exits.
+                    if (oldAvailPixels > 0) {
+                        availPixels = oldAvailPixels;
+                    }
+                } else {
+                    oldAvailPixels = availPixels;
+                }
+            }
+            // put table back to flow
+            DOM.setStyleAttribute(getElement(), "position", "static");
+            // set 100% height with borders
+            int pixelSize = (availPixels - borders);
+            if (pixelSize < 0) {
+                pixelSize = 0;
+            }
+            super.setHeight(pixelSize + "px");
+        } else {
+            // normally height don't include borders
+            super.setHeight(height);
+        }
+    }
+
+    private int getBorderSpace() {
+        final Element el = getElement();
+        return DOM.getElementPropertyInt(el, "offsetHeight")
+                - DOM.getElementPropertyInt(el, "clientHeight");
+    }
+
+    public void setWidth(String width) {
+        // NOP size handled internally
+    }
+
+    public void setHeight(String height) {
+        // NOP size handled internally
+    }
+
+    /*
+     * Overridden due Table might not survive of visibility change (scroll pos
+     * lost). Example ITabPanel just set contained components invisible and back
+     * when changing tabs.
+     */
+    public void setVisible(boolean visible) {
+        if (isVisible() != visible) {
+            super.setVisible(visible);
+            if (initializedAndAttached) {
+                if (visible) {
+                    DeferredCommand.addCommand(new Command() {
+                        public void execute() {
+                            bodyContainer.setScrollPosition(firstRowInViewPort
+                                    * tBody.getRowHeight());
+                        }
+                    });
+                }
+            }
+        }
+    }
+
+}
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/ISlider.java b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/ISlider.java
new file mode 100644 (file)
index 0000000..4cf56de
--- /dev/null
@@ -0,0 +1,415 @@
+/* \r
+@ITMillApache2LicenseForJavaFiles@\r
+ */\r
+// \r
+package com.itmill.toolkit.terminal.gwt.client.ui;\r
+\r
+import com.google.gwt.user.client.Command;\r
+import com.google.gwt.user.client.DOM;\r
+import com.google.gwt.user.client.DeferredCommand;\r
+import com.google.gwt.user.client.Element;\r
+import com.google.gwt.user.client.Event;\r
+import com.google.gwt.user.client.ui.Widget;\r
+import com.itmill.toolkit.terminal.gwt.client.ApplicationConnection;\r
+import com.itmill.toolkit.terminal.gwt.client.ContainerResizedListener;\r
+import com.itmill.toolkit.terminal.gwt.client.Paintable;\r
+import com.itmill.toolkit.terminal.gwt.client.UIDL;\r
+import com.itmill.toolkit.terminal.gwt.client.Util;\r
+\r
+public class ISlider extends Widget implements Paintable, Field,\r
+        ContainerResizedListener {\r
+\r
+    public static final String CLASSNAME = "i-slider";\r
+\r
+    /**\r
+     * Minimum size (width or height, depending on orientation) of the slider\r
+     * base.\r
+     */\r
+    private static final int MIN_SIZE = 50;\r
+\r
+    ApplicationConnection client;\r
+\r
+    String id;\r
+\r
+    private boolean immediate;\r
+    private boolean disabled;\r
+    private boolean readonly;\r
+    private boolean scrollbarStyle;\r
+\r
+    private int handleSize;\r
+    private double min;\r
+    private double max;\r
+    private int resolution;\r
+    private Double value;\r
+    private boolean vertical;\r
+    private int size = -1;\r
+    private boolean arrows;\r
+\r
+    /* DOM element for slider's base */\r
+    private final Element base;\r
+\r
+    /* DOM element for slider's handle */\r
+    private final Element handle;\r
+\r
+    /* DOM element for decrement arrow */\r
+    private final Element smaller;\r
+\r
+    /* DOM element for increment arrow */\r
+    private final Element bigger;\r
+\r
+    /* Temporary dragging/animation variables */\r
+    private boolean dragging = false;\r
+\r
+    public ISlider() {\r
+        super();\r
+\r
+        setElement(DOM.createDiv());\r
+        base = DOM.createDiv();\r
+        handle = DOM.createDiv();\r
+        smaller = DOM.createDiv();\r
+        bigger = DOM.createDiv();\r
+\r
+        setStyleName(CLASSNAME);\r
+        DOM.setElementProperty(base, "className", CLASSNAME + "-base");\r
+        DOM.setElementProperty(handle, "className", CLASSNAME + "-handle");\r
+        DOM.setElementProperty(smaller, "className", CLASSNAME + "-smaller");\r
+        DOM.setElementProperty(bigger, "className", CLASSNAME + "-bigger");\r
+\r
+        DOM.appendChild(getElement(), bigger);\r
+        DOM.appendChild(getElement(), smaller);\r
+        DOM.appendChild(getElement(), base);\r
+        DOM.appendChild(base, handle);\r
+\r
+        // Hide initially\r
+        DOM.setStyleAttribute(smaller, "display", "none");\r
+        DOM.setStyleAttribute(bigger, "display", "none");\r
+        DOM.setStyleAttribute(handle, "visibility", "hidden");\r
+\r
+        DOM.sinkEvents(getElement(), Event.MOUSEEVENTS | Event.ONMOUSEWHEEL);\r
+        DOM.sinkEvents(base, Event.ONCLICK);\r
+        DOM.sinkEvents(handle, Event.MOUSEEVENTS);\r
+        DOM.sinkEvents(smaller, Event.ONMOUSEDOWN | Event.ONMOUSEUP\r
+                | Event.ONMOUSEOUT);\r
+        DOM.sinkEvents(bigger, Event.ONMOUSEDOWN | Event.ONMOUSEUP\r
+                | Event.ONMOUSEOUT);\r
+    }\r
+\r
+    public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {\r
+\r
+        this.client = client;\r
+        id = uidl.getId();\r
+\r
+        // Ensure correct implementation\r
+        if (client.updateComponent(this, uidl, true)) {\r
+            return;\r
+        }\r
+\r
+        immediate = uidl.getBooleanAttribute("immediate");\r
+        disabled = uidl.getBooleanAttribute("disabled");\r
+        readonly = uidl.getBooleanAttribute("readonly");\r
+\r
+        vertical = uidl.hasAttribute("vertical");\r
+        arrows = uidl.hasAttribute("arrows");\r
+\r
+        String style = "";\r
+        if (uidl.hasAttribute("style")) {\r
+            style = uidl.getStringAttribute("style");\r
+        }\r
+\r
+        scrollbarStyle = style.indexOf("scrollbar") > -1;\r
+\r
+        if (arrows) {\r
+            DOM.setStyleAttribute(smaller, "display", "block");\r
+            DOM.setStyleAttribute(bigger, "display", "block");\r
+        }\r
+\r
+        if (vertical) {\r
+            addStyleName(CLASSNAME + "-vertical");\r
+        } else {\r
+            removeStyleName(CLASSNAME + "-vertical");\r
+        }\r
+\r
+        min = uidl.getDoubleAttribute("min");\r
+        max = uidl.getDoubleAttribute("max");\r
+        resolution = uidl.getIntAttribute("resolution");\r
+        value = new Double(uidl.getDoubleVariable("value"));\r
+\r
+        handleSize = uidl.getIntAttribute("hsize");\r
+\r
+        buildBase();\r
+\r
+        if (!vertical) {\r
+            // Draw handle with a delay to allow base to gain maximum width\r
+            DeferredCommand.addCommand(new Command() {\r
+                public void execute() {\r
+                    buildHandle();\r
+                    setValue(value, false, false);\r
+                }\r
+            });\r
+        } else {\r
+            buildHandle();\r
+            setValue(value, false, false);\r
+        }\r
+    }\r
+\r
+    private void buildBase() {\r
+        final String styleAttribute = vertical ? "height" : "width";\r
+        final String domProperty = vertical ? "offsetHeight" : "offsetWidth";\r
+\r
+        if (size == -1) {\r
+            final Element p = DOM.getParent(getElement());\r
+            if (DOM.getElementPropertyInt(p, domProperty) > 50) {\r
+                if (vertical) {\r
+                    setHeight();\r
+                } else {\r
+                    DOM.setStyleAttribute(base, styleAttribute, "");\r
+                }\r
+            } else {\r
+                // Set minimum size and adjust after all components have\r
+                // (supposedly) been drawn completely.\r
+                DOM.setStyleAttribute(base, styleAttribute, MIN_SIZE + "px");\r
+                DeferredCommand.addCommand(new Command() {\r
+                    public void execute() {\r
+                        final Element p = DOM.getParent(getElement());\r
+                        if (DOM.getElementPropertyInt(p, domProperty) > (MIN_SIZE + 5)) {\r
+                            if (vertical) {\r
+                                setHeight();\r
+                            } else {\r
+                                DOM.setStyleAttribute(base, styleAttribute, "");\r
+                            }\r
+                            // Ensure correct position\r
+                            setValue(value, false, false);\r
+                        }\r
+                    }\r
+                });\r
+            }\r
+        } else {\r
+            DOM.setStyleAttribute(base, styleAttribute, size + "px");\r
+        }\r
+\r
+        // TODO attach listeners for focusing and arrow keys\r
+    }\r
+\r
+    private void buildHandle() {\r
+        final String styleAttribute = vertical ? "height" : "width";\r
+        final String handleAttribute = vertical ? "marginTop" : "marginLeft";\r
+        final String domProperty = vertical ? "offsetHeight" : "offsetWidth";\r
+\r
+        DOM.setStyleAttribute(handle, handleAttribute, "0");\r
+\r
+        if (scrollbarStyle) {\r
+            // Only stretch the handle if scrollbar style is set.\r
+            int s = (int) (Double.parseDouble(DOM.getElementProperty(base,\r
+                    domProperty)) / 100 * handleSize);\r
+            if (handleSize == -1) {\r
+                final int baseS = Integer.parseInt(DOM.getElementProperty(base,\r
+                        domProperty));\r
+                final double range = (max - min) * (resolution + 1) * 3;\r
+                s = (int) (baseS - range);\r
+            }\r
+            if (s < 3) {\r
+                s = 3;\r
+            }\r
+            DOM.setStyleAttribute(handle, styleAttribute, s + "px");\r
+        } else {\r
+            DOM.setStyleAttribute(handle, styleAttribute, "");\r
+        }\r
+\r
+        // Restore visibility\r
+        DOM.setStyleAttribute(handle, "visibility", "visible");\r
+\r
+    }\r
+\r
+    private void setValue(Double value, boolean animate, boolean updateToServer) {\r
+        if (value == null) {\r
+            return;\r
+        }\r
+\r
+        if (value.doubleValue() < min) {\r
+            value = new Double(min);\r
+        } else if (value.doubleValue() > max) {\r
+            value = new Double(max);\r
+        }\r
+\r
+        // Update handle position\r
+        final String styleAttribute = vertical ? "marginTop" : "marginLeft";\r
+        final String domProperty = vertical ? "offsetHeight" : "offsetWidth";\r
+        final int handleSize = Integer.parseInt(DOM.getElementProperty(handle,\r
+                domProperty));\r
+        final int baseSize = Integer.parseInt(DOM.getElementProperty(base,\r
+                domProperty));\r
+        final int range = baseSize - handleSize;\r
+        double v = value.doubleValue();\r
+        // Round value to resolution\r
+        if (resolution > 0) {\r
+            v = Math.round(v * Math.pow(10, resolution));\r
+            v = v / Math.pow(10, resolution);\r
+        } else {\r
+            v = Math.round(v);\r
+        }\r
+        final double valueRange = max - min;\r
+        double p = 0;\r
+        if (valueRange > 0) {\r
+            p = range * ((v - min) / valueRange);\r
+        }\r
+        if (p < 0) {\r
+            p = 0;\r
+        }\r
+        if (vertical) {\r
+            // IE6 rounding behaves a little unstable, reduce one pixel so the\r
+            // containing element (base) won't expand without limits\r
+            p = range - p - (Util.isIE6() ? 1 : 0);\r
+        }\r
+        final double pos = p;\r
+\r
+        final int current = DOM.getIntStyleAttribute(handle, styleAttribute);\r
+\r
+        DOM.setStyleAttribute(handle, styleAttribute, (Math.round(pos)) + "px");\r
+\r
+        // TODO give more detailed info when dragging and do roundup\r
+        DOM.setElementAttribute(handle, "title", "" + v);\r
+\r
+        // Update value\r
+        this.value = new Double(v);\r
+\r
+        if (updateToServer) {\r
+            client.updateVariable(id, "value", this.value.doubleValue(),\r
+                    immediate);\r
+        }\r
+    }\r
+\r
+    public void onBrowserEvent(Event event) {\r
+        if (disabled || readonly) {\r
+            return;\r
+        }\r
+        final Element targ = DOM.eventGetTarget(event);\r
+\r
+        if (DOM.eventGetType(event) == Event.ONMOUSEWHEEL) {\r
+            processMouseWheelEvent(event);\r
+        } else if (dragging || DOM.compare(targ, handle)) {\r
+            processHandleEvent(event);\r
+        } else if (DOM.compare(targ, smaller)) {\r
+            decreaseValue(event);\r
+        } else if (DOM.compare(targ, bigger)) {\r
+            increaseValue(event);\r
+        } else {\r
+            processBaseEvent(event);\r
+        }\r
+    }\r
+\r
+    private void processMouseWheelEvent(Event event) {\r
+        final int dir = DOM.eventGetMouseWheelVelocityY(event);\r
+        if (dir < 0) {\r
+            increaseValue(event);\r
+        } else {\r
+            decreaseValue(event);\r
+        }\r
+        DOM.eventPreventDefault(event);\r
+        DOM.eventCancelBubble(event, true);\r
+    }\r
+\r
+    private void processHandleEvent(Event event) {\r
+        switch (DOM.eventGetType(event)) {\r
+        case Event.ONMOUSEDOWN:\r
+            if (!disabled && !readonly) {\r
+                dragging = true;\r
+                DOM.setCapture(getElement());\r
+                DOM.eventPreventDefault(event); // prevent selecting text\r
+                DOM.eventCancelBubble(event, true);\r
+            }\r
+            break;\r
+        case Event.ONMOUSEMOVE:\r
+            if (dragging) {\r
+                // DOM.setCapture(getElement());\r
+                setValueByEvent(event, false, false);\r
+            }\r
+            break;\r
+        case Event.ONMOUSEUP:\r
+            dragging = false;\r
+            DOM.releaseCapture(getElement());\r
+            setValueByEvent(event, true, true);\r
+            break;\r
+        default:\r
+            break;\r
+        }\r
+    }\r
+\r
+    private void processBaseEvent(Event event) {\r
+        if (DOM.eventGetType(event) == Event.ONMOUSEDOWN) {\r
+            if (!disabled && !readonly && !dragging) {\r
+                setValueByEvent(event, true, true);\r
+                DOM.eventCancelBubble(event, true);\r
+            }\r
+        } else if (DOM.eventGetType(event) == Event.ONMOUSEDOWN && dragging) {\r
+            dragging = false;\r
+            DOM.releaseCapture(getElement());\r
+            setValueByEvent(event, true, true);\r
+        }\r
+    }\r
+\r
+    private void decreaseValue(Event event) {\r
+        setValue(new Double(value.doubleValue() - Math.pow(10, -resolution)),\r
+                false, true);\r
+    }\r
+\r
+    private void increaseValue(Event event) {\r
+        setValue(new Double(value.doubleValue() + Math.pow(10, -resolution)),\r
+                false, true);\r
+    }\r
+\r
+    private void setValueByEvent(Event event, boolean animate, boolean roundup) {\r
+        double v = min; // Fallback to min\r
+\r
+        final int coord = vertical ? DOM.eventGetClientY(event) : DOM\r
+                .eventGetClientX(event);\r
+        final String domProperty = vertical ? "offsetHeight" : "offsetWidth";\r
+\r
+        final double handleSize = Integer.parseInt(DOM.getElementProperty(\r
+                handle, domProperty));\r
+        final double baseSize = Integer.parseInt(DOM.getElementProperty(base,\r
+                domProperty));\r
+        final double baseOffset = vertical ? DOM.getAbsoluteTop(base)\r
+                - handleSize / 2 : DOM.getAbsoluteLeft(base) + handleSize / 2;\r
+\r
+        if (vertical) {\r
+            v = ((baseSize - (coord - baseOffset)) / (baseSize - handleSize))\r
+                    * (max - min) + min;\r
+        } else {\r
+            v = ((coord - baseOffset) / (baseSize - handleSize)) * (max - min)\r
+                    + min;\r
+        }\r
+\r
+        if (v < min) {\r
+            v = min;\r
+        } else if (v > max) {\r
+            v = max;\r
+        }\r
+\r
+        setValue(new Double(v), animate, roundup);\r
+    }\r
+\r
+    public void iLayout() {\r
+        if (vertical) {\r
+            setHeight();\r
+        }\r
+        // Update handle position\r
+        setValue(value, false, false);\r
+    }\r
+\r
+    private void setHeight() {\r
+        if (size == -1) {\r
+            // Calculate decoration size\r
+            DOM.setStyleAttribute(base, "height", "0");\r
+            DOM.setStyleAttribute(base, "overflow", "hidden");\r
+            int h = DOM.getElementPropertyInt(getElement(), "offsetHeight");\r
+            if (h < MIN_SIZE) {\r
+                h = MIN_SIZE;\r
+            }\r
+            DOM.setStyleAttribute(base, "height", h + "px");\r
+        } else {\r
+            DOM.setStyleAttribute(base, "height", size + "px");\r
+        }\r
+        DOM.setStyleAttribute(base, "overflow", "");\r
+    }\r
+\r
+}\r
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/ISplitPanel.java b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/ISplitPanel.java
new file mode 100644 (file)
index 0000000..efdc13b
--- /dev/null
@@ -0,0 +1,410 @@
+/* 
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.itmill.toolkit.terminal.gwt.client.ui;
+
+import com.google.gwt.user.client.Command;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.DeferredCommand;
+import com.google.gwt.user.client.Element;
+import com.google.gwt.user.client.Event;
+import com.google.gwt.user.client.ui.ComplexPanel;
+import com.google.gwt.user.client.ui.Widget;
+import com.itmill.toolkit.terminal.gwt.client.ApplicationConnection;
+import com.itmill.toolkit.terminal.gwt.client.ContainerResizedListener;
+import com.itmill.toolkit.terminal.gwt.client.Paintable;
+import com.itmill.toolkit.terminal.gwt.client.UIDL;
+import com.itmill.toolkit.terminal.gwt.client.Util;
+
+public class ISplitPanel extends ComplexPanel implements Paintable,
+        ContainerResizedListener {
+    public static final String CLASSNAME = "i-splitpanel";
+
+    public static final int ORIENTATION_HORIZONTAL = 0;
+
+    public static final int ORIENTATION_VERTICAL = 1;
+
+    private static final int MIN_SIZE = 30;
+
+    private int orientation = ORIENTATION_HORIZONTAL;
+
+    private Widget firstChild;
+
+    private Widget secondChild;
+
+    private final Element wrapper = DOM.createDiv();
+
+    private final Element firstContainer = DOM.createDiv();
+
+    private final Element secondContainer = DOM.createDiv();
+
+    private final Element splitter = DOM.createDiv();
+
+    private boolean resizing;
+
+    private int origX;
+
+    private int origY;
+
+    private int origMouseX;
+
+    private int origMouseY;
+
+    private boolean locked;
+
+    private String splitterStyleName;
+
+    public ISplitPanel() {
+        this(ORIENTATION_HORIZONTAL);
+    }
+
+    public ISplitPanel(int orientation) {
+        setElement(DOM.createDiv());
+        switch (orientation) {
+        case ORIENTATION_HORIZONTAL:
+            setStyleName(CLASSNAME + "-horizontal");
+            break;
+        case ORIENTATION_VERTICAL:
+        default:
+            setStyleName(CLASSNAME + "-vertical");
+            break;
+        }
+        // size below will be overridden in update from uidl, initial size
+        // needed to keep IE alive
+        setWidth(MIN_SIZE + "px");
+        setHeight(MIN_SIZE + "px");
+        constructDom();
+        setOrientation(orientation);
+        DOM.sinkEvents(splitter, (Event.MOUSEEVENTS));
+        DOM.sinkEvents(getElement(), (Event.MOUSEEVENTS));
+    }
+
+    protected void constructDom() {
+        DOM.appendChild(splitter, DOM.createDiv()); // for styling
+        DOM.appendChild(getElement(), wrapper);
+        DOM.setStyleAttribute(wrapper, "position", "relative");
+        DOM.setStyleAttribute(wrapper, "width", "100%");
+        DOM.setStyleAttribute(wrapper, "height", "100%");
+
+        DOM.appendChild(wrapper, splitter);
+        DOM.appendChild(wrapper, secondContainer);
+        DOM.appendChild(wrapper, firstContainer);
+
+        DOM.setStyleAttribute(splitter, "position", "absolute");
+        DOM.setStyleAttribute(secondContainer, "position", "absolute");
+
+        DOM.setStyleAttribute(firstContainer, "overflow", "auto");
+        DOM.setStyleAttribute(secondContainer, "overflow", "auto");
+        if (Util.isIE7()) {
+            /*
+             * Part I of IE7 weirdness hack, will be set to auto in layout phase
+             * 
+             * With IE7 one will sometimes get scrollbars with overflow auto
+             * even though there is nothing to scroll (content fits into area).
+             * 
+             */
+            DOM.setStyleAttribute(firstContainer, "overflow", "hidden");
+            DOM.setStyleAttribute(secondContainer, "overflow", "hidden");
+        }
+    }
+
+    private void setOrientation(int orientation) {
+        this.orientation = orientation;
+        if (orientation == ORIENTATION_HORIZONTAL) {
+            DOM.setStyleAttribute(splitter, "height", "100%");
+            DOM.setStyleAttribute(firstContainer, "height", "100%");
+            DOM.setStyleAttribute(secondContainer, "height", "100%");
+        } else {
+            DOM.setStyleAttribute(splitter, "width", "100%");
+            DOM.setStyleAttribute(firstContainer, "width", "100%");
+            DOM.setStyleAttribute(secondContainer, "width", "100%");
+        }
+
+        splitterStyleName = CLASSNAME
+                + (orientation == ORIENTATION_HORIZONTAL ? "-hsplitter"
+                        : "-vsplitter");
+        DOM.setElementProperty(splitter, "className", splitterStyleName);
+    }
+
+    public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+        if (client.updateComponent(this, uidl, true)) {
+            return;
+        }
+
+        setSplitPosition(uidl.getStringAttribute("position"));
+
+        locked = uidl.hasAttribute("locked");
+        if (locked) {
+            DOM.setElementProperty(splitter, "className", splitterStyleName
+                    + "-locked");
+        } else {
+            DOM.setElementProperty(splitter, "className", splitterStyleName);
+        }
+
+        final Paintable newFirstChild = client.getPaintable(uidl
+                .getChildUIDL(0));
+        final Paintable newSecondChild = client.getPaintable(uidl
+                .getChildUIDL(1));
+        if (firstChild != newFirstChild) {
+            if (firstChild != null) {
+                client.unregisterPaintable((Paintable) firstChild);
+            }
+            setFirstWidget((Widget) newFirstChild);
+        }
+        if (secondChild != newSecondChild) {
+            if (secondChild != null) {
+                client.unregisterPaintable((Paintable) secondChild);
+            }
+            setSecondWidget((Widget) newSecondChild);
+        }
+        newFirstChild.updateFromUIDL(uidl.getChildUIDL(0), client);
+        newSecondChild.updateFromUIDL(uidl.getChildUIDL(1), client);
+
+        if (Util.isIE7()) {
+            // Part III of IE7 hack
+            DeferredCommand.addCommand(new Command() {
+                public void execute() {
+                    iLayout();
+                }
+            });
+        }
+    }
+
+    private void setSplitPosition(String pos) {
+        if (orientation == ORIENTATION_HORIZONTAL) {
+            DOM.setStyleAttribute(splitter, "left", pos);
+        } else {
+            DOM.setStyleAttribute(splitter, "top", pos);
+        }
+        iLayout();
+    }
+
+    /*
+     * Calculates absolutely positioned container places/sizes (non-Javadoc)
+     * 
+     * @see com.itmill.toolkit.terminal.gwt.client.NeedsLayout#layout()
+     */
+    public void iLayout() {
+        if (!isAttached()) {
+            return;
+        }
+        int wholeSize;
+        int pixelPosition;
+
+        DOM.setStyleAttribute(firstContainer, "overflow", "hidden");
+        DOM.setStyleAttribute(secondContainer, "overflow", "hidden");
+
+        switch (orientation) {
+        case ORIENTATION_HORIZONTAL:
+            wholeSize = DOM.getElementPropertyInt(wrapper, "clientWidth");
+            pixelPosition = DOM.getElementPropertyInt(splitter, "offsetLeft");
+
+            // reposition splitter in case it is out of box
+            if (pixelPosition > 0
+                    && pixelPosition + getSplitterSize() > wholeSize) {
+                pixelPosition = wholeSize - getSplitterSize();
+                if (pixelPosition < 0) {
+                    pixelPosition = 0;
+                }
+                setSplitPosition(pixelPosition + "px");
+                return;
+            }
+
+            DOM
+                    .setStyleAttribute(firstContainer, "width", pixelPosition
+                            + "px");
+            int secondContainerWidth = (wholeSize - pixelPosition - getSplitterSize());
+            if (secondContainerWidth < 0) {
+                secondContainerWidth = 0;
+            }
+            DOM.setStyleAttribute(secondContainer, "width",
+                    secondContainerWidth + "px");
+            DOM.setStyleAttribute(secondContainer, "left",
+                    (pixelPosition + getSplitterSize()) + "px");
+
+            break;
+        case ORIENTATION_VERTICAL:
+            wholeSize = DOM.getElementPropertyInt(wrapper, "clientHeight");
+            pixelPosition = DOM.getElementPropertyInt(splitter, "offsetTop");
+
+            // reposition splitter in case it is out of box
+            if (pixelPosition > 0
+                    && pixelPosition + getSplitterSize() > wholeSize) {
+                pixelPosition = wholeSize - getSplitterSize();
+                if (pixelPosition < 0) {
+                    pixelPosition = 0;
+                }
+                setSplitPosition(pixelPosition + "px");
+                return;
+            }
+
+            DOM.setStyleAttribute(firstContainer, "height", pixelPosition
+                    + "px");
+            int secondContainerHeight = (wholeSize - pixelPosition - getSplitterSize());
+            if (secondContainerHeight < 0) {
+                secondContainerHeight = 0;
+            }
+            DOM.setStyleAttribute(secondContainer, "height",
+                    secondContainerHeight + "px");
+            DOM.setStyleAttribute(secondContainer, "top",
+                    (pixelPosition + getSplitterSize()) + "px");
+            break;
+        }
+
+        if (Util.isIE7()) {
+            // Part I of IE7 weirdness hack, will be set to auto in layout phase
+            Util.runDescendentsLayout(this);
+            DeferredCommand.addCommand(new Command() {
+                public void execute() {
+                    DOM.setStyleAttribute(firstContainer, "overflow", "auto");
+                    DOM.setStyleAttribute(secondContainer, "overflow", "auto");
+                }
+            });
+        } else {
+            Util.runDescendentsLayout(this);
+            DOM.setStyleAttribute(firstContainer, "overflow", "auto");
+            DOM.setStyleAttribute(secondContainer, "overflow", "auto");
+        }
+
+    }
+
+    private void setFirstWidget(Widget w) {
+        if (firstChild != null) {
+            firstChild.removeFromParent();
+        }
+        super.add(w, firstContainer);
+        firstChild = w;
+    }
+
+    private void setSecondWidget(Widget w) {
+        if (secondChild != null) {
+            secondChild.removeFromParent();
+        }
+        super.add(w, secondContainer);
+        secondChild = w;
+    }
+
+    public void setHeight(String height) {
+        super.setHeight(height);
+        // give sane height
+        getOffsetHeight(); // shake IE
+        if (getOffsetHeight() < MIN_SIZE) {
+            super.setHeight(MIN_SIZE + "px");
+        }
+    }
+
+    public void setWidth(String width) {
+        super.setWidth(width);
+        // give sane width
+        getOffsetWidth(); // shake IE
+        if (getOffsetWidth() < MIN_SIZE) {
+            super.setWidth(MIN_SIZE + "px");
+        }
+    }
+
+    public void onBrowserEvent(Event event) {
+        switch (DOM.eventGetType(event)) {
+        case Event.ONMOUSEMOVE:
+            if (resizing) {
+                onMouseMove(event);
+            }
+            break;
+        case Event.ONMOUSEDOWN:
+            onMouseDown(event);
+            break;
+        case Event.ONMOUSEUP:
+            if (resizing) {
+                onMouseUp(event);
+            }
+            break;
+        case Event.ONCLICK:
+            resizing = false;
+            break;
+        }
+    }
+
+    public void onMouseDown(Event event) {
+        if (locked) {
+            return;
+        }
+        final Element trg = DOM.eventGetTarget(event);
+        if (DOM.compare(trg, splitter)
+                || DOM.compare(trg, DOM.getChild(splitter, 0))) {
+            resizing = true;
+            DOM.setCapture(getElement());
+            origX = DOM.getElementPropertyInt(splitter, "offsetLeft");
+            origY = DOM.getElementPropertyInt(splitter, "offsetTop");
+            origMouseX = DOM.eventGetClientX(event);
+            origMouseY = DOM.eventGetClientY(event);
+            DOM.eventCancelBubble(event, true);
+            DOM.eventPreventDefault(event);
+        }
+    }
+
+    public void onMouseMove(Event event) {
+        switch (orientation) {
+        case ORIENTATION_HORIZONTAL:
+            final int x = DOM.eventGetClientX(event);
+            onHorizontalMouseMove(x);
+            break;
+        case ORIENTATION_VERTICAL:
+        default:
+            final int y = DOM.eventGetClientY(event);
+            onVerticalMouseMove(y);
+            break;
+        }
+        iLayout();
+    }
+
+    private void onHorizontalMouseMove(int x) {
+        int newX = origX + x - origMouseX;
+        if (newX < 0) {
+            newX = 0;
+        }
+        if (newX + getSplitterSize() > getOffsetWidth()) {
+            newX = getOffsetWidth() - getSplitterSize();
+        }
+        DOM.setStyleAttribute(splitter, "left", newX + "px");
+    }
+
+    private void onVerticalMouseMove(int y) {
+        int newY = origY + y - origMouseY;
+        if (newY < 0) {
+            newY = 0;
+        }
+
+        if (newY + getSplitterSize() > getOffsetHeight()) {
+            newY = getOffsetHeight() - getSplitterSize();
+        }
+        DOM.setStyleAttribute(splitter, "top", newY + "px");
+    }
+
+    public void onMouseUp(Event event) {
+        DOM.releaseCapture(getElement());
+        resizing = false;
+        onMouseMove(event);
+    }
+
+    private static int splitterSize = -1;
+
+    private int getSplitterSize() {
+        if (splitterSize < 0) {
+            if (isAttached()) {
+                switch (orientation) {
+                case ORIENTATION_HORIZONTAL:
+                    splitterSize = DOM.getElementPropertyInt(splitter,
+                            "offsetWidth");
+                    break;
+
+                default:
+                    splitterSize = DOM.getElementPropertyInt(splitter,
+                            "offsetHeight");
+                    break;
+                }
+            }
+        }
+        return splitterSize;
+    }
+
+}
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/ISplitPanelHorizontal.java b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/ISplitPanelHorizontal.java
new file mode 100644 (file)
index 0000000..cfc686b
--- /dev/null
@@ -0,0 +1,12 @@
+/* 
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.itmill.toolkit.terminal.gwt.client.ui;
+
+public class ISplitPanelHorizontal extends ISplitPanel {
+
+    public ISplitPanelHorizontal() {
+        super(ISplitPanel.ORIENTATION_HORIZONTAL);
+    }
+}
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/ISplitPanelVertical.java b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/ISplitPanelVertical.java
new file mode 100644 (file)
index 0000000..d6e7238
--- /dev/null
@@ -0,0 +1,12 @@
+/* 
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.itmill.toolkit.terminal.gwt.client.ui;
+
+public class ISplitPanelVertical extends ISplitPanel {
+
+    public ISplitPanelVertical() {
+        super(ISplitPanel.ORIENTATION_VERTICAL);
+    }
+}
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/ITablePaging.java b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/ITablePaging.java
new file mode 100644 (file)
index 0000000..e1dbf37
--- /dev/null
@@ -0,0 +1,438 @@
+/* 
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.itmill.toolkit.terminal.gwt.client.ui;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Set;
+import java.util.Vector;
+
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.Event;
+import com.google.gwt.user.client.Window;
+import com.google.gwt.user.client.ui.Button;
+import com.google.gwt.user.client.ui.ClickListener;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.Grid;
+import com.google.gwt.user.client.ui.HTML;
+import com.google.gwt.user.client.ui.HorizontalPanel;
+import com.google.gwt.user.client.ui.Label;
+import com.google.gwt.user.client.ui.SimplePanel;
+import com.google.gwt.user.client.ui.VerticalPanel;
+import com.google.gwt.user.client.ui.Widget;
+import com.itmill.toolkit.terminal.gwt.client.ApplicationConnection;
+import com.itmill.toolkit.terminal.gwt.client.Paintable;
+import com.itmill.toolkit.terminal.gwt.client.UIDL;
+
+/**
+ * TODO make this work (just an early prototype). We may want to have paging
+ * style table which will be much lighter than IScrollTable is.
+ */
+public class ITablePaging extends Composite implements Table, Paintable,
+        ClickListener {
+
+    private final Grid tBody = new Grid();
+    private final Button nextPage = new Button("&gt;");
+    private final Button prevPage = new Button("&lt;");
+    private final Button firstPage = new Button("&lt;&lt;");
+    private final Button lastPage = new Button("&gt;&gt;");
+
+    private int pageLength = 15;
+
+    private boolean rowHeaders = false;
+
+    private ApplicationConnection client;
+    private String id;
+
+    private boolean immediate = false;
+
+    private int selectMode = Table.SELECT_MODE_NONE;
+
+    private final Vector selectedRowKeys = new Vector();
+
+    private int totalRows;
+
+    private final HashMap visibleColumns = new HashMap();
+
+    private int rows;
+
+    private int firstRow;
+    private boolean sortAscending = true;
+    private final HorizontalPanel pager;
+
+    public HashMap rowKeysToTableRows = new HashMap();
+
+    public ITablePaging() {
+
+        tBody.setStyleName("itable-tbody");
+
+        final VerticalPanel panel = new VerticalPanel();
+
+        pager = new HorizontalPanel();
+        pager.add(firstPage);
+        firstPage.addClickListener(this);
+        pager.add(prevPage);
+        prevPage.addClickListener(this);
+        pager.add(nextPage);
+        nextPage.addClickListener(this);
+        pager.add(lastPage);
+        lastPage.addClickListener(this);
+
+        panel.add(pager);
+        panel.add(tBody);
+
+        initWidget(panel);
+    }
+
+    public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+        if (client.updateComponent(this, uidl, true)) {
+            return;
+        }
+
+        this.client = client;
+        id = uidl.getStringAttribute("id");
+        immediate = uidl.getBooleanAttribute("immediate");
+        totalRows = uidl.getIntAttribute("totalrows");
+        pageLength = uidl.getIntAttribute("pagelength");
+        firstRow = uidl.getIntAttribute("firstrow");
+        rows = uidl.getIntAttribute("rows");
+
+        if (uidl.hasAttribute("selectmode")) {
+            if (uidl.getStringAttribute("selectmode").equals("multi")) {
+                selectMode = Table.SELECT_MODE_MULTI;
+            } else {
+                selectMode = Table.SELECT_MODE_SINGLE;
+            }
+
+            if (uidl.hasAttribute("selected")) {
+                final Set selectedKeys = uidl
+                        .getStringArrayVariableAsSet("selected");
+                selectedRowKeys.clear();
+                for (final Iterator it = selectedKeys.iterator(); it.hasNext();) {
+                    selectedRowKeys.add(it.next());
+                }
+            }
+        }
+
+        if (uidl.hasVariable("sortascending")) {
+            sortAscending = uidl.getBooleanVariable("sortascending");
+        }
+
+        if (uidl.hasAttribute("rowheaders")) {
+            rowHeaders = true;
+        }
+
+        UIDL rowData = null;
+        UIDL visibleColumns = null;
+        for (final Iterator it = uidl.getChildIterator(); it.hasNext();) {
+            final UIDL c = (UIDL) it.next();
+            if (c.getTag().equals("rows")) {
+                rowData = c;
+            } else if (c.getTag().equals("actions")) {
+                updateActionMap(c);
+            } else if (c.getTag().equals("visiblecolumns")) {
+                visibleColumns = c;
+            }
+        }
+        tBody.resize(rows + 1, uidl.getIntAttribute("cols")
+                + (rowHeaders ? 1 : 0));
+        updateHeader(visibleColumns);
+        updateBody(rowData);
+
+        updatePager();
+    }
+
+    private void updateHeader(UIDL c) {
+        final Iterator it = c.getChildIterator();
+        visibleColumns.clear();
+        int colIndex = (rowHeaders ? 1 : 0);
+        while (it.hasNext()) {
+            final UIDL col = (UIDL) it.next();
+            final String cid = col.getStringAttribute("cid");
+            if (!col.hasAttribute("collapsed")) {
+                tBody.setWidget(0, colIndex, new HeaderCell(cid, col
+                        .getStringAttribute("caption")));
+
+            }
+            colIndex++;
+        }
+    }
+
+    private void updateActionMap(UIDL c) {
+        // TODO Auto-generated method stub
+
+    }
+
+    /**
+     * Updates row data from uidl. UpdateFromUIDL delegates updating tBody to
+     * this method.
+     * 
+     * Updates may be to different part of tBody, depending on update type. It
+     * can be initial row data, scroll up, scroll down...
+     * 
+     * @param uidl
+     *                which contains row data
+     */
+    private void updateBody(UIDL uidl) {
+        final Iterator it = uidl.getChildIterator();
+
+        int curRowIndex = 1;
+        while (it.hasNext()) {
+            final UIDL rowUidl = (UIDL) it.next();
+            final TableRow row = new TableRow(curRowIndex, String
+                    .valueOf(rowUidl.getIntAttribute("key")), rowUidl
+                    .hasAttribute("selected"));
+            int colIndex = 0;
+            if (rowHeaders) {
+                tBody.setWidget(curRowIndex, colIndex, new BodyCell(row,
+                        rowUidl.getStringAttribute("caption")));
+                colIndex++;
+            }
+            final Iterator cells = rowUidl.getChildIterator();
+            while (cells.hasNext()) {
+                final Object cell = cells.next();
+                if (cell instanceof String) {
+                    tBody.setWidget(curRowIndex, colIndex, new BodyCell(row,
+                            (String) cell));
+                } else {
+                    final Paintable cellContent = client
+                            .getPaintable((UIDL) cell);
+                    final BodyCell bodyCell = new BodyCell(row);
+                    bodyCell.setWidget((Widget) cellContent);
+                    tBody.setWidget(curRowIndex, colIndex, bodyCell);
+                }
+                colIndex++;
+            }
+            curRowIndex++;
+        }
+    }
+
+    private void updatePager() {
+        if (pageLength == 0) {
+            pager.setVisible(false);
+            return;
+        }
+        if (isFirstPage()) {
+            firstPage.setEnabled(false);
+            prevPage.setEnabled(false);
+        } else {
+            firstPage.setEnabled(true);
+            prevPage.setEnabled(true);
+        }
+        if (hasNextPage()) {
+            nextPage.setEnabled(true);
+            lastPage.setEnabled(true);
+        } else {
+            nextPage.setEnabled(false);
+            lastPage.setEnabled(false);
+
+        }
+    }
+
+    private boolean hasNextPage() {
+        if (firstRow + rows + 1 > totalRows) {
+            return false;
+        }
+        return true;
+    }
+
+    private boolean isFirstPage() {
+        if (firstRow == 0) {
+            return true;
+        }
+        return false;
+    }
+
+    public void onClick(Widget sender) {
+        if (sender instanceof Button) {
+            if (sender == firstPage) {
+                client.updateVariable(id, "firstvisible", 0, true);
+            } else if (sender == nextPage) {
+                client.updateVariable(id, "firstvisible",
+                        firstRow + pageLength, true);
+            } else if (sender == prevPage) {
+                int newFirst = firstRow - pageLength;
+                if (newFirst < 0) {
+                    newFirst = 0;
+                }
+                client.updateVariable(id, "firstvisible", newFirst, true);
+            } else if (sender == lastPage) {
+                client.updateVariable(id, "firstvisible", totalRows
+                        - pageLength, true);
+            }
+        }
+        if (sender instanceof HeaderCell) {
+            final HeaderCell hCell = (HeaderCell) sender;
+            client.updateVariable(id, "sortcolumn", hCell.getCid(), false);
+            client.updateVariable(id, "sortascending", (sortAscending ? false
+                    : true), true);
+        }
+    }
+
+    private class HeaderCell extends HTML {
+
+        private String cid;
+
+        public String getCid() {
+            return cid;
+        }
+
+        public void setCid(String pid) {
+            cid = pid;
+        }
+
+        HeaderCell(String pid, String caption) {
+            super();
+            cid = pid;
+            addClickListener(ITablePaging.this);
+            setText(caption);
+            // TODO remove debug color
+            DOM.setStyleAttribute(getElement(), "color", "brown");
+            DOM.setStyleAttribute(getElement(), "font-weight", "bold");
+        }
+    }
+
+    /**
+     * Abstraction of table cell content. In needs to know on which row it is in
+     * case of context click.
+     * 
+     * @author mattitahvonen
+     */
+    public class BodyCell extends SimplePanel {
+        private final TableRow row;
+
+        public BodyCell(TableRow row) {
+            super();
+            sinkEvents(Event.BUTTON_LEFT | Event.BUTTON_RIGHT);
+            this.row = row;
+        }
+
+        public BodyCell(TableRow row2, String textContent) {
+            super();
+            sinkEvents(Event.BUTTON_LEFT | Event.BUTTON_RIGHT);
+            row = row2;
+            setWidget(new Label(textContent));
+        }
+
+        public void onBrowserEvent(Event event) {
+            System.out.println("CEll event: " + event.toString());
+            switch (DOM.eventGetType(event)) {
+            case Event.BUTTON_RIGHT:
+                row.showContextMenu(event);
+                Window.alert("context menu un-implemented");
+                DOM.eventCancelBubble(event, true);
+                break;
+            case Event.BUTTON_LEFT:
+                if (selectMode > Table.SELECT_MODE_NONE) {
+                    row.toggleSelected();
+                }
+                break;
+            default:
+                break;
+            }
+            super.onBrowserEvent(event);
+        }
+    }
+
+    private class TableRow {
+
+        private final String key;
+        private final int rowIndex;
+        private boolean selected = false;
+
+        public TableRow(int rowIndex, String rowKey, boolean selected) {
+            rowKeysToTableRows.put(rowKey, this);
+            this.rowIndex = rowIndex;
+            key = rowKey;
+            setSelected(selected);
+        }
+
+        /**
+         * This method is used to set row status. Does not change value on
+         * server.
+         * 
+         * @param selected
+         */
+        public void setSelected(boolean sel) {
+            selected = sel;
+            if (selected) {
+                selectedRowKeys.add(key);
+                DOM.setStyleAttribute(tBody.getRowFormatter().getElement(
+                        rowIndex), "background", "yellow");
+
+            } else {
+                selectedRowKeys.remove(key);
+                DOM.setStyleAttribute(tBody.getRowFormatter().getElement(
+                        rowIndex), "background", "transparent");
+            }
+        }
+
+        public void setContextMenuOptions(HashMap options) {
+
+        }
+
+        /**
+         * Toggles rows select state. Also updates state to server according to
+         * tables immediate flag.
+         * 
+         */
+        public void toggleSelected() {
+            if (selected) {
+                setSelected(false);
+            } else {
+                if (selectMode == Table.SELECT_MODE_SINGLE) {
+                    deselectAll();
+                }
+                setSelected(true);
+            }
+            client.updateVariable(id, "selected", selectedRowKeys.toArray(),
+                    immediate);
+        }
+
+        /**
+         * Shows context menu for this row.
+         * 
+         * @param event
+         *                Event which triggered context menu. Correct place for
+         *                context menu can be determined with it.
+         */
+        public void showContextMenu(Event event) {
+            System.out.println("TODO: Show context menu");
+        }
+    }
+
+    public void deselectAll() {
+        final Object[] keys = selectedRowKeys.toArray();
+        for (int i = 0; i < keys.length; i++) {
+            final TableRow tableRow = (TableRow) rowKeysToTableRows
+                    .get(keys[i]);
+            if (tableRow != null) {
+                tableRow.setSelected(false);
+            }
+        }
+        // still ensure all selects are removed from
+        selectedRowKeys.clear();
+    }
+
+    public void add(Widget w) {
+        // TODO Auto-generated method stub
+
+    }
+
+    public void clear() {
+        // TODO Auto-generated method stub
+
+    }
+
+    public Iterator iterator() {
+        // TODO Auto-generated method stub
+        return null;
+    }
+
+    public boolean remove(Widget w) {
+        // TODO Auto-generated method stub
+        return false;
+    }
+}
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/ITabsheet.java b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/ITabsheet.java
new file mode 100644 (file)
index 0000000..3c76cbd
--- /dev/null
@@ -0,0 +1,383 @@
+/* 
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.itmill.toolkit.terminal.gwt.client.ui;
+
+import java.util.Iterator;
+
+import com.google.gwt.user.client.Command;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.DeferredCommand;
+import com.google.gwt.user.client.Element;
+import com.google.gwt.user.client.Event;
+import com.google.gwt.user.client.ui.Label;
+import com.google.gwt.user.client.ui.SourcesTabEvents;
+import com.google.gwt.user.client.ui.TabBar;
+import com.google.gwt.user.client.ui.TabListener;
+import com.google.gwt.user.client.ui.Widget;
+import com.itmill.toolkit.terminal.gwt.client.ApplicationConnection;
+import com.itmill.toolkit.terminal.gwt.client.Caption;
+import com.itmill.toolkit.terminal.gwt.client.ContainerResizedListener;
+import com.itmill.toolkit.terminal.gwt.client.Paintable;
+import com.itmill.toolkit.terminal.gwt.client.UIDL;
+import com.itmill.toolkit.terminal.gwt.client.Util;
+
+public class ITabsheet extends ITabsheetBase implements
+        ContainerResizedListener {
+
+    public static final String CLASSNAME = "i-tabsheet";
+
+    public static final String TABS_CLASSNAME = "i-tabsheet-tabcontainer";
+    public static final String SCROLLER_CLASSNAME = "i-tabsheet-scroller";
+    private final Element tabs; // tabbar and 'scroller' container
+    private final Element scroller; // tab-scroller element
+    private final Element scrollerNext; // tab-scroller next button element
+    private final Element scrollerPrev; // tab-scroller prev button element
+    private int scrollerIndex = 0;
+
+    private final TabBar tb;
+    private final ITabsheetPanel tp;
+    private final Element contentNode, deco;
+
+    private String height;
+    private String width;
+
+    private boolean waitingForResponse;
+
+    /**
+     * Previous visible widget is set invisible with CSS (not display: none, but
+     * visibility: hidden), to avoid flickering during render process. Normal
+     * visibility must be returned later when new widget is rendered.
+     */
+    private Widget previousVisibleWidget;
+
+    private final TabListener tl = new TabListener() {
+
+        public void onTabSelected(SourcesTabEvents sender, final int tabIndex) {
+            if (client != null && activeTabIndex != tabIndex) {
+                addStyleDependentName("loading");
+                // run updating variables in deferred command to bypass some
+                // FF
+                // optimization issues
+                DeferredCommand.addCommand(new Command() {
+                    public void execute() {
+                        previousVisibleWidget = tp.getWidget(tp
+                                .getVisibleWidget());
+                        DOM.setStyleAttribute(previousVisibleWidget
+                                .getElement(), "visibility", "hidden");
+                        client.updateVariable(id, "selected", tabKeys.get(
+                                tabIndex).toString(), true);
+                    }
+                });
+                waitingForResponse = true;
+            }
+        }
+
+        public boolean onBeforeTabSelected(SourcesTabEvents sender, int tabIndex) {
+            if (disabled || waitingForResponse) {
+                return false;
+            }
+            final Object tabKey = tabKeys.get(tabIndex);
+            if (disabledTabKeys.contains(tabKey)) {
+                return false;
+            }
+
+            return true;
+        }
+
+    };
+
+    public ITabsheet() {
+        super(CLASSNAME);
+
+        // Tab scrolling
+        DOM.setStyleAttribute(getElement(), "overflow", "hidden");
+        tabs = DOM.createDiv();
+        DOM.setElementProperty(tabs, "className", TABS_CLASSNAME);
+        scroller = DOM.createDiv();
+
+        DOM.setElementProperty(scroller, "className", SCROLLER_CLASSNAME);
+        scrollerPrev = DOM.createButton();
+        DOM.setElementProperty(scrollerPrev, "className", SCROLLER_CLASSNAME
+                + "Prev");
+        DOM.sinkEvents(scrollerPrev, Event.ONCLICK);
+        scrollerNext = DOM.createButton();
+        DOM.setElementProperty(scrollerNext, "className", SCROLLER_CLASSNAME
+                + "Next");
+        DOM.sinkEvents(scrollerNext, Event.ONCLICK);
+        DOM.appendChild(getElement(), tabs);
+
+        // Tabs
+        tb = new TabBar();
+        tp = new ITabsheetPanel();
+        tp.setStyleName(CLASSNAME + "-tabsheetpanel");
+        contentNode = DOM.createDiv();
+
+        deco = DOM.createDiv();
+
+        addStyleDependentName("loading"); // Indicate initial progress
+        tb.setStyleName(CLASSNAME + "-tabs");
+        DOM
+                .setElementProperty(contentNode, "className", CLASSNAME
+                        + "-content");
+        DOM.setElementProperty(deco, "className", CLASSNAME + "-deco");
+
+        add(tb, tabs);
+        DOM.appendChild(scroller, scrollerPrev);
+        DOM.appendChild(scroller, scrollerNext);
+
+        DOM.appendChild(getElement(), contentNode);
+        add(tp, contentNode);
+        DOM.appendChild(getElement(), deco);
+
+        DOM.appendChild(tabs, scroller);
+
+        tb.addTabListener(tl);
+
+        // TODO Use for Safari only. Fix annoying 1px first cell in TabBar.
+        DOM.setStyleAttribute(DOM.getFirstChild(DOM.getFirstChild(DOM
+                .getFirstChild(tb.getElement()))), "display", "none");
+
+    }
+
+    public void onBrowserEvent(Event event) {
+
+        // Tab scrolling
+        if (isScrolledTabs()
+                && DOM.compare(DOM.eventGetTarget(event), scrollerPrev)) {
+            if (scrollerIndex > 0) {
+                DOM.setStyleAttribute(DOM.getChild(DOM.getFirstChild(DOM
+                        .getFirstChild(tb.getElement())), scrollerIndex),
+                        "display", "");
+                scrollerIndex--;
+                updateTabScroller();
+            }
+        } else if (isClippedTabs()
+                && DOM.compare(DOM.eventGetTarget(event), scrollerNext)) {
+            int tabs = tb.getTabCount();
+            if (scrollerIndex + 1 <= tabs) {
+                scrollerIndex++;
+                DOM.setStyleAttribute(DOM.getChild(DOM.getFirstChild(DOM
+                        .getFirstChild(tb.getElement())), scrollerIndex),
+                        "display", "none");
+                updateTabScroller();
+            }
+        } else {
+            super.onBrowserEvent(event);
+        }
+    }
+
+    public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+        super.updateFromUIDL(uidl, client);
+
+        // Add proper stylenames for all elements (easier to prevent unwanted
+        // style inheritance)
+        if (uidl.hasAttribute("style")) {
+            final String[] styles = uidl.getStringAttribute("style").split(" ");
+            final String contentBaseClass = CLASSNAME + "-content";
+            String contentClass = contentBaseClass;
+            final String decoBaseClass = CLASSNAME + "-deco";
+            String decoClass = decoBaseClass;
+            for (int i = 0; i < styles.length; i++) {
+                tb.addStyleDependentName(styles[i]);
+                contentClass += " " + contentBaseClass + "-" + styles[i];
+                decoClass += " " + decoBaseClass + "-" + styles[i];
+            }
+            DOM.setElementProperty(contentNode, "className", contentClass);
+            DOM.setElementProperty(deco, "className", decoClass);
+        } else {
+            tb.setStyleName(CLASSNAME + "-tabs");
+            DOM.setElementProperty(contentNode, "className", CLASSNAME
+                    + "-content");
+            DOM.setElementProperty(deco, "className", CLASSNAME + "-deco");
+        }
+
+        if (uidl.hasAttribute("hidetabs")) {
+            tb.setVisible(false);
+            addStyleName(CLASSNAME + "-hidetabs");
+        } else {
+            tb.setVisible(true);
+            removeStyleName(CLASSNAME + "-hidetabs");
+        }
+
+        // tabs; push or not
+        if (uidl.hasAttribute("width")) {
+            // update width later, in updateTabScroller();
+            DOM.setStyleAttribute(tabs, "width", "1px");
+            DOM.setStyleAttribute(tabs, "overflow", "hidden");
+        } else {
+            showAllTabs();
+            DOM.setStyleAttribute(tabs, "width", "");
+            DOM.setStyleAttribute(tabs, "overflow", "visible");
+        }
+
+        updateTabScroller();
+        waitingForResponse = false;
+    }
+
+    protected void renderTab(final UIDL tabUidl, int index, boolean selected) {
+        // TODO check indexes, now new tabs get placed last (changing tab order
+        // is not supported from server-side)
+        Caption c = new Caption(null, client);
+        c.updateCaption(tabUidl);
+        tb.addTab(c);
+        if (selected) {
+            renderContent(tabUidl.getChildUIDL(0));
+            tb.selectTab(index);
+        } else if (tabUidl.getChildCount() > 0) {
+            // updating a drawn child on hidden tab
+            Paintable paintable = client.getPaintable(tabUidl.getChildUIDL(0));
+            paintable.updateFromUIDL(tabUidl.getChildUIDL(0), client);
+        }
+        // Add place-holder content
+        tp.add(new Label(""));
+    }
+
+    protected void selectTab(int index, final UIDL contentUidl) {
+        if (index != activeTabIndex) {
+            activeTabIndex = index;
+            tb.selectTab(activeTabIndex);
+        }
+        renderContent(contentUidl);
+    }
+
+    private void renderContent(final UIDL contentUIDL) {
+        final Paintable content = client.getPaintable(contentUIDL);
+        if (tp.getWidgetCount() > activeTabIndex) {
+            Widget old = tp.getWidget(activeTabIndex);
+            if (old != content) {
+                tp.remove(activeTabIndex);
+                if (old instanceof Paintable) {
+                    client.unregisterPaintable((Paintable) old);
+                }
+                tp.insert((Widget) content, activeTabIndex);
+            }
+        } else {
+            tp.add((Widget) content);
+        }
+
+        tp.showWidget(activeTabIndex);
+
+        ITabsheet.this.iLayout();
+        (content).updateFromUIDL(contentUIDL, client);
+        ITabsheet.this.removeStyleDependentName("loading");
+        if (previousVisibleWidget != null) {
+            DOM.setStyleAttribute(previousVisibleWidget.getElement(),
+                    "visibility", "");
+            previousVisibleWidget = null;
+        }
+    }
+
+    public void setHeight(String height) {
+        if (this.height == null && height == null) {
+            return;
+        }
+        String oldHeight = this.height;
+        this.height = height;
+        if ((this.height != null && height == null)
+                || (this.height == null && height != null)
+                || !height.equals(oldHeight)) {
+            iLayout();
+        }
+    }
+
+    public void setWidth(String width) {
+        String oldWidth = this.width;
+        this.width = width;
+        if ("100%".equals(width)) {
+            // Allow browser to calculate width
+            super.setWidth("");
+        } else {
+            super.setWidth(width);
+        }
+        if ((this.width != null && width == null)
+                || (this.width == null && width != null)
+                || !width.equals(oldWidth)) {
+            // Run descendant layout functions
+            Util.runDescendentsLayout(this);
+        }
+    }
+
+    public void iLayout() {
+        if (height != null && height != "") {
+            super.setHeight(height);
+
+            final int contentHeight = getOffsetHeight()
+                    - DOM.getElementPropertyInt(deco, "offsetHeight")
+                    - tb.getOffsetHeight();
+
+            // Set proper values for content element
+            DOM.setStyleAttribute(contentNode, "height", contentHeight + "px");
+            DOM.setStyleAttribute(contentNode, "overflow", "auto");
+            tp.setHeight("100%");
+
+        } else {
+            DOM.setStyleAttribute(contentNode, "height", "");
+            DOM.setStyleAttribute(contentNode, "overflow", "");
+        }
+        Util.runDescendentsLayout(this);
+
+        updateTabScroller();
+    }
+
+    /**
+     * Layouts the tab-scroller elements, and applies styles.
+     */
+    private void updateTabScroller() {
+        if (width != null) {
+            DOM.setStyleAttribute(tabs, "width", width);
+        }
+        if (scrollerIndex > tb.getTabCount()) {
+            scrollerIndex = 0;
+        }
+        boolean scrolled = isScrolledTabs();
+        boolean clipped = isClippedTabs();
+        if (tb.isVisible() && (scrolled || clipped)) {
+            DOM.setStyleAttribute(scroller, "display", "");
+            DOM.setElementProperty(scrollerPrev, "className",
+                    SCROLLER_CLASSNAME + (scrolled ? "Prev" : "Prev-disabled"));
+            DOM.setElementProperty(scrollerNext, "className",
+                    SCROLLER_CLASSNAME + (clipped ? "Next" : "Next-disabled"));
+        } else {
+            DOM.setStyleAttribute(scroller, "display", "none");
+        }
+
+    }
+
+    private void showAllTabs() {
+        scrollerIndex = 0;
+        for (int i = 0; i < tb.getTabCount(); i++) {
+            DOM.setStyleAttribute(DOM.getChild(DOM.getFirstChild(DOM
+                    .getFirstChild(tb.getElement())), i + 1), "display", "");
+        }
+    }
+
+    private boolean isScrolledTabs() {
+        return scrollerIndex > 0;
+    }
+
+    private boolean isClippedTabs() {
+        return tb.getOffsetWidth() > getOffsetWidth();
+    }
+
+    protected void clearPaintables() {
+
+        int i = tb.getTabCount();
+        while (i > 0) {
+            tb.removeTab(--i);
+        }
+        tp.clear();
+
+        // Get rid of unnecessary 100% cell heights in TabBar (really ugly hack)
+        final Element tr = DOM.getChild(DOM.getChild(tb.getElement(), 0), 0);
+        final Element rest = DOM.getChild(DOM.getChild(tr, DOM
+                .getChildCount(tr) - 1), 0);
+        DOM.removeElementAttribute(rest, "style");
+
+    }
+
+    protected Iterator getPaintableIterator() {
+        return tp.iterator();
+    }
+}
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/ITabsheetBase.java b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/ITabsheetBase.java
new file mode 100644 (file)
index 0000000..05a77cf
--- /dev/null
@@ -0,0 +1,157 @@
+package com.itmill.toolkit.terminal.gwt.client.ui;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Set;
+
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.ui.ComplexPanel;
+import com.google.gwt.user.client.ui.Widget;
+import com.itmill.toolkit.terminal.gwt.client.ApplicationConnection;
+import com.itmill.toolkit.terminal.gwt.client.Paintable;
+import com.itmill.toolkit.terminal.gwt.client.UIDL;
+
+abstract class ITabsheetBase extends ComplexPanel implements Paintable {
+
+    String id;
+    ApplicationConnection client;
+
+    protected final ArrayList tabKeys = new ArrayList();
+    protected final ArrayList captions = new ArrayList();
+    protected int activeTabIndex = 0;
+    protected boolean disabled;
+    protected boolean readonly;
+    protected Set disabledTabKeys = new HashSet();
+
+    public ITabsheetBase(String classname) {
+        setElement(DOM.createDiv());
+        setStylePrimaryName(classname);
+    }
+
+    public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+
+        // Ensure correct implementation
+        if (client.updateComponent(this, uidl, true)) {
+            return;
+        }
+
+        // Update member references
+        this.client = client;
+        id = uidl.getId();
+        disabled = uidl.hasAttribute("disabled");
+
+        // Render content
+        final UIDL tabs = uidl.getChildUIDL(0);
+        if (keepCurrentTabs(uidl)) {
+            int index = 0;
+            for (final Iterator it = tabs.getChildIterator(); it.hasNext();) {
+                final UIDL tab = (UIDL) it.next();
+                final boolean selected = tab.getBooleanAttribute("selected");
+                if (selected) {
+                    selectTab(index, tab.getChildUIDL(0));
+                } else if (tab.getChildCount() > 0) {
+                    // updating a drawn child on hidden tab
+                    Paintable paintable = client.getPaintable(tab
+                            .getChildUIDL(0));
+                    // TODO widget may flash on screen
+                    paintable.updateFromUIDL(tab.getChildUIDL(0), client);
+                    // Hack #1 in ITabsheetBase: due ITabsheets content has no
+                    // wrappers for each tab, we need to hide the actual widgets
+                    ((Widget) paintable).setVisible(false);
+                }
+                index++;
+            }
+        } else {
+
+            ArrayList oldPaintables = new ArrayList();
+            for (Iterator iterator = getPaintableIterator(); iterator.hasNext();) {
+                oldPaintables.add(iterator.next());
+            }
+
+            // Clear previous values
+            tabKeys.clear();
+            captions.clear();
+            disabledTabKeys.clear();
+
+            clearPaintables();
+
+            int index = 0;
+            for (final Iterator it = tabs.getChildIterator(); it.hasNext();) {
+                final UIDL tab = (UIDL) it.next();
+                final String key = tab.getStringAttribute("key");
+                final boolean selected = tab.getBooleanAttribute("selected");
+                String caption = tab.getStringAttribute("caption");
+                if (caption == null) {
+                    caption = " ";
+                }
+
+                if (tab.getBooleanAttribute("disabled")) {
+                    disabledTabKeys.add(key);
+                }
+
+                captions.add(caption);
+                tabKeys.add(key);
+
+                if (selected) {
+                    activeTabIndex = index;
+                }
+                if (tab.getChildCount() > 0) {
+                    Paintable p = client.getPaintable(tab.getChildUIDL(0));
+                    oldPaintables.remove(p);
+                }
+                renderTab(tab, index, selected);
+                index++;
+            }
+
+            for (Iterator iterator = oldPaintables.iterator(); iterator
+                    .hasNext();) {
+                Object oldPaintable = iterator.next();
+                if (oldPaintable instanceof Paintable) {
+                    client.unregisterPaintable((Paintable) oldPaintable);
+                }
+            }
+        }
+
+    }
+
+    /**
+     * @return a list of currently shown Paintables
+     */
+    abstract protected Iterator getPaintableIterator();
+
+    /**
+     * Clears current tabs and contents
+     */
+    abstract protected void clearPaintables();
+
+    protected boolean keepCurrentTabs(UIDL uidl) {
+        final UIDL tabs = uidl.getChildUIDL(0);
+        boolean retval = tabKeys.size() == tabs.getNumberOfChildren();
+        for (int i = 0; retval && i < tabKeys.size(); i++) {
+            String key = (String) tabKeys.get(i);
+            UIDL tabUIDL = tabs.getChildUIDL(i);
+            retval = key.equals(tabUIDL.getStringAttribute("key"))
+                    && captions.get(i).equals(
+                            tabUIDL.getStringAttribute("caption"))
+                    && (tabUIDL.hasAttribute("disabled") == disabledTabKeys
+                            .contains(key));
+        }
+        return retval;
+    }
+
+    /**
+     * Implement in extending classes. This method should render needed elements
+     * and set the visibility of the tab according to the 'selected' parameter.
+     */
+    protected abstract void renderTab(final UIDL tabUidl, int index,
+            boolean selected);
+
+    /**
+     * Implement in extending classes. This method should render any previously
+     * non-cached content and set the activeTabIndex property to the specified
+     * index.
+     */
+    protected abstract void selectTab(int index, final UIDL contentUidl);
+
+}
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/ITabsheetPanel.java b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/ITabsheetPanel.java
new file mode 100644 (file)
index 0000000..01ac636
--- /dev/null
@@ -0,0 +1,112 @@
+/* \r
+@ITMillApache2LicenseForJavaFiles@\r
+ */\r
+\r
+package com.itmill.toolkit.terminal.gwt.client.ui;\r
+\r
+import com.google.gwt.user.client.DOM;\r
+import com.google.gwt.user.client.ui.ComplexPanel;\r
+import com.google.gwt.user.client.ui.Widget;\r
+\r
+/**\r
+ * A panel that displays all of its child widgets in a 'deck', where only one\r
+ * can be visible at a time. It is used by\r
+ * {@link com.itmill.toolkit.terminal.gwt.client.ui.ITabsheetPanel}.\r
+ * \r
+ * This class has the same basic functionality as the GWT DeckPanel\r
+ * {@link com.google.gwt.user.client.ui.DeckPanel}, with the exception that it\r
+ * doesn't manipulate the child widgets' width and height attributes.\r
+ */\r
+public class ITabsheetPanel extends ComplexPanel {\r
+\r
+    private Widget visibleWidget;\r
+\r
+    /**\r
+     * Creates an empty tabsheet panel.\r
+     */\r
+    public ITabsheetPanel() {\r
+        setElement(DOM.createDiv());\r
+    }\r
+\r
+    /**\r
+     * Adds the specified widget to the deck.\r
+     * \r
+     * @param w\r
+     *                the widget to be added\r
+     */\r
+    public void add(Widget w) {\r
+        super.add(w, getElement());\r
+        initChildWidget(w);\r
+    }\r
+\r
+    /**\r
+     * Gets the index of the currently-visible widget.\r
+     * \r
+     * @return the visible widget's index\r
+     */\r
+    public int getVisibleWidget() {\r
+        return getWidgetIndex(visibleWidget);\r
+    }\r
+\r
+    /**\r
+     * Inserts a widget before the specified index.\r
+     * \r
+     * @param w\r
+     *                the widget to be inserted\r
+     * @param beforeIndex\r
+     *                the index before which it will be inserted\r
+     * @throws IndexOutOfBoundsException\r
+     *                 if <code>beforeIndex</code> is out of range\r
+     */\r
+    public void insert(Widget w, int beforeIndex) {\r
+        super.insert(w, getElement(), beforeIndex, true);\r
+        initChildWidget(w);\r
+    }\r
+\r
+    public boolean remove(Widget w) {\r
+        final boolean removed = super.remove(w);\r
+        if (removed) {\r
+            resetChildWidget(w);\r
+\r
+            if (visibleWidget == w) {\r
+                visibleWidget = null;\r
+            }\r
+        }\r
+        return removed;\r
+    }\r
+\r
+    /**\r
+     * Shows the widget at the specified index. This causes the currently-\r
+     * visible widget to be hidden.\r
+     * \r
+     * @param index\r
+     *                the index of the widget to be shown\r
+     */\r
+    public void showWidget(int index) {\r
+        checkIndexBoundsForAccess(index);\r
+        Widget newVisible = getWidget(index);\r
+        if (visibleWidget != newVisible) {\r
+            if (visibleWidget != null) {\r
+                visibleWidget.setVisible(false);\r
+            }\r
+            visibleWidget = newVisible;\r
+            visibleWidget.setVisible(true);\r
+        }\r
+    }\r
+\r
+    /**\r
+     * Make the widget invisible, and set its width and height to full.\r
+     */\r
+    private void initChildWidget(Widget w) {\r
+        w.setVisible(false);\r
+    }\r
+\r
+    /**\r
+     * Make the widget visible, and clear the widget's width and height\r
+     * attributes. This is done so that any changes to the visibility, height,\r
+     * or width of the widget that were done by the panel are undone.\r
+     */\r
+    private void resetChildWidget(Widget w) {\r
+        w.setVisible(true);\r
+    }\r
+}\r
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/ITextArea.java b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/ITextArea.java
new file mode 100644 (file)
index 0000000..94bccd7
--- /dev/null
@@ -0,0 +1,50 @@
+/* \r
+@ITMillApache2LicenseForJavaFiles@\r
+ */\r
+\r
+package com.itmill.toolkit.terminal.gwt.client.ui;\r
+\r
+import com.google.gwt.user.client.DOM;\r
+import com.google.gwt.user.client.Element;\r
+import com.itmill.toolkit.terminal.gwt.client.ApplicationConnection;\r
+import com.itmill.toolkit.terminal.gwt.client.UIDL;\r
+\r
+/**\r
+ * This class represents a multiline textfield (textarea).\r
+ * \r
+ * TODO consider replacing this with a RichTextArea based implementation. IE\r
+ * does not support CSS height for textareas in Strict mode :-(\r
+ * \r
+ * @author IT Mill Ltd.\r
+ * \r
+ */\r
+public class ITextArea extends ITextField {\r
+    public static final String CLASSNAME = "i-textarea";\r
+\r
+    public ITextArea() {\r
+        super(DOM.createTextArea());\r
+        setStyleName(CLASSNAME);\r
+    }\r
+\r
+    public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {\r
+        // Call parent renderer explicitly\r
+        super.updateFromUIDL(uidl, client);\r
+\r
+        if (uidl.hasAttribute("rows")) {\r
+            setRows(new Integer(uidl.getStringAttribute("rows")).intValue());\r
+        }\r
+    }\r
+\r
+    public void setRows(int rows) {\r
+        setRows(getElement(), rows);\r
+    }\r
+\r
+    private native void setRows(Element e, int r)\r
+    /*-{\r
+    try {\r
+        if(e.tagName.toLowerCase() == "textarea")\r
+                e.rows = r;\r
+    } catch (e) {}\r
+    }-*/;\r
+\r
+}\r
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/ITextField.java b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/ITextField.java
new file mode 100644 (file)
index 0000000..b024e37
--- /dev/null
@@ -0,0 +1,121 @@
+/* 
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.itmill.toolkit.terminal.gwt.client.ui;
+
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.Element;
+import com.google.gwt.user.client.Event;
+import com.google.gwt.user.client.ui.ChangeListener;
+import com.google.gwt.user.client.ui.FocusListener;
+import com.google.gwt.user.client.ui.TextBoxBase;
+import com.google.gwt.user.client.ui.Widget;
+import com.itmill.toolkit.terminal.gwt.client.ApplicationConnection;
+import com.itmill.toolkit.terminal.gwt.client.Paintable;
+import com.itmill.toolkit.terminal.gwt.client.Tooltip;
+import com.itmill.toolkit.terminal.gwt.client.UIDL;
+
+/**
+ * This class represents a basic text input field with one row.
+ * 
+ * @author IT Mill Ltd.
+ * 
+ */
+public class ITextField extends TextBoxBase implements Paintable, Field,
+        ChangeListener, FocusListener {
+
+    /**
+     * The input node CSS classname.
+     */
+    public static final String CLASSNAME = "i-textfield";
+    /**
+     * This CSS classname is added to the input node on hover.
+     */
+    public static final String CLASSNAME_FOCUS = "focus";
+
+    protected String id;
+
+    protected ApplicationConnection client;
+
+    private boolean immediate = false;
+
+    public ITextField() {
+        this(DOM.createInputText());
+    }
+
+    protected ITextField(Element node) {
+        super(node);
+        setStyleName(CLASSNAME);
+        addChangeListener(this);
+        addFocusListener(this);
+        sinkEvents(Tooltip.TOOLTIP_EVENTS);
+    }
+
+    public void onBrowserEvent(Event event) {
+        super.onBrowserEvent(event);
+        if (client != null) {
+            client.handleTooltipEvent(event, this);
+        }
+    }
+
+    public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+        this.client = client;
+        id = uidl.getId();
+
+        if (client.updateComponent(this, uidl, true)) {
+            return;
+        }
+
+        if (uidl.getBooleanAttribute("readonly")) {
+            setReadOnly(true);
+        } else {
+            setReadOnly(false);
+        }
+
+        immediate = uidl.getBooleanAttribute("immediate");
+
+        if (uidl.hasAttribute("cols")) {
+            setColumns(new Integer(uidl.getStringAttribute("cols")).intValue());
+        }
+
+        setText(uidl.getStringVariable("text"));
+
+    }
+
+    public void onChange(Widget sender) {
+        if (client != null && id != null) {
+            client.updateVariable(id, "text", getText(), immediate);
+        }
+    }
+
+    public void onFocus(Widget sender) {
+        addStyleDependentName(CLASSNAME_FOCUS);
+    }
+
+    public void onLostFocus(Widget sender) {
+        removeStyleDependentName(CLASSNAME_FOCUS);
+    }
+
+    public void setColumns(int columns) {
+        setColumns(getElement(), columns);
+    }
+
+    private native void setColumns(Element e, int c)
+    /*-{
+    try {
+       switch(e.tagName.toLowerCase()) {
+               case "input":
+                       //e.size = c;
+                       e.style.width = c+"em";
+                       break;
+               case "textarea":
+                       //e.cols = c;
+                       e.style.width = c+"em";
+                       break;
+               default:;
+       }
+    } catch (e) {}
+    }-*/;
+
+}
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/ITextualDate.java b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/ITextualDate.java
new file mode 100644 (file)
index 0000000..fc67d36
--- /dev/null
@@ -0,0 +1,268 @@
+/* \r
+@ITMillApache2LicenseForJavaFiles@\r
+ */\r
+\r
+package com.itmill.toolkit.terminal.gwt.client.ui;\r
+\r
+import java.util.Date;\r
+\r
+import com.google.gwt.i18n.client.DateTimeFormat;\r
+import com.google.gwt.user.client.ui.ChangeListener;\r
+import com.google.gwt.user.client.ui.Widget;\r
+import com.itmill.toolkit.terminal.gwt.client.ApplicationConnection;\r
+import com.itmill.toolkit.terminal.gwt.client.ContainerResizedListener;\r
+import com.itmill.toolkit.terminal.gwt.client.Focusable;\r
+import com.itmill.toolkit.terminal.gwt.client.LocaleNotLoadedException;\r
+import com.itmill.toolkit.terminal.gwt.client.LocaleService;\r
+import com.itmill.toolkit.terminal.gwt.client.Paintable;\r
+import com.itmill.toolkit.terminal.gwt.client.UIDL;\r
+import com.itmill.toolkit.terminal.gwt.client.Util;\r
+\r
+public class ITextualDate extends IDateField implements Paintable, Field,\r
+        ChangeListener, ContainerResizedListener, Focusable {\r
+\r
+    private static final String PARSE_ERROR_CLASSNAME = CLASSNAME\r
+            + "-parseerror";\r
+\r
+    private final ITextField text;\r
+\r
+    private String formatStr;\r
+\r
+    private String width;\r
+\r
+    private boolean needLayout;\r
+\r
+    protected int fieldExtraWidth = -1;\r
+\r
+    public ITextualDate() {\r
+        super();\r
+        text = new ITextField();\r
+        text.addChangeListener(this);\r
+        add(text);\r
+    }\r
+\r
+    public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {\r
+\r
+        int origRes = currentResolution;\r
+        super.updateFromUIDL(uidl, client);\r
+        if (origRes != currentResolution) {\r
+            // force recreating format string\r
+            formatStr = null;\r
+        }\r
+        buildDate();\r
+    }\r
+\r
+    protected String getFormatString() {\r
+        if (formatStr == null) {\r
+            if (currentResolution == RESOLUTION_YEAR) {\r
+                formatStr = "yyyy"; // force full year\r
+            } else {\r
+\r
+                try {\r
+                    String frmString = LocaleService\r
+                            .getDateFormat(currentLocale);\r
+                    frmString = cleanFormat(frmString);\r
+                    String delim = LocaleService\r
+                            .getClockDelimiter(currentLocale);\r
+\r
+                    if (currentResolution >= RESOLUTION_HOUR) {\r
+                        if (dts.isTwelveHourClock()) {\r
+                            frmString += " hh";\r
+                        } else {\r
+                            frmString += " HH";\r
+                        }\r
+                        if (currentResolution >= RESOLUTION_MIN) {\r
+                            frmString += ":mm";\r
+                            if (currentResolution >= RESOLUTION_SEC) {\r
+                                frmString += ":ss";\r
+                                if (currentResolution >= RESOLUTION_MSEC) {\r
+                                    frmString += ".SSS";\r
+                                }\r
+                            }\r
+                        }\r
+                        if (dts.isTwelveHourClock()) {\r
+                            frmString += " aaa";\r
+                        }\r
+\r
+                    }\r
+\r
+                    formatStr = frmString;\r
+                } catch (LocaleNotLoadedException e) {\r
+                    // TODO Auto-generated catch block\r
+                    e.printStackTrace();\r
+                }\r
+            }\r
+        }\r
+        return formatStr;\r
+    }\r
+\r
+    /**\r
+     * \r
+     */\r
+    protected void buildDate() {\r
+        removeStyleName(PARSE_ERROR_CLASSNAME);\r
+        // Create the initial text for the textfield\r
+        String dateText;\r
+        if (date != null) {\r
+            dateText = DateTimeFormat.getFormat(getFormatString()).format(date);\r
+        } else {\r
+            dateText = "";\r
+        }\r
+\r
+        text.setText(dateText);\r
+        text.setEnabled(enabled && !readonly);\r
+\r
+        if (readonly) {\r
+            text.addStyleName("i-readonly");\r
+        } else {\r
+            text.removeStyleName("i-readonly");\r
+        }\r
+\r
+    }\r
+\r
+    public void onChange(Widget sender) {\r
+        if (sender == text) {\r
+            if (!text.getText().equals("")) {\r
+                try {\r
+                    date = DateTimeFormat.getFormat(getFormatString()).parse(\r
+                            text.getText());\r
+                    // remove possibly added invalid value indication\r
+                    removeStyleName(PARSE_ERROR_CLASSNAME);\r
+                } catch (final Exception e) {\r
+                    ApplicationConnection.getConsole().log(e.getMessage());\r
+                    addStyleName(PARSE_ERROR_CLASSNAME);\r
+                    client.updateVariable(id, "lastInvalidDateString", text\r
+                            .getText(), false);\r
+                    date = null;\r
+                }\r
+            } else {\r
+                date = null;\r
+                // remove possibly added invalid value indication\r
+                removeStyleName(PARSE_ERROR_CLASSNAME);\r
+            }\r
+\r
+            if (date != null) {\r
+                showingDate = new Date(date.getTime());\r
+            }\r
+\r
+            // Update variables\r
+            // (only the smallest defining resolution needs to be\r
+            // immediate)\r
+            client.updateVariable(id, "year",\r
+                    date != null ? date.getYear() + 1900 : -1,\r
+                    currentResolution == IDateField.RESOLUTION_YEAR\r
+                            && immediate);\r
+            if (currentResolution >= IDateField.RESOLUTION_MONTH) {\r
+                client.updateVariable(id, "month", date != null ? date\r
+                        .getMonth() + 1 : -1,\r
+                        currentResolution == IDateField.RESOLUTION_MONTH\r
+                                && immediate);\r
+            }\r
+            if (currentResolution >= IDateField.RESOLUTION_DAY) {\r
+                client.updateVariable(id, "day", date != null ? date.getDate()\r
+                        : -1, currentResolution == IDateField.RESOLUTION_DAY\r
+                        && immediate);\r
+            }\r
+            if (currentResolution >= IDateField.RESOLUTION_HOUR) {\r
+                client.updateVariable(id, "hour", date != null ? date\r
+                        .getHours() : -1,\r
+                        currentResolution == IDateField.RESOLUTION_HOUR\r
+                                && immediate);\r
+            }\r
+            if (currentResolution >= IDateField.RESOLUTION_MIN) {\r
+                client.updateVariable(id, "min", date != null ? date\r
+                        .getMinutes() : -1,\r
+                        currentResolution == IDateField.RESOLUTION_MIN\r
+                                && immediate);\r
+            }\r
+            if (currentResolution >= IDateField.RESOLUTION_SEC) {\r
+                client.updateVariable(id, "sec", date != null ? date\r
+                        .getSeconds() : -1,\r
+                        currentResolution == IDateField.RESOLUTION_SEC\r
+                                && immediate);\r
+            }\r
+            if (currentResolution == IDateField.RESOLUTION_MSEC) {\r
+                client.updateVariable(id, "msec",\r
+                        date != null ? getMilliseconds() : -1, immediate);\r
+            }\r
+\r
+        }\r
+    }\r
+\r
+    private String cleanFormat(String format) {\r
+        // Remove unnecessary d & M if resolution is too low\r
+        if (currentResolution < IDateField.RESOLUTION_DAY) {\r
+            format = format.replaceAll("d", "");\r
+        }\r
+        if (currentResolution < IDateField.RESOLUTION_MONTH) {\r
+            format = format.replaceAll("M", "");\r
+        }\r
+\r
+        // Remove unsupported patterns\r
+        // TODO support for 'G', era designator (used at least in Japan)\r
+        format = format.replaceAll("[GzZwWkK]", "");\r
+\r
+        // Remove extra delimiters ('/' and '.')\r
+        while (format.startsWith("/") || format.startsWith(".")\r
+                || format.startsWith("-")) {\r
+            format = format.substring(1);\r
+        }\r
+        while (format.endsWith("/") || format.endsWith(".")\r
+                || format.endsWith("-")) {\r
+            format = format.substring(0, format.length() - 1);\r
+        }\r
+\r
+        // Remove duplicate delimiters\r
+        format = format.replaceAll("//", "/");\r
+        format = format.replaceAll("\\.\\.", ".");\r
+        format = format.replaceAll("--", "-");\r
+\r
+        return format.trim();\r
+    }\r
+\r
+    public void setWidth(String newWidth) {\r
+        if (!"".equals(newWidth) && (width == null || !newWidth.equals(width))) {\r
+            if (Util.isIE6()) {\r
+                text.setColumns(1); // in IE6 cols ~ min-width\r
+            }\r
+            needLayout = true;\r
+            width = newWidth;\r
+            super.setWidth(width);\r
+            iLayout();\r
+            if (newWidth.indexOf("%") < 0) {\r
+                needLayout = false;\r
+            }\r
+        } else {\r
+            if ("".equals(newWidth) && width != null && !"".equals(width)) {\r
+                super.setWidth("");\r
+                needLayout = true;\r
+                iLayout();\r
+                needLayout = false;\r
+                width = null;\r
+            }\r
+        }\r
+    }\r
+\r
+    /**\r
+     * Returns pixels in x-axis reserved for other than textfield content.\r
+     * \r
+     * @return extra width in pixels\r
+     */\r
+    protected int getFieldExtraWidth() {\r
+        if (fieldExtraWidth < 0) {\r
+            text.setWidth("0px");\r
+            fieldExtraWidth = text.getOffsetWidth();\r
+        }\r
+        return fieldExtraWidth;\r
+    }\r
+\r
+    public void iLayout() {\r
+        if (needLayout) {\r
+            text.setWidth((getOffsetWidth() - getFieldExtraWidth()) + "px");\r
+        }\r
+    }\r
+\r
+    public void focus() {\r
+        text.setFocus(true);\r
+    }\r
+}\r
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/ITree.java b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/ITree.java
new file mode 100644 (file)
index 0000000..2169a41
--- /dev/null
@@ -0,0 +1,409 @@
+/* 
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.itmill.toolkit.terminal.gwt.client.ui;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Set;
+
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.Element;
+import com.google.gwt.user.client.Event;
+import com.google.gwt.user.client.Window;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.SimplePanel;
+import com.itmill.toolkit.terminal.gwt.client.ApplicationConnection;
+import com.itmill.toolkit.terminal.gwt.client.Paintable;
+import com.itmill.toolkit.terminal.gwt.client.UIDL;
+import com.itmill.toolkit.terminal.gwt.client.Util;
+
+/**
+ * 
+ */
+public class ITree extends FlowPanel implements Paintable {
+
+    public static final String CLASSNAME = "i-tree";
+
+    private Set selectedIds = new HashSet();
+    private ApplicationConnection client;
+    private String paintableId;
+    private boolean selectable;
+    private boolean isMultiselect;
+
+    private final HashMap keyToNode = new HashMap();
+
+    /**
+     * This map contains captions and icon urls for actions like: * "33_c" ->
+     * "Edit" * "33_i" -> "http://dom.com/edit.png"
+     */
+    private final HashMap actionMap = new HashMap();
+
+    private boolean immediate;
+
+    private boolean isNullSelectionAllowed = true;
+
+    private boolean disabled = false;
+
+    private boolean readonly;
+
+    public ITree() {
+        super();
+        setStyleName(CLASSNAME);
+    }
+
+    private void updateActionMap(UIDL c) {
+        final Iterator it = c.getChildIterator();
+        while (it.hasNext()) {
+            final UIDL action = (UIDL) it.next();
+            final String key = action.getStringAttribute("key");
+            final String caption = action.getStringAttribute("caption");
+            actionMap.put(key + "_c", caption);
+            if (action.hasAttribute("icon")) {
+                // TODO need some uri handling ??
+                actionMap.put(key + "_i", client.translateToolkitUri(action
+                        .getStringAttribute("icon")));
+            }
+        }
+
+    }
+
+    public String getActionCaption(String actionKey) {
+        return (String) actionMap.get(actionKey + "_c");
+    }
+
+    public String getActionIcon(String actionKey) {
+        return (String) actionMap.get(actionKey + "_i");
+    }
+
+    public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+        // Ensure correct implementation and let container manage caption
+        if (client.updateComponent(this, uidl, true)) {
+            return;
+        }
+
+        this.client = client;
+
+        if (uidl.hasAttribute("partialUpdate")) {
+            handleUpdate(uidl);
+            return;
+        }
+
+        paintableId = uidl.getId();
+
+        immediate = uidl.hasAttribute("immediate");
+
+        disabled = uidl.getBooleanAttribute("disabled");
+        readonly = uidl.getBooleanAttribute("readonly");
+
+        isNullSelectionAllowed = uidl.getBooleanAttribute("nullselect");
+
+        clear();
+        for (final Iterator i = uidl.getChildIterator(); i.hasNext();) {
+            final UIDL childUidl = (UIDL) i.next();
+            if ("actions".equals(childUidl.getTag())) {
+                updateActionMap(childUidl);
+                continue;
+            }
+            final TreeNode childTree = new TreeNode();
+            this.add(childTree);
+            childTree.updateFromUIDL(childUidl, client);
+        }
+        final String selectMode = uidl.getStringAttribute("selectmode");
+        selectable = !"none".equals(selectMode);
+        isMultiselect = "multi".equals(selectMode);
+
+        selectedIds = uidl.getStringArrayVariableAsSet("selected");
+
+    }
+
+    private void handleUpdate(UIDL uidl) {
+        final TreeNode rootNode = (TreeNode) keyToNode.get(uidl
+                .getStringAttribute("rootKey"));
+        if (rootNode != null) {
+            if (!rootNode.getState()) {
+                // expanding node happened server side
+                rootNode.setState(true, false);
+            }
+            rootNode.renderChildNodes(uidl.getChildIterator());
+        }
+
+    }
+
+    public void setSelected(TreeNode treeNode, boolean selected) {
+        if (selected) {
+            if (!isMultiselect) {
+                while (selectedIds.size() > 0) {
+                    final String id = (String) selectedIds.iterator().next();
+                    final TreeNode oldSelection = (TreeNode) keyToNode.get(id);
+                    if (oldSelection != null) {
+                        // can be null if the node is not visible (parent
+                        // expanded)
+                        oldSelection.setSelected(false);
+                    }
+                    selectedIds.remove(id);
+                }
+            }
+            treeNode.setSelected(true);
+            selectedIds.add(treeNode.key);
+        } else {
+            if (!isNullSelectionAllowed) {
+                if (!isMultiselect || selectedIds.size() == 1) {
+                    return;
+                }
+            }
+            selectedIds.remove(treeNode.key);
+            treeNode.setSelected(false);
+        }
+        client.updateVariable(paintableId, "selected", selectedIds.toArray(),
+                immediate);
+    }
+
+    public boolean isSelected(TreeNode treeNode) {
+        return selectedIds.contains(treeNode.key);
+    }
+
+    protected class TreeNode extends SimplePanel implements ActionOwner {
+
+        public static final String CLASSNAME = "i-tree-node";
+
+        String key;
+
+        private String[] actionKeys = null;
+
+        private boolean childrenLoaded;
+
+        private Element nodeCaptionDiv;
+
+        protected Element nodeCaptionSpan;
+
+        private FlowPanel childNodeContainer;
+
+        private boolean open;
+
+        private Icon icon;
+
+        public TreeNode() {
+            constructDom();
+            sinkEvents(Event.ONCLICK);
+        }
+
+        public void onBrowserEvent(Event event) {
+            super.onBrowserEvent(event);
+            if (disabled) {
+                return;
+            }
+            final Element target = DOM.eventGetTarget(event);
+            if (DOM.compare(getElement(), target)) {
+                // state change
+                toggleState();
+            } else if (!readonly && DOM.compare(target, nodeCaptionSpan)) {
+                // caption click = selection change
+                toggleSelection();
+            }
+
+        }
+
+        private void toggleSelection() {
+            if (selectable) {
+                ITree.this.setSelected(this, !isSelected());
+            }
+        }
+
+        private void toggleState() {
+            setState(!getState(), true);
+        }
+
+        protected void constructDom() {
+            nodeCaptionDiv = DOM.createDiv();
+            DOM.setElementProperty(nodeCaptionDiv, "className", CLASSNAME
+                    + "-caption");
+            nodeCaptionSpan = DOM.createSpan();
+            DOM.appendChild(getElement(), nodeCaptionDiv);
+            DOM.appendChild(nodeCaptionDiv, nodeCaptionSpan);
+
+            childNodeContainer = new FlowPanel();
+            childNodeContainer.setStylePrimaryName(CLASSNAME + "-children");
+            setWidget(childNodeContainer);
+        }
+
+        public void onDetach() {
+            Util.removeContextMenuEvent(nodeCaptionSpan);
+            super.onDetach();
+        }
+
+        public void onAttach() {
+            attachContextMenuEvent(nodeCaptionSpan);
+            super.onAttach();
+        }
+
+        public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+            setText(uidl.getStringAttribute("caption"));
+            key = uidl.getStringAttribute("key");
+
+            keyToNode.put(key, this);
+
+            if (uidl.hasAttribute("al")) {
+                actionKeys = uidl.getStringArrayAttribute("al");
+            }
+
+            if (uidl.getTag().equals("node")) {
+                if (uidl.getChildCount() == 0) {
+                    childNodeContainer.setVisible(false);
+                } else {
+                    renderChildNodes(uidl.getChildIterator());
+                    childrenLoaded = true;
+                }
+            } else {
+                addStyleName(CLASSNAME + "-leaf");
+            }
+            addStyleName(CLASSNAME);
+
+            if (uidl.getBooleanAttribute("expanded") && !getState()) {
+                setState(true, false);
+            }
+
+            if (uidl.getBooleanAttribute("selected")) {
+                setSelected(true);
+            }
+
+            if (uidl.hasAttribute("icon")) {
+                if (icon == null) {
+                    icon = new Icon(client);
+                    DOM.insertBefore(nodeCaptionDiv, icon.getElement(),
+                            nodeCaptionSpan);
+                }
+                icon.setUri(uidl.getStringAttribute("icon"));
+            } else {
+                if (icon != null) {
+                    DOM.removeChild(nodeCaptionDiv, icon.getElement());
+                    icon = null;
+                }
+            }
+        }
+
+        private void setState(boolean state, boolean notifyServer) {
+            if (open == state) {
+                return;
+            }
+            if (state) {
+                if (!childrenLoaded && notifyServer) {
+                    client.updateVariable(paintableId, "requestChildTree",
+                            true, false);
+                }
+                if (notifyServer) {
+                    client.updateVariable(paintableId, "expand",
+                            new String[] { key }, true);
+                }
+                addStyleName(CLASSNAME + "-expanded");
+                childNodeContainer.setVisible(true);
+            } else {
+                removeStyleName(CLASSNAME + "-expanded");
+                childNodeContainer.setVisible(false);
+                if (notifyServer) {
+                    client.updateVariable(paintableId, "collapse",
+                            new String[] { key }, true);
+                }
+            }
+
+            open = state;
+        }
+
+        private boolean getState() {
+            return open;
+        }
+
+        private void setText(String text) {
+            DOM.setInnerText(nodeCaptionSpan, text);
+        }
+
+        private void renderChildNodes(Iterator i) {
+            childNodeContainer.clear();
+            childNodeContainer.setVisible(true);
+            while (i.hasNext()) {
+                final UIDL childUidl = (UIDL) i.next();
+                // actions are in bit weird place, don't mix them with children,
+                // but current node's actions
+                if ("actions".equals(childUidl.getTag())) {
+                    updateActionMap(childUidl);
+                    continue;
+                }
+                final TreeNode childTree = new TreeNode();
+                childNodeContainer.add(childTree);
+                childTree.updateFromUIDL(childUidl, client);
+            }
+            childrenLoaded = true;
+        }
+
+        public boolean isChildrenLoaded() {
+            return childrenLoaded;
+        }
+
+        public Action[] getActions() {
+            if (actionKeys == null) {
+                return new Action[] {};
+            }
+            final Action[] actions = new Action[actionKeys.length];
+            for (int i = 0; i < actions.length; i++) {
+                final String actionKey = actionKeys[i];
+                final TreeAction a = new TreeAction(this, String.valueOf(key),
+                        actionKey);
+                a.setCaption(getActionCaption(actionKey));
+                a.setIconUrl(getActionIcon(actionKey));
+                actions[i] = a;
+            }
+            return actions;
+        }
+
+        public ApplicationConnection getClient() {
+            return client;
+        }
+
+        public String getPaintableId() {
+            return paintableId;
+        }
+
+        /**
+         * Adds/removes IT Mill Toolkit spesific style name. This method ought
+         * to be called only from Tree.
+         * 
+         * @param selected
+         */
+        public void setSelected(boolean selected) {
+            // add style name to caption dom structure only, not to subtree
+            setStyleName(nodeCaptionDiv, "i-tree-node-selected", selected);
+        }
+
+        public boolean isSelected() {
+            return ITree.this.isSelected(this);
+        }
+
+        public void showContextMenu(Event event) {
+            if (!readonly && !disabled) {
+                if (actionKeys != null) {
+                    int left = DOM.eventGetClientX(event);
+                    int top = DOM.eventGetClientY(event);
+                    top += Window.getScrollTop();
+                    left += Window.getScrollLeft();
+                    client.getContextMenu().showAt(this, left, top);
+                }
+                DOM.eventCancelBubble(event, true);
+            }
+
+        }
+
+        private native void attachContextMenuEvent(Element el)
+        /*-{
+               var node = this;
+               el.oncontextmenu = function(e) {
+                       if(!e)
+                               e = $wnd.event;
+                       node.@com.itmill.toolkit.terminal.gwt.client.ui.ITree.TreeNode::showContextMenu(Lcom/google/gwt/user/client/Event;)(e);
+                       return false;
+               };
+        }-*/;
+
+    }
+}
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/ITwinColSelect.java b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/ITwinColSelect.java
new file mode 100644 (file)
index 0000000..40aae9d
--- /dev/null
@@ -0,0 +1,220 @@
+/* \r
+@ITMillApache2LicenseForJavaFiles@\r
+ */\r
+\r
+package com.itmill.toolkit.terminal.gwt.client.ui;\r
+\r
+import java.util.Iterator;\r
+import java.util.Vector;\r
+\r
+import com.google.gwt.user.client.DOM;\r
+import com.google.gwt.user.client.ui.FlowPanel;\r
+import com.google.gwt.user.client.ui.HTML;\r
+import com.google.gwt.user.client.ui.ListBox;\r
+import com.google.gwt.user.client.ui.Panel;\r
+import com.google.gwt.user.client.ui.Widget;\r
+import com.itmill.toolkit.terminal.gwt.client.UIDL;\r
+\r
+public class ITwinColSelect extends IOptionGroupBase {\r
+\r
+    private static final String CLASSNAME = "i-select-twincol";\r
+\r
+    private static final int VISIBLE_COUNT = 10;\r
+\r
+    private static final int DEFAULT_COLUMN_COUNT = 10;\r
+\r
+    private final ListBox options;\r
+\r
+    private final ListBox selections;\r
+\r
+    private final IButton add;\r
+\r
+    private final IButton remove;\r
+\r
+    private FlowPanel buttons;\r
+\r
+    private Panel panel;\r
+\r
+    private boolean widthSet = false;\r
+\r
+    public ITwinColSelect() {\r
+        super(CLASSNAME);\r
+        options = new ListBox();\r
+        options.addClickListener(this);\r
+        selections = new ListBox();\r
+        selections.addClickListener(this);\r
+        options.setVisibleItemCount(VISIBLE_COUNT);\r
+        selections.setVisibleItemCount(VISIBLE_COUNT);\r
+        options.setStyleName(CLASSNAME + "-options");\r
+        selections.setStyleName(CLASSNAME + "-selections");\r
+        buttons = new FlowPanel();\r
+        buttons.setStyleName(CLASSNAME + "-buttons");\r
+        add = new IButton();\r
+        add.setText(">>");\r
+        add.addClickListener(this);\r
+        remove = new IButton();\r
+        remove.setText("<<");\r
+        remove.addClickListener(this);\r
+        panel = ((Panel) optionsContainer);\r
+        panel.add(options);\r
+        buttons.add(add);\r
+        final HTML br = new HTML("<span/>");\r
+        br.setStyleName(CLASSNAME + "-deco");\r
+        buttons.add(br);\r
+        buttons.add(remove);\r
+        panel.add(buttons);\r
+        panel.add(selections);\r
+    }\r
+\r
+    protected void buildOptions(UIDL uidl) {\r
+        final boolean enabled = !isDisabled() && !isReadonly();\r
+        options.setMultipleSelect(isMultiselect());\r
+        selections.setMultipleSelect(isMultiselect());\r
+        options.setEnabled(enabled);\r
+        selections.setEnabled(enabled);\r
+        add.setEnabled(enabled);\r
+        remove.setEnabled(enabled);\r
+        options.clear();\r
+        selections.clear();\r
+        for (final Iterator i = uidl.getChildIterator(); i.hasNext();) {\r
+            final UIDL optionUidl = (UIDL) i.next();\r
+            if (optionUidl.hasAttribute("selected")) {\r
+                selections.addItem(optionUidl.getStringAttribute("caption"),\r
+                        optionUidl.getStringAttribute("key"));\r
+            } else {\r
+                options.addItem(optionUidl.getStringAttribute("caption"),\r
+                        optionUidl.getStringAttribute("key"));\r
+            }\r
+        }\r
+        if (getColumns() > 0) {\r
+            options.setWidth(getColumns() + "em");\r
+            selections.setWidth(getColumns() + "em");\r
+            optionsContainer.setWidth((getColumns() * 2 + 3) + "em");\r
+        } else if (!widthSet) {\r
+            options.setWidth(DEFAULT_COLUMN_COUNT + "em");\r
+            selections.setWidth(DEFAULT_COLUMN_COUNT + "em");\r
+            optionsContainer.setWidth((DEFAULT_COLUMN_COUNT * 2 + 2) + "em");\r
+        }\r
+        if (getRows() > 0) {\r
+            options.setVisibleItemCount(getRows());\r
+            selections.setVisibleItemCount(getRows());\r
+        }\r
+    }\r
+\r
+    protected Object[] getSelectedItems() {\r
+        final Vector selectedItemKeys = new Vector();\r
+        for (int i = 0; i < selections.getItemCount(); i++) {\r
+            selectedItemKeys.add(selections.getValue(i));\r
+        }\r
+        return selectedItemKeys.toArray();\r
+    }\r
+\r
+    private boolean[] getItemsToAdd() {\r
+        final boolean[] selectedIndexes = new boolean[options.getItemCount()];\r
+        for (int i = 0; i < options.getItemCount(); i++) {\r
+            if (options.isItemSelected(i)) {\r
+                selectedIndexes[i] = true;\r
+            } else {\r
+                selectedIndexes[i] = false;\r
+            }\r
+        }\r
+        return selectedIndexes;\r
+    }\r
+\r
+    private boolean[] getItemsToRemove() {\r
+        final boolean[] selectedIndexes = new boolean[selections.getItemCount()];\r
+        for (int i = 0; i < selections.getItemCount(); i++) {\r
+            if (selections.isItemSelected(i)) {\r
+                selectedIndexes[i] = true;\r
+            } else {\r
+                selectedIndexes[i] = false;\r
+            }\r
+        }\r
+        return selectedIndexes;\r
+    }\r
+\r
+    public void onClick(Widget sender) {\r
+        super.onClick(sender);\r
+        if (sender == add) {\r
+            final boolean[] sel = getItemsToAdd();\r
+            for (int i = 0; i < sel.length; i++) {\r
+                if (sel[i]) {\r
+                    final int optionIndex = i\r
+                            - (sel.length - options.getItemCount());\r
+                    selectedKeys.add(options.getValue(optionIndex));\r
+\r
+                    // Move selection to another column\r
+                    final String text = options.getItemText(optionIndex);\r
+                    final String value = options.getValue(optionIndex);\r
+                    selections.addItem(text, value);\r
+                    selections.setItemSelected(selections.getItemCount() - 1,\r
+                            true);\r
+                    options.removeItem(optionIndex);\r
+                }\r
+            }\r
+            client.updateVariable(id, "selected", selectedKeys.toArray(),\r
+                    isImmediate());\r
+\r
+        } else if (sender == remove) {\r
+            final boolean[] sel = getItemsToRemove();\r
+            for (int i = 0; i < sel.length; i++) {\r
+                if (sel[i]) {\r
+                    final int selectionIndex = i\r
+                            - (sel.length - selections.getItemCount());\r
+                    selectedKeys.remove(selections.getValue(selectionIndex));\r
+\r
+                    // Move selection to another column\r
+                    final String text = selections.getItemText(selectionIndex);\r
+                    final String value = selections.getValue(selectionIndex);\r
+                    options.addItem(text, value);\r
+                    options.setItemSelected(options.getItemCount() - 1, true);\r
+                    selections.removeItem(selectionIndex);\r
+                }\r
+            }\r
+            client.updateVariable(id, "selected", selectedKeys.toArray(),\r
+                    isImmediate());\r
+        } else if (sender == options) {\r
+            // unselect all in other list, to avoid mistakes (i.e wrong button)\r
+            final int c = selections.getItemCount();\r
+            for (int i = 0; i < c; i++) {\r
+                selections.setItemSelected(i, false);\r
+            }\r
+        } else if (sender == selections) {\r
+            // unselect all in other list, to avoid mistakes (i.e wrong button)\r
+            final int c = options.getItemCount();\r
+            for (int i = 0; i < c; i++) {\r
+                options.setItemSelected(i, false);\r
+            }\r
+        }\r
+    }\r
+\r
+    public void setHeight(String height) {\r
+        super.setHeight(height);\r
+        if ("".equals(height)) {\r
+            options.setHeight("");\r
+            selections.setHeight("");\r
+        } else {\r
+            setFullHeightInternals();\r
+        }\r
+    }\r
+\r
+    private void setFullHeightInternals() {\r
+        options.setHeight("100%");\r
+        selections.setHeight("100%");\r
+    }\r
+\r
+    public void setWidth(String width) {\r
+        super.setWidth(width);\r
+        if (!"".equals(width) && width != null) {\r
+            setRelativeInternalWidths();\r
+        }\r
+    }\r
+\r
+    private void setRelativeInternalWidths() {\r
+        DOM.setStyleAttribute(getElement(), "position", "relative");\r
+        buttons.setWidth("16%");\r
+        options.setWidth("42%");\r
+        selections.setWidth("42%");\r
+        widthSet = true;\r
+    }\r
+}\r
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/IUnknownComponent.java b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/IUnknownComponent.java
new file mode 100644 (file)
index 0000000..e90de5c
--- /dev/null
@@ -0,0 +1,40 @@
+/* 
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.itmill.toolkit.terminal.gwt.client.ui;
+
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.Tree;
+import com.google.gwt.user.client.ui.VerticalPanel;
+import com.itmill.toolkit.terminal.gwt.client.ApplicationConnection;
+import com.itmill.toolkit.terminal.gwt.client.Paintable;
+import com.itmill.toolkit.terminal.gwt.client.UIDL;
+
+public class IUnknownComponent extends Composite implements Paintable {
+
+    com.google.gwt.user.client.ui.Label caption = new com.google.gwt.user.client.ui.Label();;
+    Tree uidlTree = new Tree();
+
+    public IUnknownComponent() {
+        final VerticalPanel panel = new VerticalPanel();
+        panel.add(caption);
+        panel.add(uidlTree);
+        initWidget(panel);
+        setStyleName("itmill-unknown");
+        caption.setStyleName("itmill-unknown-caption");
+    }
+
+    public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+        if (client.updateComponent(this, uidl, false)) {
+            return;
+        }
+        setCaption("Client faced an unknown component type. Unrendered UIDL:");
+        uidlTree.clear();
+        uidlTree.addItem(uidl.dir());
+    }
+
+    public void setCaption(String c) {
+        caption.setText(c);
+    }
+}
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/IUpload.java b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/IUpload.java
new file mode 100644 (file)
index 0000000..84707de
--- /dev/null
@@ -0,0 +1,151 @@
+/* 
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.itmill.toolkit.terminal.gwt.client.ui;
+
+import com.google.gwt.user.client.Timer;
+import com.google.gwt.user.client.ui.Button;
+import com.google.gwt.user.client.ui.ClickListener;
+import com.google.gwt.user.client.ui.FileUpload;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.FormHandler;
+import com.google.gwt.user.client.ui.FormPanel;
+import com.google.gwt.user.client.ui.FormSubmitCompleteEvent;
+import com.google.gwt.user.client.ui.FormSubmitEvent;
+import com.google.gwt.user.client.ui.Panel;
+import com.google.gwt.user.client.ui.Widget;
+import com.itmill.toolkit.terminal.gwt.client.ApplicationConnection;
+import com.itmill.toolkit.terminal.gwt.client.Paintable;
+import com.itmill.toolkit.terminal.gwt.client.UIDL;
+
+public class IUpload extends FormPanel implements Paintable, ClickListener,
+        FormHandler {
+
+    public static final String CLASSNAME = "i-upload";
+
+    /**
+     * FileUpload component that opens native OS dialog to select file.
+     */
+    FileUpload fu = new FileUpload();
+
+    Panel panel = new FlowPanel();
+
+    ApplicationConnection client;
+
+    private String paintableId;
+
+    /**
+     * Button that initiates uploading
+     */
+    private final Button submitButton;
+
+    /**
+     * When expecting big files, programmer may initiate some UI changes when
+     * uploading the file starts. Bit after submitting file we'll visit the
+     * server to check possible changes.
+     */
+    private Timer t;
+
+    /**
+     * some browsers tries to send form twice if submit is called in button
+     * click handler, some don't submit at all without it, so we need to track
+     * if form is already being submitted
+     */
+    private boolean submitted = false;
+
+    private boolean enabled = true;
+
+    public IUpload() {
+        super();
+        setEncoding(FormPanel.ENCODING_MULTIPART);
+        setMethod(FormPanel.METHOD_POST);
+
+        setWidget(panel);
+        panel.add(fu);
+        submitButton = new Button();
+        submitButton.addClickListener(this);
+        panel.add(submitButton);
+
+        addFormHandler(this);
+
+        setStyleName(CLASSNAME);
+    }
+
+    public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+        if (client.updateComponent(this, uidl, true)) {
+            return;
+        }
+        this.client = client;
+        paintableId = uidl.getId();
+        setAction(client.getAppUri());
+        submitButton.setText(uidl.getStringAttribute("buttoncaption"));
+        fu.setName(paintableId + "_file");
+
+        if (uidl.hasAttribute("disabled") || uidl.hasAttribute("readonly")) {
+            disableUpload();
+        } else if (uidl.getBooleanAttribute("state")) {
+            enableUploaod();
+        }
+
+    }
+
+    public void onClick(Widget sender) {
+        submit();
+    }
+
+    public void onSubmit(FormSubmitEvent event) {
+        if (fu.getFilename().length() == 0 || submitted || !enabled) {
+            event.setCancelled(true);
+            ApplicationConnection
+                    .getConsole()
+                    .log(
+                            "Submit cancelled (disabled, no file or already submitted)");
+            return;
+        }
+        // flush possibly pending variable changes, so they will be handled
+        // before upload
+        client.sendPendingVariableChanges();
+
+        submitted = true;
+        ApplicationConnection.getConsole().log("Submitted form");
+
+        disableUpload();
+
+        /*
+         * Visit server a moment after upload has started to see possible
+         * changes from UploadStarted event. Will be cleared on complete.
+         */
+        t = new Timer() {
+            public void run() {
+                client.sendPendingVariableChanges();
+            }
+        };
+        t.schedule(800);
+    }
+
+    protected void disableUpload() {
+        submitButton.setEnabled(false);
+        fu.setVisible(false);
+        enabled = false;
+    }
+
+    protected void enableUploaod() {
+        submitButton.setEnabled(true);
+        fu.setVisible(true);
+        enabled = true;
+    }
+
+    public void onSubmitComplete(FormSubmitCompleteEvent event) {
+        if (client != null) {
+            if (t != null) {
+                t.cancel();
+            }
+            ApplicationConnection.getConsole().log("Submit complete");
+            client.sendPendingVariableChanges();
+        }
+        submitted = false;
+        enableUploaod();
+    }
+
+}
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/IView.java b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/IView.java
new file mode 100644 (file)
index 0000000..da0c66e
--- /dev/null
@@ -0,0 +1,281 @@
+/* 
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.itmill.toolkit.terminal.gwt.client.ui;
+
+import java.util.HashSet;
+import java.util.Iterator;
+
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.Element;
+import com.google.gwt.user.client.Event;
+import com.google.gwt.user.client.Timer;
+import com.google.gwt.user.client.Window;
+import com.google.gwt.user.client.WindowResizeListener;
+import com.google.gwt.user.client.ui.RootPanel;
+import com.google.gwt.user.client.ui.SimplePanel;
+import com.google.gwt.user.client.ui.Widget;
+import com.itmill.toolkit.terminal.gwt.client.ApplicationConnection;
+import com.itmill.toolkit.terminal.gwt.client.BrowserInfo;
+import com.itmill.toolkit.terminal.gwt.client.Paintable;
+import com.itmill.toolkit.terminal.gwt.client.UIDL;
+import com.itmill.toolkit.terminal.gwt.client.Util;
+
+/**
+ * 
+ */
+public class IView extends SimplePanel implements Paintable,
+        WindowResizeListener {
+
+    private static final String CLASSNAME = "i-view";
+
+    private String theme;
+
+    private Paintable layout;
+
+    private final HashSet subWindows = new HashSet();
+
+    private String id;
+
+    private ShortcutActionHandler actionHandler;
+
+    /** stored width for IE resize optiomization */
+    private int width;
+
+    /** stored height for IE resize optiomization */
+    private int height;
+
+    /**
+     * We are postponing resize process with IE. IE bugs with scrollbars in some
+     * situations, that causes false onWindowResized calls. With Timer we will
+     * give IE some time to decide if it really wants to keep current size
+     * (scrollbars).
+     */
+    private Timer resizeTimer;
+
+    public IView(String elementId) {
+        super();
+        setStyleName(CLASSNAME);
+
+        DOM.sinkEvents(getElement(), Event.ONKEYDOWN);
+
+        DOM.setElementProperty(getElement(), "tabIndex", "0");
+
+        RootPanel.get(elementId).add(this);
+
+        Window.addWindowResizeListener(this);
+
+        // set focus to iview element by default to listen possible keyboard
+        // shortcuts
+        if (BrowserInfo.get().isOpera() || BrowserInfo.get().isSafari()
+                && BrowserInfo.get().getWebkitVersion() < 526) {
+            // old webkits don't support focusing div elements
+            Element fElem = DOM.createInputCheck();
+            DOM.setStyleAttribute(fElem, "margin", "0");
+            DOM.setStyleAttribute(fElem, "padding", "0");
+            DOM.setStyleAttribute(fElem, "border", "0");
+            DOM.setStyleAttribute(fElem, "outline", "0");
+            DOM.setStyleAttribute(fElem, "width", "1px");
+            DOM.setStyleAttribute(fElem, "height", "1px");
+            DOM.setStyleAttribute(fElem, "position", "absolute");
+            DOM.setStyleAttribute(fElem, "opacity", "0.1");
+            DOM.appendChild(getElement(), fElem);
+            focus(fElem);
+        } else {
+            focus(getElement());
+        }
+
+    }
+
+    private static native void focus(Element el)
+    /*-{
+        try {
+            el.focus();
+        } catch (e) {
+        
+        }
+    }-*/;
+
+    public String getTheme() {
+        return theme;
+    }
+
+    public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+
+        id = uidl.getId();
+
+        // Some attributes to note
+        theme = uidl.getStringAttribute("theme");
+        if (uidl.hasAttribute("style")) {
+            addStyleName(uidl.getStringAttribute("style"));
+        }
+
+        com.google.gwt.user.client.Window.setTitle(uidl
+                .getStringAttribute("caption"));
+
+        // Process children
+        int childIndex = 0;
+
+        // Open URL:s
+        while (childIndex < uidl.getChildCount()
+                && "open".equals(uidl.getChildUIDL(childIndex).getTag())) {
+            final UIDL open = uidl.getChildUIDL(childIndex);
+            final String url = open.getStringAttribute("src");
+            final String target = open.getStringAttribute("name");
+            if (target == null) {
+                goTo(url);
+            } else {
+                // TODO width & height
+                Window.open(url, target != null ? target : null, "");
+            }
+            childIndex++;
+        }
+
+        // Draw this application level window
+        UIDL childUidl = uidl.getChildUIDL(childIndex);
+        final Paintable lo = client.getPaintable(childUidl);
+
+        if (layout != null) {
+            if (layout != lo) {
+                // remove old
+                client.unregisterPaintable(layout);
+                // add new
+                setWidget((Widget) lo);
+                layout = lo;
+            }
+        } else {
+            setWidget((Widget) lo);
+            layout = lo;
+        }
+        layout.updateFromUIDL(childUidl, client);
+
+        // Update subwindows
+        final HashSet removedSubWindows = new HashSet(subWindows);
+
+        // Open new windows
+        while ((childUidl = uidl.getChildUIDL(childIndex++)) != null) {
+            if ("window".equals(childUidl.getTag())) {
+                final Paintable w = client.getPaintable(childUidl);
+                if (subWindows.contains(w)) {
+                    removedSubWindows.remove(w);
+                } else {
+                    subWindows.add(w);
+                }
+                w.updateFromUIDL(childUidl, client);
+            } else if ("actions".equals(childUidl.getTag())) {
+                if (actionHandler == null) {
+                    actionHandler = new ShortcutActionHandler(id, client);
+                }
+                actionHandler.updateActionMap(childUidl);
+            } else if (childUidl.getTag().equals("notifications")) {
+                for (final Iterator it = childUidl.getChildIterator(); it
+                        .hasNext();) {
+                    final UIDL notification = (UIDL) it.next();
+                    String html = "";
+                    if (notification.hasAttribute("icon")) {
+                        final String parsedUri = client
+                                .translateToolkitUri(notification
+                                        .getStringAttribute("icon"));
+                        html += "<IMG src=\"" + parsedUri + "\" />";
+                    }
+                    if (notification.hasAttribute("caption")) {
+                        html += "<H1>"
+                                + notification.getStringAttribute("caption")
+                                + "</H1>";
+                    }
+                    if (notification.hasAttribute("message")) {
+                        html += "<p>"
+                                + notification.getStringAttribute("message")
+                                + "</p>";
+                    }
+
+                    final String style = notification.hasAttribute("style") ? notification
+                            .getStringAttribute("style")
+                            : null;
+                    final int position = notification
+                            .getIntAttribute("position");
+                    final int delay = notification.getIntAttribute("delay");
+                    new Notification(delay).show(html, position, style);
+                }
+            }
+        }
+
+        // Close old windows
+        for (final Iterator rem = removedSubWindows.iterator(); rem.hasNext();) {
+            final IWindow w = (IWindow) rem.next();
+            client.unregisterPaintable(w);
+            subWindows.remove(w);
+            w.hide();
+        }
+
+        onWindowResized(Window.getClientWidth(), Window.getClientHeight());
+        // IE somehow fails some layout on first run, force layout
+        // functions
+        // Util.runDescendentsLayout(this);
+
+    }
+
+    public void onBrowserEvent(Event event) {
+        super.onBrowserEvent(event);
+        if (DOM.eventGetType(event) == Event.ONKEYDOWN && actionHandler != null) {
+            actionHandler.handleKeyboardEvent(event);
+            return;
+        }
+    }
+
+    public void onWindowResized(int width, int height) {
+        if (Util.isIE()) {
+            /*
+             * IE will give us some false resized events due bugs with
+             * scrollbars. Postponing layout phase to see if size was really
+             * changed.
+             */
+            if (resizeTimer == null) {
+                resizeTimer = new Timer() {
+                    public void run() {
+                        boolean changed = false;
+                        if (IView.this.width != getOffsetWidth()) {
+                            IView.this.width = getOffsetWidth();
+                            changed = true;
+                            ApplicationConnection.getConsole().log(
+                                    "window w" + IView.this.width);
+                        }
+                        if (IView.this.height != getOffsetHeight()) {
+                            IView.this.height = getOffsetHeight();
+                            changed = true;
+                            ApplicationConnection.getConsole().log(
+                                    "window h" + IView.this.height);
+                        }
+                        if (changed) {
+                            ApplicationConnection
+                                    .getConsole()
+                                    .log(
+                                            "Running layout functions due window resize");
+                            Util.runDescendentsLayout(IView.this);
+                        }
+                    }
+                };
+            } else {
+                resizeTimer.cancel();
+            }
+            resizeTimer.schedule(200);
+        } else {
+            // temporary set overflow hidden, not to let scrollbars disturb
+            // layout functions
+            final String overflow = DOM.getStyleAttribute(getElement(),
+                    "overflow");
+            DOM.setStyleAttribute(getElement(), "overflow", "hidden");
+            ApplicationConnection.getConsole().log(
+                    "Running layout functions due window resize");
+            Util.runDescendentsLayout(this);
+            DOM.setStyleAttribute(getElement(), "overflow", overflow);
+        }
+    }
+
+    public native static void goTo(String url)
+    /*-{
+       $wnd.location = url;
+     }-*/;
+
+}
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/IWindow.java b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/IWindow.java
new file mode 100644 (file)
index 0000000..e34a8ef
--- /dev/null
@@ -0,0 +1,677 @@
+/* 
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.itmill.toolkit.terminal.gwt.client.ui;
+
+import java.util.Iterator;
+import java.util.Vector;
+
+import com.google.gwt.user.client.Command;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.DeferredCommand;
+import com.google.gwt.user.client.Element;
+import com.google.gwt.user.client.Event;
+import com.google.gwt.user.client.Window;
+import com.google.gwt.user.client.ui.Frame;
+import com.google.gwt.user.client.ui.PopupPanel;
+import com.google.gwt.user.client.ui.RootPanel;
+import com.google.gwt.user.client.ui.ScrollListener;
+import com.google.gwt.user.client.ui.ScrollPanel;
+import com.google.gwt.user.client.ui.Widget;
+import com.itmill.toolkit.terminal.gwt.client.ApplicationConnection;
+import com.itmill.toolkit.terminal.gwt.client.BrowserInfo;
+import com.itmill.toolkit.terminal.gwt.client.Paintable;
+import com.itmill.toolkit.terminal.gwt.client.UIDL;
+import com.itmill.toolkit.terminal.gwt.client.Util;
+
+/**
+ * "Sub window" component.
+ * 
+ * TODO update position / scrollposition / size to client
+ * 
+ * @author IT Mill Ltd
+ */
+public class IWindow extends PopupPanel implements Paintable, ScrollListener {
+
+    private static final int MIN_HEIGHT = 60;
+
+    private static final int MIN_WIDTH = 80;
+
+    private static Vector windowOrder = new Vector();
+
+    public static final String CLASSNAME = "i-window";
+
+    /** pixels used by inner borders and paddings horizontally */
+    protected static final int BORDER_WIDTH_HORIZONTAL = 41;
+
+    /** pixels used by headers, footers, inner borders and paddings vertically */
+    protected static final int BORDER_WIDTH_VERTICAL = 58;
+
+    private static final int STACKING_OFFSET_PIXELS = 15;
+
+    private static final int Z_INDEX_BASE = 10000;
+
+    private Paintable layout;
+
+    private Element contents;
+
+    private Element header;
+
+    private Element footer;
+
+    private Element resizeBox;
+
+    private final ScrollPanel contentPanel = new ScrollPanel();
+
+    private boolean dragging;
+
+    private int startX;
+
+    private int startY;
+
+    private int origX;
+
+    private int origY;
+
+    private boolean resizing;
+
+    private int origW;
+
+    private int origH;
+
+    private Element closeBox;
+
+    protected ApplicationConnection client;
+
+    private String id;
+
+    ShortcutActionHandler shortcutHandler;
+
+    /** Last known positionx read from UIDL or updated to application connection */
+    private int uidlPositionX = -1;
+
+    /** Last known positiony read from UIDL or updated to application connection */
+    private int uidlPositionY = -1;
+
+    private boolean modal = false;
+
+    private Element modalityCurtain;
+    private Element draggingCurtain;
+
+    private Element headerText;
+
+    public IWindow() {
+        super();
+        final int order = windowOrder.size();
+        setWindowOrder(order);
+        windowOrder.add(this);
+        constructDOM();
+        setPopupPosition(order * STACKING_OFFSET_PIXELS, order
+                * STACKING_OFFSET_PIXELS);
+        contentPanel.addScrollListener(this);
+    }
+
+    private void bringToFront() {
+        int curIndex = windowOrder.indexOf(this);
+        if (curIndex + 1 < windowOrder.size()) {
+            windowOrder.remove(this);
+            windowOrder.add(this);
+            for (; curIndex < windowOrder.size(); curIndex++) {
+                ((IWindow) windowOrder.get(curIndex)).setWindowOrder(curIndex);
+            }
+        }
+    }
+
+    /**
+     * Returns true if window is the topmost window
+     * 
+     * @return
+     */
+    private boolean isActive() {
+        return windowOrder.lastElement().equals(this);
+    }
+
+    public void setWindowOrder(int order) {
+        int zIndex = (order + Z_INDEX_BASE);
+        if (modal) {
+            zIndex += 1000;
+            DOM.setStyleAttribute(modalityCurtain, "zIndex", "" + zIndex);
+        }
+        DOM.setStyleAttribute(getElement(), "zIndex", "" + zIndex);
+    }
+
+    protected void constructDOM() {
+        header = DOM.createDiv();
+        DOM.setElementProperty(header, "className", CLASSNAME + "-outerheader");
+        headerText = DOM.createDiv();
+        DOM.setElementProperty(headerText, "className", CLASSNAME + "-header");
+        contents = DOM.createDiv();
+        DOM.setElementProperty(contents, "className", CLASSNAME + "-contents");
+        footer = DOM.createDiv();
+        DOM.setElementProperty(footer, "className", CLASSNAME + "-footer");
+        resizeBox = DOM.createDiv();
+        DOM
+                .setElementProperty(resizeBox, "className", CLASSNAME
+                        + "-resizebox");
+        closeBox = DOM.createDiv();
+        DOM.setElementProperty(closeBox, "className", CLASSNAME + "-closebox");
+        DOM.appendChild(footer, resizeBox);
+
+        DOM.sinkEvents(getElement(), Event.ONLOSECAPTURE);
+        DOM.sinkEvents(closeBox, Event.ONCLICK);
+        DOM.sinkEvents(contents, Event.ONCLICK);
+
+        final Element wrapper = DOM.createDiv();
+        DOM.setElementProperty(wrapper, "className", CLASSNAME + "-wrap");
+
+        final Element wrapper2 = DOM.createDiv();
+        DOM.setElementProperty(wrapper2, "className", CLASSNAME + "-wrap2");
+
+        DOM.sinkEvents(wrapper, Event.ONKEYDOWN);
+
+        DOM.appendChild(wrapper2, closeBox);
+        DOM.appendChild(wrapper2, header);
+        DOM.appendChild(header, headerText);
+        DOM.appendChild(wrapper2, contents);
+        DOM.appendChild(wrapper2, footer);
+        DOM.appendChild(wrapper, wrapper2);
+        DOM.appendChild(super.getContainerElement(), wrapper);
+        DOM.setElementProperty(getElement(), "className", CLASSNAME);
+
+        sinkEvents(Event.MOUSEEVENTS);
+
+        setWidget(contentPanel);
+    }
+
+    public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+        id = uidl.getId();
+        this.client = client;
+
+        // Workaround needed for Testing Tools (GWT generates window DOM
+        // slightly different in different browsers).
+        DOM.setElementProperty(closeBox, "id", id + "_window_close");
+
+        if (uidl.hasAttribute("invisible")) {
+            this.hide();
+            return;
+        }
+
+        if (client.updateComponent(this, uidl, false)) {
+            return;
+        }
+
+        if (uidl.getBooleanAttribute("modal") != modal) {
+            setModal(!modal);
+        }
+
+        // Initialize the size from UIDL
+        // FIXME relational size is for outer size, others are applied for
+        // content
+        if (uidl.hasVariable("width")) {
+            final String width = uidl.getStringVariable("width");
+            if (width.indexOf("px") < 0) {
+                DOM.setStyleAttribute(getElement(), "width", width);
+            } else {
+                setWidth(width);
+            }
+        }
+
+        // Initialize the position form UIDL
+        try {
+            final int positionx = uidl.getIntVariable("positionx");
+            final int positiony = uidl.getIntVariable("positiony");
+            if (positionx >= 0 && positiony >= 0) {
+                setPopupPosition(positionx, positiony);
+            }
+        } catch (final IllegalArgumentException e) {
+            // Silently ignored as positionx and positiony are not required
+            // parameters
+        }
+
+        if (!isAttached()) {
+            show();
+        }
+
+        // Height set after show so we can detect space used by decorations
+        if (uidl.hasVariable("height")) {
+            final String height = uidl.getStringVariable("height");
+            if (height.indexOf("%") > 0) {
+                int winHeight = Window.getClientHeight();
+                float percent = Float.parseFloat(height.substring(0, height
+                        .indexOf("%"))) / 100.0f;
+                int contentPixels = (int) (winHeight * percent);
+                contentPixels -= (DOM.getElementPropertyInt(getElement(),
+                        "offsetHeight") - DOM.getElementPropertyInt(contents,
+                        "offsetHeight"));
+                // FIXME hardcoded contents elements border size
+                contentPixels -= 2;
+
+                setHeight(contentPixels + "px");
+            } else {
+                setHeight(height);
+            }
+        }
+
+        if (uidl.hasAttribute("caption")) {
+            setCaption(uidl.getStringAttribute("caption"), uidl
+                    .getStringAttribute("icon"));
+        }
+
+        boolean showingUrl = false;
+        int childIndex = 0;
+        UIDL childUidl = uidl.getChildUIDL(childIndex++);
+        while ("open".equals(childUidl.getTag())) {
+            // TODO multiple opens with the same target will in practice just
+            // open the last one - should we fix that somehow?
+            final String parsedUri = client.translateToolkitUri(childUidl
+                    .getStringAttribute("src"));
+            if (!childUidl.hasAttribute("name")) {
+                final Frame frame = new Frame();
+                DOM.setStyleAttribute(frame.getElement(), "width", "100%");
+                DOM.setStyleAttribute(frame.getElement(), "height", "100%");
+                DOM.setStyleAttribute(frame.getElement(), "border", "0px");
+                frame.setUrl(parsedUri);
+                contentPanel.setWidget(frame);
+                showingUrl = true;
+            } else {
+                final String target = childUidl.getStringAttribute("name");
+                Window.open(parsedUri, target, "");
+            }
+            childUidl = uidl.getChildUIDL(childIndex++);
+        }
+
+        final Paintable lo = client.getPaintable(childUidl);
+        if (layout != null) {
+            if (layout != lo) {
+                // remove old
+                client.unregisterPaintable(layout);
+                contentPanel.remove((Widget) layout);
+                // add new
+                if (!showingUrl) {
+                    contentPanel.setWidget((Widget) lo);
+                }
+                layout = lo;
+            }
+        } else if (!showingUrl) {
+            contentPanel.setWidget((Widget) lo);
+        }
+        lo.updateFromUIDL(childUidl, client);
+
+        // we may have actions and notifications
+        if (uidl.getChildCount() > 1) {
+            final int cnt = uidl.getChildCount();
+            for (int i = 1; i < cnt; i++) {
+                childUidl = uidl.getChildUIDL(i);
+                if (childUidl.getTag().equals("actions")) {
+                    if (shortcutHandler == null) {
+                        shortcutHandler = new ShortcutActionHandler(id, client);
+                    }
+                    shortcutHandler.updateActionMap(childUidl);
+                } else if (childUidl.getTag().equals("notifications")) {
+                    // TODO needed? move ->
+                    for (final Iterator it = childUidl.getChildIterator(); it
+                            .hasNext();) {
+                        final UIDL notification = (UIDL) it.next();
+                        String html = "";
+                        if (notification.hasAttribute("icon")) {
+                            final String parsedUri = client
+                                    .translateToolkitUri(notification
+                                            .getStringAttribute("icon"));
+                            html += "<img src=\"" + parsedUri + "\" />";
+                        }
+                        if (notification.hasAttribute("caption")) {
+                            html += "<h1>"
+                                    + notification
+                                            .getStringAttribute("caption")
+                                    + "</h1>";
+                        }
+                        if (notification.hasAttribute("message")) {
+                            html += "<p>"
+                                    + notification
+                                            .getStringAttribute("message")
+                                    + "</p>";
+                        }
+
+                        final String style = notification.hasAttribute("style") ? notification
+                                .getStringAttribute("style")
+                                : null;
+                        final int position = notification
+                                .getIntAttribute("position");
+                        final int delay = notification.getIntAttribute("delay");
+                        new Notification(delay).show(html, position, style);
+                    }
+                }
+            }
+
+        }
+
+        // setting scrollposition must happen after children is rendered
+        contentPanel.setScrollPosition(uidl.getIntVariable("scrolltop"));
+        contentPanel.setHorizontalScrollPosition(uidl
+                .getIntVariable("scrollleft"));
+
+    }
+
+    public void show() {
+        if (modal) {
+            showModalityCurtain();
+        }
+        super.show();
+
+        setFF2CaretFixEnabled(true);
+        fixFF3OverflowBug();
+    }
+
+    /** Disable overflow auto with FF3 to fix #1837. */
+    private void fixFF3OverflowBug() {
+        if (BrowserInfo.get().isFF3()) {
+            DeferredCommand.addCommand(new Command() {
+                public void execute() {
+                    DOM.setStyleAttribute(getElement(), "overflow", "");
+                }
+            });
+        }
+    }
+
+    /**
+     * Fix "missing cursor" browser bug workaround for FF2 in Windows and Linux.
+     * 
+     * Calling this method has no effect on other browsers than the ones based
+     * on Gecko 1.8
+     * 
+     * @param enable
+     */
+    private void setFF2CaretFixEnabled(boolean enable) {
+        if (BrowserInfo.get().isFF2()) {
+            if (enable) {
+                DeferredCommand.addCommand(new Command() {
+                    public void execute() {
+                        DOM.setStyleAttribute(getElement(), "overflow", "auto");
+                    }
+                });
+            } else {
+                DOM.setStyleAttribute(getElement(), "overflow", "");
+            }
+        }
+    }
+
+    public void hide() {
+        if (modal) {
+            hideModalityCurtain();
+        }
+        super.hide();
+    }
+
+    private void setModal(boolean modality) {
+        modal = modality;
+        if (modal) {
+            modalityCurtain = DOM.createDiv();
+            DOM.setElementProperty(modalityCurtain, "className", CLASSNAME
+                    + "-modalitycurtain");
+            if (isAttached()) {
+                showModalityCurtain();
+                bringToFront();
+            } else {
+                DeferredCommand.addCommand(new Command() {
+                    public void execute() {
+                        // modal window must on top of others
+                        bringToFront();
+                    }
+                });
+            }
+        } else {
+            if (modalityCurtain != null) {
+                if (isAttached()) {
+                    hideModalityCurtain();
+                }
+                modalityCurtain = null;
+            }
+        }
+    }
+
+    private void showModalityCurtain() {
+        if (Util.isFF2()) {
+            DOM.setStyleAttribute(modalityCurtain, "height", DOM
+                    .getElementPropertyInt(RootPanel.getBodyElement(),
+                            "offsetHeight")
+                    + "px");
+            DOM.setStyleAttribute(modalityCurtain, "position", "absolute");
+        }
+        DOM.appendChild(RootPanel.getBodyElement(), modalityCurtain);
+    }
+
+    private void hideModalityCurtain() {
+        DOM.removeChild(RootPanel.getBodyElement(), modalityCurtain);
+    }
+
+    /*
+     * Shows (or hides) an empty div on top of all other content; used when
+     * resizing or moving, so that iframes (etc) do not steal event.
+     */
+    private void showDraggingCurtain(boolean show) {
+        if (show && draggingCurtain == null) {
+
+            setFF2CaretFixEnabled(false); // makes FF2 slow
+
+            draggingCurtain = DOM.createDiv();
+            DOM.setStyleAttribute(draggingCurtain, "position", "absolute");
+            DOM.setStyleAttribute(draggingCurtain, "top", "0px");
+            DOM.setStyleAttribute(draggingCurtain, "left", "0px");
+            DOM.setStyleAttribute(draggingCurtain, "width", "100%");
+            DOM.setStyleAttribute(draggingCurtain, "height", "100%");
+            DOM.setStyleAttribute(draggingCurtain, "zIndex", ""
+                    + ToolkitOverlay.Z_INDEX);
+
+            DOM.appendChild(RootPanel.getBodyElement(), draggingCurtain);
+        } else if (!show && draggingCurtain != null) {
+
+            setFF2CaretFixEnabled(true); // makes FF2 slow
+
+            DOM.removeChild(RootPanel.getBodyElement(), draggingCurtain);
+            draggingCurtain = null;
+        }
+
+    }
+
+    public void setPopupPosition(int left, int top) {
+        super.setPopupPosition(left, top);
+        if (left != uidlPositionX && client != null) {
+            client.updateVariable(id, "positionx", left, false);
+            uidlPositionX = left;
+        }
+        if (top != uidlPositionY && client != null) {
+            client.updateVariable(id, "positiony", top, false);
+            uidlPositionY = top;
+        }
+    }
+
+    public void setCaption(String c) {
+        setCaption(c, null);
+    }
+
+    public void setCaption(String c, String icon) {
+        String html = c;
+        if (icon != null) {
+            icon = client.translateToolkitUri(icon);
+            html = "<img src=\"" + icon + "\" class=\"i-icon\" />" + html;
+        }
+        DOM.setInnerHTML(headerText, html);
+    }
+
+    protected Element getContainerElement() {
+        return contents;
+    }
+
+    public void onBrowserEvent(final Event event) {
+        final int type = DOM.eventGetType(event);
+
+        if (type == Event.ONKEYDOWN && shortcutHandler != null) {
+            shortcutHandler.handleKeyboardEvent(event);
+            return;
+        }
+
+        final Element target = DOM.eventGetTarget(event);
+
+        if (client != null && !DOM.compare(target, header)) {
+            client.handleTooltipEvent(event, this);
+        }
+
+        if (resizing || DOM.compare(resizeBox, target)) {
+            onResizeEvent(event);
+            DOM.eventCancelBubble(event, true);
+        } else if (DOM.compare(target, closeBox)) {
+            if (type == Event.ONCLICK) {
+                onCloseClick();
+                DOM.eventCancelBubble(event, true);
+            }
+        } else if (dragging || !DOM.isOrHasChild(contents, target)) {
+            onDragEvent(event);
+            DOM.eventCancelBubble(event, true);
+        } else if (type == Event.ONCLICK) {
+            // clicked inside window, ensure to be on top
+            if (!isActive()) {
+                bringToFront();
+            }
+        }
+    }
+
+    private void onCloseClick() {
+        client.updateVariable(id, "close", true, true);
+    }
+
+    private void onResizeEvent(Event event) {
+        switch (DOM.eventGetType(event)) {
+        case Event.ONMOUSEDOWN:
+            if (!isActive()) {
+                bringToFront();
+            }
+            showDraggingCurtain(true);
+            resizing = true;
+            startX = DOM.eventGetScreenX(event);
+            startY = DOM.eventGetScreenY(event);
+            origW = getWidget().getOffsetWidth();
+            origH = getWidget().getOffsetHeight();
+            DOM.setCapture(getElement());
+            DOM.eventPreventDefault(event);
+            break;
+        case Event.ONMOUSEUP:
+            showDraggingCurtain(false);
+            resizing = false;
+            DOM.releaseCapture(getElement());
+            setSize(event, true);
+            break;
+        case Event.ONLOSECAPTURE:
+            showDraggingCurtain(false);
+            resizing = false;
+        case Event.ONMOUSEMOVE:
+            if (resizing) {
+                setSize(event, false);
+                DOM.eventPreventDefault(event);
+            }
+            break;
+        default:
+            DOM.eventPreventDefault(event);
+            break;
+        }
+    }
+
+    public void setSize(Event event, boolean updateVariables) {
+        int w = DOM.eventGetScreenX(event) - startX + origW;
+        if (w < MIN_WIDTH) {
+            w = MIN_WIDTH;
+        }
+        int h = DOM.eventGetScreenY(event) - startY + origH;
+        if (h < MIN_HEIGHT) {
+            h = MIN_HEIGHT;
+        }
+        setWidth(w + "px");
+        setHeight(h + "px");
+        if (updateVariables) {
+            // sending width back always as pixels, no need for unit
+            client.updateVariable(id, "width", w, false);
+            client.updateVariable(id, "height", h, false);
+        }
+        // Update child widget dimensions
+        Util.runDescendentsLayout(this);
+    }
+
+    public void setWidth(String width) {
+        if (!"".equals(width)) {
+            DOM
+                    .setStyleAttribute(
+                            getElement(),
+                            "width",
+                            (Integer.parseInt(width.substring(0,
+                                    width.length() - 2)) + BORDER_WIDTH_HORIZONTAL)
+                                    + "px");
+        }
+    }
+
+    private void onDragEvent(Event event) {
+        switch (DOM.eventGetType(event)) {
+        case Event.ONMOUSEDOWN:
+            if (!isActive()) {
+                bringToFront();
+            }
+            showDraggingCurtain(true);
+            dragging = true;
+            startX = DOM.eventGetScreenX(event);
+            startY = DOM.eventGetScreenY(event);
+            origX = DOM.getAbsoluteLeft(getElement());
+            origY = DOM.getAbsoluteTop(getElement());
+            DOM.setCapture(getElement());
+            DOM.eventPreventDefault(event);
+            break;
+        case Event.ONMOUSEUP:
+            dragging = false;
+            showDraggingCurtain(false);
+            DOM.releaseCapture(getElement());
+            break;
+        case Event.ONLOSECAPTURE:
+            showDraggingCurtain(false);
+            dragging = false;
+            break;
+        case Event.ONMOUSEMOVE:
+            if (dragging) {
+                final int x = DOM.eventGetScreenX(event) - startX + origX;
+                final int y = DOM.eventGetScreenY(event) - startY + origY;
+                setPopupPosition(x, y);
+                DOM.eventPreventDefault(event);
+            }
+            break;
+        default:
+            break;
+        }
+    }
+
+    public boolean onEventPreview(Event event) {
+        if (dragging) {
+            onDragEvent(event);
+            return false;
+        } else if (resizing) {
+            onResizeEvent(event);
+            return false;
+        } else if (modal) {
+            // return false when modal and outside window
+            final Element target = DOM.eventGetTarget(event);
+            if (!DOM.isOrHasChild(getElement(), target)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    public void onScroll(Widget widget, int scrollLeft, int scrollTop) {
+        client.updateVariable(id, "scrolltop", scrollTop, false);
+        client.updateVariable(id, "scrollleft", scrollLeft, false);
+    }
+
+    public void addStyleDependentName(String styleSuffix) {
+        // IWindow's getStyleElement() does not return the same element as
+        // getElement(), so we need to override this.
+        setStyleName(getElement(), getStylePrimaryName() + "-" + styleSuffix,
+                true);
+    }
+
+}
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/Icon.java b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/Icon.java
new file mode 100644 (file)
index 0000000..52672d6
--- /dev/null
@@ -0,0 +1,36 @@
+/* 
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.itmill.toolkit.terminal.gwt.client.ui;
+
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.ui.UIObject;
+import com.itmill.toolkit.terminal.gwt.client.ApplicationConnection;
+
+public class Icon extends UIObject {
+    private final ApplicationConnection client;
+    private String myUri;
+
+    public Icon(ApplicationConnection client) {
+        setElement(DOM.createImg());
+        DOM.setElementProperty(getElement(), "alt", "icon");
+        setStyleName("i-icon");
+        this.client = client;
+        client.addPngFix(getElement());
+    }
+
+    public Icon(ApplicationConnection client, String uidlUri) {
+        this(client);
+        setUri(uidlUri);
+    }
+
+    public void setUri(String uidlUri) {
+        if (!uidlUri.equals(myUri)) {
+            String uri = client.translateToolkitUri(uidlUri);
+            DOM.setElementProperty(getElement(), "src", uri);
+            myUri = uidlUri;
+        }
+    }
+
+}
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/MarginInfo.java b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/MarginInfo.java
new file mode 100644 (file)
index 0000000..6058433
--- /dev/null
@@ -0,0 +1,59 @@
+/* 
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.itmill.toolkit.terminal.gwt.client.ui;
+
+public class MarginInfo {
+
+    private static final int TOP = 1;
+    private static final int RIGHT = 2;
+    private static final int BOTTOM = 4;
+    private static final int LEFT = 8;
+
+    private int bitMask;
+
+    public MarginInfo(int bitMask) {
+        this.bitMask = bitMask;
+    }
+
+    public MarginInfo(boolean top, boolean right, boolean bottom, boolean left) {
+        setMargins(top, right, bottom, left);
+    }
+
+    public void setMargins(boolean top, boolean right, boolean bottom,
+            boolean left) {
+        bitMask = top ? TOP : 0;
+        bitMask += right ? RIGHT : 0;
+        bitMask += bottom ? BOTTOM : 0;
+        bitMask += left ? LEFT : 0;
+    }
+
+    public boolean hasLeft() {
+        return (bitMask & LEFT) == LEFT;
+    }
+
+    public boolean hasRight() {
+        return (bitMask & RIGHT) == RIGHT;
+    }
+
+    public boolean hasTop() {
+        return (bitMask & TOP) == TOP;
+    }
+
+    public boolean hasBottom() {
+        return (bitMask & BOTTOM) == BOTTOM;
+    }
+
+    public int getBitMask() {
+        return bitMask;
+    }
+
+    public void setMargins(boolean enabled) {
+        if (enabled) {
+            bitMask = TOP + RIGHT + BOTTOM + LEFT;
+        } else {
+            bitMask = 0;
+        }
+    }
+}
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/MenuBar.java b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/MenuBar.java
new file mode 100644 (file)
index 0000000..251ec8f
--- /dev/null
@@ -0,0 +1,511 @@
+/* 
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.itmill.toolkit.terminal.gwt.client.ui;
+
+/*
+ * Copyright 2007 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+// COPIED HERE DUE package privates in GWT
+import java.util.ArrayList;
+import java.util.List;
+
+import com.google.gwt.user.client.Command;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.DeferredCommand;
+import com.google.gwt.user.client.Element;
+import com.google.gwt.user.client.Event;
+import com.google.gwt.user.client.ui.PopupListener;
+import com.google.gwt.user.client.ui.PopupPanel;
+import com.google.gwt.user.client.ui.Widget;
+
+/**
+ * A standard menu bar widget. A menu bar can contain any number of menu items,
+ * each of which can either fire a {@link com.google.gwt.user.client.Command} or
+ * open a cascaded menu bar.
+ * 
+ * <p>
+ * <img class='gallery' src='MenuBar.png'/>
+ * </p>
+ * 
+ * <h3>CSS Style Rules</h3>
+ * <ul class='css'>
+ * <li>.gwt-MenuBar { the menu bar itself }</li>
+ * <li>.gwt-MenuBar .gwt-MenuItem { menu items }</li>
+ * <li>.gwt-MenuBar .gwt-MenuItem-selected { selected menu items }</li>
+ * </ul>
+ * 
+ * <p>
+ * <h3>Example</h3>
+ * {@example com.google.gwt.examples.MenuBarExample}
+ * </p>
+ */
+public class MenuBar extends Widget implements PopupListener {
+
+    private final Element body;
+    private final ArrayList items = new ArrayList();
+    private MenuBar parentMenu;
+    private PopupPanel popup;
+    private MenuItem selectedItem;
+    private MenuBar shownChildMenu;
+    private final boolean vertical;
+    private boolean autoOpen;
+
+    /**
+     * Creates an empty horizontal menu bar.
+     */
+    public MenuBar() {
+        this(false);
+    }
+
+    /**
+     * Creates an empty menu bar.
+     * 
+     * @param vertical
+     *                <code>true</code> to orient the menu bar vertically
+     */
+    public MenuBar(boolean vertical) {
+        super();
+
+        final Element table = DOM.createTable();
+        body = DOM.createTBody();
+        DOM.appendChild(table, body);
+
+        if (!vertical) {
+            final Element tr = DOM.createTR();
+            DOM.appendChild(body, tr);
+        }
+
+        this.vertical = vertical;
+
+        final Element outer = DOM.createDiv();
+        DOM.appendChild(outer, table);
+        setElement(outer);
+
+        sinkEvents(Event.ONCLICK | Event.ONMOUSEOVER | Event.ONMOUSEOUT);
+        setStyleName("gwt-MenuBar");
+    }
+
+    /**
+     * Adds a menu item to the bar.
+     * 
+     * @param item
+     *                the item to be added
+     */
+    public void addItem(MenuItem item) {
+        Element tr;
+        if (vertical) {
+            tr = DOM.createTR();
+            DOM.appendChild(body, tr);
+        } else {
+            tr = DOM.getChild(body, 0);
+        }
+
+        DOM.appendChild(tr, item.getElement());
+
+        item.setParentMenu(this);
+        item.setSelectionStyle(false);
+        items.add(item);
+    }
+
+    /**
+     * Adds a menu item to the bar, that will fire the given command when it is
+     * selected.
+     * 
+     * @param text
+     *                the item's text
+     * @param asHTML
+     *                <code>true</code> to treat the specified text as html
+     * @param cmd
+     *                the command to be fired
+     * @return the {@link MenuItem} object created
+     */
+    public MenuItem addItem(String text, boolean asHTML, Command cmd) {
+        final MenuItem item = new MenuItem(text, asHTML, cmd);
+        addItem(item);
+        return item;
+    }
+
+    /**
+     * Adds a menu item to the bar, that will open the specified menu when it is
+     * selected.
+     * 
+     * @param text
+     *                the item's text
+     * @param asHTML
+     *                <code>true</code> to treat the specified text as html
+     * @param popup
+     *                the menu to be cascaded from it
+     * @return the {@link MenuItem} object created
+     */
+    public MenuItem addItem(String text, boolean asHTML, MenuBar popup) {
+        final MenuItem item = new MenuItem(text, asHTML, popup);
+        addItem(item);
+        return item;
+    }
+
+    /**
+     * Adds a menu item to the bar, that will fire the given command when it is
+     * selected.
+     * 
+     * @param text
+     *                the item's text
+     * @param cmd
+     *                the command to be fired
+     * @return the {@link MenuItem} object created
+     */
+    public MenuItem addItem(String text, Command cmd) {
+        final MenuItem item = new MenuItem(text, cmd);
+        addItem(item);
+        return item;
+    }
+
+    /**
+     * Adds a menu item to the bar, that will open the specified menu when it is
+     * selected.
+     * 
+     * @param text
+     *                the item's text
+     * @param popup
+     *                the menu to be cascaded from it
+     * @return the {@link MenuItem} object created
+     */
+    public MenuItem addItem(String text, MenuBar popup) {
+        final MenuItem item = new MenuItem(text, popup);
+        addItem(item);
+        return item;
+    }
+
+    /**
+     * Removes all menu items from this menu bar.
+     */
+    public void clearItems() {
+        final Element container = getItemContainerElement();
+        while (DOM.getChildCount(container) > 0) {
+            DOM.removeChild(container, DOM.getChild(container, 0));
+        }
+        items.clear();
+    }
+
+    /**
+     * Gets whether this menu bar's child menus will open when the mouse is
+     * moved over it.
+     * 
+     * @return <code>true</code> if child menus will auto-open
+     */
+    public boolean getAutoOpen() {
+        return autoOpen;
+    }
+
+    public void onBrowserEvent(Event event) {
+        super.onBrowserEvent(event);
+
+        final MenuItem item = findItem(DOM.eventGetTarget(event));
+        switch (DOM.eventGetType(event)) {
+        case Event.ONCLICK: {
+            // Fire an item's command when the user clicks on it.
+            if (item != null) {
+                doItemAction(item, true);
+            }
+            break;
+        }
+
+        case Event.ONMOUSEOVER: {
+            if (item != null) {
+                itemOver(item);
+            }
+            break;
+        }
+
+        case Event.ONMOUSEOUT: {
+            if (item != null) {
+                itemOver(null);
+            }
+            break;
+        }
+        }
+    }
+
+    public void onPopupClosed(PopupPanel sender, boolean autoClosed) {
+        // If the menu popup was auto-closed, close all of its parents as well.
+        if (autoClosed) {
+            closeAllParents();
+        }
+
+        // When the menu popup closes, remember that no item is
+        // currently showing a popup menu.
+        onHide();
+        shownChildMenu = null;
+        popup = null;
+    }
+
+    /**
+     * Removes the specified menu item from the bar.
+     * 
+     * @param item
+     *                the item to be removed
+     */
+    public void removeItem(MenuItem item) {
+        final int idx = items.indexOf(item);
+        if (idx == -1) {
+            return;
+        }
+
+        final Element container = getItemContainerElement();
+        DOM.removeChild(container, DOM.getChild(container, idx));
+        items.remove(idx);
+    }
+
+    /**
+     * Sets whether this menu bar's child menus will open when the mouse is
+     * moved over it.
+     * 
+     * @param autoOpen
+     *                <code>true</code> to cause child menus to auto-open
+     */
+    public void setAutoOpen(boolean autoOpen) {
+        this.autoOpen = autoOpen;
+    }
+
+    /**
+     * Returns a list containing the <code>MenuItem</code> objects in the menu
+     * bar. If there are no items in the menu bar, then an empty
+     * <code>List</code> object will be returned.
+     * 
+     * @return a list containing the <code>MenuItem</code> objects in the menu
+     *         bar
+     */
+    protected List getItems() {
+        return items;
+    }
+
+    /**
+     * Returns the <code>MenuItem</code> that is currently selected
+     * (highlighted) by the user. If none of the items in the menu are currently
+     * selected, then <code>null</code> will be returned.
+     * 
+     * @return the <code>MenuItem</code> that is currently selected, or
+     *         <code>null</code> if no items are currently selected
+     */
+    protected MenuItem getSelectedItem() {
+        return selectedItem;
+    }
+
+    protected void onDetach() {
+        // When the menu is detached, make sure to close all of its children.
+        if (popup != null) {
+            popup.hide();
+        }
+
+        super.onDetach();
+    }
+
+    /*
+     * Closes all parent menu popups.
+     */
+    void closeAllParents() {
+        MenuBar curMenu = this;
+        while (curMenu != null) {
+            curMenu.close();
+
+            if ((curMenu.parentMenu == null) && (curMenu.selectedItem != null)) {
+                curMenu.selectedItem.setSelectionStyle(false);
+                curMenu.selectedItem = null;
+            }
+
+            curMenu = curMenu.parentMenu;
+        }
+    }
+
+    /*
+     * Performs the action associated with the given menu item. If the item has
+     * a popup associated with it, the popup will be shown. If it has a command
+     * associated with it, and 'fireCommand' is true, then the command will be
+     * fired. Popups associated with other items will be hidden.
+     * 
+     * @param item the item whose popup is to be shown. @param fireCommand
+     * <code>true</code> if the item's command should be fired, <code>false</code>
+     * otherwise.
+     */
+    void doItemAction(final MenuItem item, boolean fireCommand) {
+        // If the given item is already showing its menu, we're done.
+        if ((shownChildMenu != null) && (item.getSubMenu() == shownChildMenu)) {
+            return;
+        }
+
+        // If another item is showing its menu, then hide it.
+        if (shownChildMenu != null) {
+            shownChildMenu.onHide();
+            popup.hide();
+        }
+
+        // If the item has no popup, optionally fire its command.
+        if (item.getSubMenu() == null) {
+            if (fireCommand) {
+                // Close this menu and all of its parents.
+                closeAllParents();
+
+                // Fire the item's command.
+                final Command cmd = item.getCommand();
+                if (cmd != null) {
+                    DeferredCommand.addCommand(cmd);
+                }
+            }
+            return;
+        }
+
+        // Ensure that the item is selected.
+        selectItem(item);
+
+        // Create a new popup for this item, and position it next to
+        // the item (below if this is a horizontal menu bar, to the
+        // right if it's a vertical bar).
+        popup = new ToolkitOverlay(true) {
+            {
+                setWidget(item.getSubMenu());
+                item.getSubMenu().onShow();
+            }
+
+            public boolean onEventPreview(Event event) {
+                // Hook the popup panel's event preview. We use this to keep it
+                // from
+                // auto-hiding when the parent menu is clicked.
+                switch (DOM.eventGetType(event)) {
+                case Event.ONCLICK:
+                    // If the event target is part of the parent menu, suppress
+                    // the
+                    // event altogether.
+                    final Element target = DOM.eventGetTarget(event);
+                    final Element parentMenuElement = item.getParentMenu()
+                            .getElement();
+                    if (DOM.isOrHasChild(parentMenuElement, target)) {
+                        return false;
+                    }
+                    break;
+                }
+
+                return super.onEventPreview(event);
+            }
+        };
+        popup.addPopupListener(this);
+
+        if (vertical) {
+            popup.setPopupPosition(item.getAbsoluteLeft()
+                    + item.getOffsetWidth(), item.getAbsoluteTop());
+        } else {
+            popup.setPopupPosition(item.getAbsoluteLeft(), item
+                    .getAbsoluteTop()
+                    + item.getOffsetHeight());
+        }
+
+        shownChildMenu = item.getSubMenu();
+        item.getSubMenu().parentMenu = this;
+
+        // Show the popup, ensuring that the menubar's event preview remains on
+        // top
+        // of the popup's.
+        popup.show();
+    }
+
+    void itemOver(MenuItem item) {
+        if (item == null) {
+            // Don't clear selection if the currently selected item's menu is
+            // showing.
+            if ((selectedItem != null)
+                    && (shownChildMenu == selectedItem.getSubMenu())) {
+                return;
+            }
+        }
+
+        // Style the item selected when the mouse enters.
+        selectItem(item);
+
+        // If child menus are being shown, or this menu is itself
+        // a child menu, automatically show an item's child menu
+        // when the mouse enters.
+        if (item != null) {
+            if ((shownChildMenu != null) || (parentMenu != null) || autoOpen) {
+                doItemAction(item, false);
+            }
+        }
+    }
+
+    void selectItem(MenuItem item) {
+        if (item == selectedItem) {
+            return;
+        }
+
+        if (selectedItem != null) {
+            selectedItem.setSelectionStyle(false);
+        }
+
+        if (item != null) {
+            item.setSelectionStyle(true);
+        }
+
+        selectedItem = item;
+    }
+
+    /**
+     * Closes this menu (if it is a popup).
+     */
+    private void close() {
+        if (parentMenu != null) {
+            parentMenu.popup.hide();
+        }
+    }
+
+    private MenuItem findItem(Element hItem) {
+        for (int i = 0; i < items.size(); ++i) {
+            final MenuItem item = (MenuItem) items.get(i);
+            if (DOM.isOrHasChild(item.getElement(), hItem)) {
+                return item;
+            }
+        }
+
+        return null;
+    }
+
+    private Element getItemContainerElement() {
+        if (vertical) {
+            return body;
+        } else {
+            return DOM.getChild(body, 0);
+        }
+    }
+
+    /*
+     * This method is called when a menu bar is hidden, so that it can hide any
+     * child popups that are currently being shown.
+     */
+    private void onHide() {
+        if (shownChildMenu != null) {
+            shownChildMenu.onHide();
+            popup.hide();
+        }
+    }
+
+    /*
+     * This method is called when a menu bar is shown.
+     */
+    private void onShow() {
+        // Select the first item when a menu is shown.
+        if (items.size() > 0) {
+            selectItem((MenuItem) items.get(0));
+        }
+    }
+}
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/MenuItem.java b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/MenuItem.java
new file mode 100644 (file)
index 0000000..184e3bf
--- /dev/null
@@ -0,0 +1,186 @@
+/* 
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.itmill.toolkit.terminal.gwt.client.ui;
+
+/*
+ * Copyright 2007 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+// COPIED HERE DUE package privates in GWT
+import com.google.gwt.user.client.Command;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.ui.HasHTML;
+import com.google.gwt.user.client.ui.UIObject;
+
+/**
+ * A widget that can be placed in a
+ * {@link com.google.gwt.user.client.ui.MenuBar}. Menu items can either fire a
+ * {@link com.google.gwt.user.client.Command} when they are clicked, or open a
+ * cascading sub-menu.
+ */
+public class MenuItem extends UIObject implements HasHTML {
+
+    private static final String DEPENDENT_STYLENAME_SELECTED_ITEM = "selected";
+
+    private Command command;
+    private MenuBar parentMenu, subMenu;
+
+    /**
+     * Constructs a new menu item that fires a command when it is selected.
+     * 
+     * @param text
+     *                the item's text
+     * @param cmd
+     *                the command to be fired when it is selected
+     */
+    public MenuItem(String text, Command cmd) {
+        this(text, false);
+        setCommand(cmd);
+    }
+
+    /**
+     * Constructs a new menu item that fires a command when it is selected.
+     * 
+     * @param text
+     *                the item's text
+     * @param asHTML
+     *                <code>true</code> to treat the specified text as html
+     * @param cmd
+     *                the command to be fired when it is selected
+     */
+    public MenuItem(String text, boolean asHTML, Command cmd) {
+        this(text, asHTML);
+        setCommand(cmd);
+    }
+
+    /**
+     * Constructs a new menu item that cascades to a sub-menu when it is
+     * selected.
+     * 
+     * @param text
+     *                the item's text
+     * @param subMenu
+     *                the sub-menu to be displayed when it is selected
+     */
+    public MenuItem(String text, MenuBar subMenu) {
+        this(text, false);
+        setSubMenu(subMenu);
+    }
+
+    /**
+     * Constructs a new menu item that cascades to a sub-menu when it is
+     * selected.
+     * 
+     * @param text
+     *                the item's text
+     * @param asHTML
+     *                <code>true</code> to treat the specified text as html
+     * @param subMenu
+     *                the sub-menu to be displayed when it is selected
+     */
+    public MenuItem(String text, boolean asHTML, MenuBar subMenu) {
+        this(text, asHTML);
+        setSubMenu(subMenu);
+    }
+
+    MenuItem(String text, boolean asHTML) {
+        setElement(DOM.createTD());
+        setSelectionStyle(false);
+
+        if (asHTML) {
+            setHTML(text);
+        } else {
+            setText(text);
+        }
+        setStyleName("gwt-MenuItem");
+    }
+
+    /**
+     * Gets the command associated with this item.
+     * 
+     * @return this item's command, or <code>null</code> if none exists
+     */
+    public Command getCommand() {
+        return command;
+    }
+
+    public String getHTML() {
+        return DOM.getInnerHTML(getElement());
+    }
+
+    /**
+     * Gets the menu that contains this item.
+     * 
+     * @return the parent menu, or <code>null</code> if none exists.
+     */
+    public MenuBar getParentMenu() {
+        return parentMenu;
+    }
+
+    /**
+     * Gets the sub-menu associated with this item.
+     * 
+     * @return this item's sub-menu, or <code>null</code> if none exists
+     */
+    public MenuBar getSubMenu() {
+        return subMenu;
+    }
+
+    public String getText() {
+        return DOM.getInnerText(getElement());
+    }
+
+    /**
+     * Sets the command associated with this item.
+     * 
+     * @param cmd
+     *                the command to be associated with this item
+     */
+    public void setCommand(Command cmd) {
+        command = cmd;
+    }
+
+    public void setHTML(String html) {
+        DOM.setInnerHTML(getElement(), html);
+    }
+
+    /**
+     * Sets the sub-menu associated with this item.
+     * 
+     * @param subMenu
+     *                this item's new sub-menu
+     */
+    public void setSubMenu(MenuBar subMenu) {
+        this.subMenu = subMenu;
+    }
+
+    public void setText(String text) {
+        DOM.setInnerText(getElement(), text);
+    }
+
+    void setParentMenu(MenuBar parentMenu) {
+        this.parentMenu = parentMenu;
+    }
+
+    void setSelectionStyle(boolean selected) {
+        if (selected) {
+            addStyleDependentName(DEPENDENT_STYLENAME_SELECTED_ITEM);
+        } else {
+            removeStyleDependentName(DEPENDENT_STYLENAME_SELECTED_ITEM);
+        }
+    }
+}
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/Notification.java b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/Notification.java
new file mode 100644 (file)
index 0000000..267a07d
--- /dev/null
@@ -0,0 +1,289 @@
+/* \r
+@ITMillApache2LicenseForJavaFiles@\r
+ */\r
+\r
+package com.itmill.toolkit.terminal.gwt.client.ui;\r
+\r
+import java.util.ArrayList;\r
+import java.util.EventObject;\r
+import java.util.Iterator;\r
+\r
+import com.google.gwt.user.client.DOM;\r
+import com.google.gwt.user.client.Element;\r
+import com.google.gwt.user.client.Event;\r
+import com.google.gwt.user.client.Timer;\r
+import com.google.gwt.user.client.ui.HTML;\r
+import com.google.gwt.user.client.ui.Widget;\r
+import com.itmill.toolkit.terminal.gwt.client.BrowserInfo;\r
+\r
+public class Notification extends ToolkitOverlay {\r
+\r
+    public static final int CENTERED = 1;\r
+    public static final int CENTERED_TOP = 2;\r
+    public static final int CENTERED_BOTTOM = 3;\r
+    public static final int TOP_LEFT = 4;\r
+    public static final int TOP_RIGHT = 5;\r
+    public static final int BOTTOM_LEFT = 6;\r
+    public static final int BOTTOM_RIGHT = 7;\r
+\r
+    public static final int DELAY_FOREVER = -1;\r
+    public static final int DELAY_NONE = 0;\r
+\r
+    private static final String STYLENAME = "i-Notification";\r
+    private static final int mouseMoveThreshold = 7;\r
+    private static final int Z_INDEX_BASE = 20000;\r
+\r
+    private int startOpacity = 90;\r
+    private int fadeMsec = 400;\r
+    private int delayMsec = 1000;\r
+\r
+    private Timer fader;\r
+    private Timer delay;\r
+\r
+    private int x = -1;\r
+    private int y = -1;\r
+\r
+    private String temporaryStyle;\r
+\r
+    private ArrayList listeners;\r
+\r
+    public Notification() {\r
+        setStylePrimaryName(STYLENAME);\r
+        sinkEvents(Event.ONCLICK);\r
+        DOM.setStyleAttribute(getElement(), "zIndex", "" + Z_INDEX_BASE);\r
+    }\r
+\r
+    public Notification(int delayMsec) {\r
+        this();\r
+        this.delayMsec = delayMsec;\r
+    }\r
+\r
+    public Notification(int delayMsec, int fadeMsec, int startOpacity) {\r
+        this(delayMsec);\r
+        this.fadeMsec = fadeMsec;\r
+        this.startOpacity = startOpacity;\r
+    }\r
+\r
+    public void startDelay() {\r
+        DOM.removeEventPreview(this);\r
+        if (delayMsec > 0) {\r
+            delay = new Timer() {\r
+                public void run() {\r
+                    fade();\r
+                }\r
+            };\r
+            delay.scheduleRepeating(delayMsec);\r
+        } else if (delayMsec == 0) {\r
+            fade();\r
+        }\r
+    }\r
+\r
+    public void show() {\r
+        show(CENTERED);\r
+    }\r
+\r
+    public void show(String style) {\r
+        show(CENTERED, style);\r
+    }\r
+\r
+    public void show(int position) {\r
+        show(position, null);\r
+    }\r
+\r
+    public void show(Widget widget, int position, String style) {\r
+        setWidget(widget);\r
+        show(position, style);\r
+    }\r
+\r
+    public void show(String html, int position, String style) {\r
+        setWidget(new HTML(html));\r
+        show(position, style);\r
+    }\r
+\r
+    public void show(int position, String style) {\r
+        setOpacity(getElement(), startOpacity);\r
+        if (style != null) {\r
+            temporaryStyle = style;\r
+            addStyleName(style);\r
+        }\r
+        super.show();\r
+        setPosition(position);\r
+    }\r
+\r
+    public void hide() {\r
+        DOM.removeEventPreview(this);\r
+        cancelDelay();\r
+        cancelFade();\r
+        if (temporaryStyle != null) {\r
+            removeStyleName(temporaryStyle);\r
+            temporaryStyle = null;\r
+        }\r
+        super.hide();\r
+        fireEvent(new HideEvent(this));\r
+    }\r
+\r
+    public void fade() {\r
+        DOM.removeEventPreview(this);\r
+        cancelDelay();\r
+        fader = new Timer() {\r
+            int opacity = startOpacity;\r
+\r
+            public void run() {\r
+                opacity -= 5;\r
+                setOpacity(getElement(), opacity);\r
+                if (opacity <= 0) {\r
+                    cancel();\r
+                    hide();\r
+                    if (BrowserInfo.get().isOpera()) {\r
+                        // tray notification on opera needs to explicitly define\r
+                        // size, reset it\r
+                        DOM.setStyleAttribute(getElement(), "width", "");\r
+                        DOM.setStyleAttribute(getElement(), "height", "");\r
+                    }\r
+\r
+                }\r
+            }\r
+        };\r
+        final int msec = fadeMsec / (startOpacity / 5);\r
+        fader.scheduleRepeating(msec);\r
+    }\r
+\r
+    public void setPosition(int position) {\r
+        final Element el = getElement();\r
+        DOM.setStyleAttribute(el, "top", "");\r
+        DOM.setStyleAttribute(el, "left", "");\r
+        DOM.setStyleAttribute(el, "bottom", "");\r
+        DOM.setStyleAttribute(el, "right", "");\r
+        switch (position) {\r
+        case TOP_LEFT:\r
+            DOM.setStyleAttribute(el, "top", "0px");\r
+            DOM.setStyleAttribute(el, "left", "0px");\r
+            break;\r
+        case TOP_RIGHT:\r
+            DOM.setStyleAttribute(el, "top", "0px");\r
+            DOM.setStyleAttribute(el, "right", "0px");\r
+            break;\r
+        case BOTTOM_RIGHT:\r
+            DOM.setStyleAttribute(el, "position", "absolute");\r
+            if (BrowserInfo.get().isOpera()) {\r
+                // tray notification on opera needs explicitly defined size\r
+                DOM.setStyleAttribute(el, "width", getOffsetWidth() + "px");\r
+                DOM.setStyleAttribute(el, "height", getOffsetHeight() + "px");\r
+            }\r
+            DOM.setStyleAttribute(el, "bottom", "0px");\r
+            DOM.setStyleAttribute(el, "right", "0px");\r
+            break;\r
+        case BOTTOM_LEFT:\r
+            DOM.setStyleAttribute(el, "bottom", "0px");\r
+            DOM.setStyleAttribute(el, "left", "0px");\r
+            break;\r
+        case CENTERED_TOP:\r
+            center();\r
+            DOM.setStyleAttribute(el, "top", "0px");\r
+            break;\r
+        case CENTERED_BOTTOM:\r
+            center();\r
+            DOM.setStyleAttribute(el, "top", "");\r
+            DOM.setStyleAttribute(el, "bottom", "0px");\r
+            break;\r
+        default:\r
+        case CENTERED:\r
+            center();\r
+            break;\r
+        }\r
+    }\r
+\r
+    private void cancelFade() {\r
+        if (fader != null) {\r
+            fader.cancel();\r
+            fader = null;\r
+        }\r
+    }\r
+\r
+    private void cancelDelay() {\r
+        if (delay != null) {\r
+            delay.cancel();\r
+            delay = null;\r
+        }\r
+    }\r
+\r
+    private void setOpacity(Element el, int opacity) {\r
+        DOM.setStyleAttribute(el, "opacity", "" + (opacity / 100.0));\r
+        DOM.setStyleAttribute(el, "filter", "Alpha(opacity=" + opacity + ")");\r
+\r
+    }\r
+\r
+    public void onBrowserEvent(Event event) {\r
+        DOM.removeEventPreview(this);\r
+        if (fader == null) {\r
+            fade();\r
+        }\r
+    }\r
+\r
+    public boolean onEventPreview(Event event) {\r
+        int type = DOM.eventGetType(event);\r
+        // "modal"\r
+        if (delayMsec == -1) {\r
+            if (type == Event.ONCLICK\r
+                    && DOM\r
+                            .isOrHasChild(getElement(), DOM\r
+                                    .eventGetTarget(event))) {\r
+                fade();\r
+            }\r
+            return false;\r
+        }\r
+        // default\r
+        switch (type) {\r
+        case Event.ONMOUSEMOVE:\r
+\r
+            if (x < 0) {\r
+                x = DOM.eventGetClientX(event);\r
+                y = DOM.eventGetClientY(event);\r
+            } else if (Math.abs(DOM.eventGetClientX(event) - x) > mouseMoveThreshold\r
+                    || Math.abs(DOM.eventGetClientY(event) - y) > mouseMoveThreshold) {\r
+                startDelay();\r
+            }\r
+            break;\r
+        case Event.ONCLICK:\r
+        case Event.ONDBLCLICK:\r
+        case Event.KEYEVENTS:\r
+        case Event.ONSCROLL:\r
+        default:\r
+            startDelay();\r
+        }\r
+        return true;\r
+    }\r
+\r
+    public void addEventListener(EventListener listener) {\r
+        if (listeners == null) {\r
+            listeners = new ArrayList();\r
+        }\r
+        listeners.add(listener);\r
+    }\r
+\r
+    public void removeEventListener(EventListener listener) {\r
+        if (listeners == null) {\r
+            return;\r
+        }\r
+        listeners.remove(listener);\r
+    }\r
+\r
+    private void fireEvent(HideEvent event) {\r
+        if (listeners != null) {\r
+            for (Iterator it = listeners.iterator(); it.hasNext();) {\r
+                EventListener l = (EventListener) it.next();\r
+                l.notificationHidden(event);\r
+            }\r
+        }\r
+    }\r
+\r
+    public class HideEvent extends EventObject {\r
+        public HideEvent(Object source) {\r
+            super(source);\r
+        }\r
+    }\r
+\r
+    public interface EventListener extends java.util.EventListener {\r
+        public void notificationHidden(HideEvent event);\r
+    }\r
+}\r
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/ShortcutActionHandler.java b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/ShortcutActionHandler.java
new file mode 100644 (file)
index 0000000..244710c
--- /dev/null
@@ -0,0 +1,177 @@
+/* 
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.itmill.toolkit.terminal.gwt.client.ui;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+
+import com.google.gwt.user.client.Command;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.DeferredCommand;
+import com.google.gwt.user.client.Element;
+import com.google.gwt.user.client.Event;
+import com.google.gwt.user.client.ui.KeyboardListener;
+import com.google.gwt.user.client.ui.KeyboardListenerCollection;
+import com.itmill.toolkit.terminal.gwt.client.ApplicationConnection;
+import com.itmill.toolkit.terminal.gwt.client.UIDL;
+
+/**
+ * A helper class to implement keyboard shorcut handling. Keeps a list of owners
+ * actions and fires actions to server. User class needs to delegate keyboard
+ * events to handleKeyboardEvents function.
+ * 
+ * @author IT Mill ltd
+ */
+public class ShortcutActionHandler {
+    private final ArrayList actions = new ArrayList();
+    private ApplicationConnection client;
+    private String paintableId;
+
+    /**
+     * 
+     * @param pid
+     *                Paintable id
+     * @param c
+     *                reference to application connections
+     */
+    public ShortcutActionHandler(String pid, ApplicationConnection c) {
+        paintableId = pid;
+        client = c;
+    }
+
+    /**
+     * Updates list of actions this handler listens to.
+     * 
+     * @param c
+     *                UIDL snippet containing actions
+     */
+    public void updateActionMap(UIDL c) {
+        actions.clear();
+        final Iterator it = c.getChildIterator();
+        while (it.hasNext()) {
+            final UIDL action = (UIDL) it.next();
+
+            int[] modifiers = null;
+            if (action.hasAttribute("mk")) {
+                modifiers = action.getIntArrayAttribute("mk");
+            }
+
+            final ShortcutKeyCombination kc = new ShortcutKeyCombination(action
+                    .getIntAttribute("kc"), modifiers);
+            final String key = action.getStringAttribute("key");
+            final String caption = action.getStringAttribute("caption");
+            actions.add(new ShortcutAction(key, kc, caption));
+        }
+    }
+
+    public void handleKeyboardEvent(Event event) {
+        final int modifiers = KeyboardListenerCollection
+                .getKeyboardModifiers(event);
+        final char keyCode = (char) DOM.eventGetKeyCode(event);
+        final ShortcutKeyCombination kc = new ShortcutKeyCombination(keyCode,
+                modifiers);
+        final Iterator it = actions.iterator();
+        while (it.hasNext()) {
+            final ShortcutAction a = (ShortcutAction) it.next();
+            if (a.getShortcutCombination().equals(kc)) {
+                shakeTarget(DOM.eventGetTarget(event));
+                DeferredCommand.addCommand(new Command() {
+                    public void execute() {
+                        client.updateVariable(paintableId, "action",
+                                a.getKey(), true);
+                    }
+                });
+                break;
+            }
+        }
+    }
+
+    public static native void shakeTarget(Element e)
+    /*-{
+            if(e.blur) {
+                e.blur();
+                e.focus();
+       }
+    }-*/;
+
+}
+
+class ShortcutKeyCombination {
+
+    public static final int SHIFT = 16;
+    public static final int CTRL = 17;
+    public static final int ALT = 18;
+
+    char keyCode = 0;
+    private int modifiersMask;
+
+    public ShortcutKeyCombination() {
+    }
+
+    ShortcutKeyCombination(char kc, int modifierMask) {
+        keyCode = kc;
+        modifiersMask = modifierMask;
+    }
+
+    ShortcutKeyCombination(int kc, int[] modifiers) {
+        keyCode = (char) kc;
+        keyCode = Character.toUpperCase(keyCode);
+
+        modifiersMask = 0;
+        if (modifiers != null) {
+            for (int i = 0; i < modifiers.length; i++) {
+                switch (modifiers[i]) {
+                case ALT:
+                    modifiersMask = modifiersMask
+                            | KeyboardListener.MODIFIER_ALT;
+                    break;
+                case CTRL:
+                    modifiersMask = modifiersMask
+                            | KeyboardListener.MODIFIER_CTRL;
+                    break;
+                case SHIFT:
+                    modifiersMask = modifiersMask
+                            | KeyboardListener.MODIFIER_SHIFT;
+                    break;
+                default:
+                    break;
+                }
+            }
+        }
+    }
+
+    public boolean equals(ShortcutKeyCombination other) {
+        if (keyCode == other.keyCode && modifiersMask == other.modifiersMask) {
+            return true;
+        }
+        return false;
+    }
+}
+
+class ShortcutAction {
+
+    private final ShortcutKeyCombination sc;
+    private final String caption;
+    private final String key;
+
+    public ShortcutAction(String key, ShortcutKeyCombination sc, String caption) {
+        this.sc = sc;
+        this.key = key;
+        this.caption = caption;
+    }
+
+    public ShortcutKeyCombination getShortcutCombination() {
+        return sc;
+    }
+
+    public String getCaption() {
+        return caption;
+    }
+
+    public String getKey() {
+        return key;
+    }
+
+}
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/Table.java b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/Table.java
new file mode 100644 (file)
index 0000000..40cccf0
--- /dev/null
@@ -0,0 +1,15 @@
+/* 
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.itmill.toolkit.terminal.gwt.client.ui;
+
+import com.google.gwt.user.client.ui.HasWidgets;
+import com.itmill.toolkit.terminal.gwt.client.Paintable;
+
+public interface Table extends Paintable, HasWidgets {
+    final int SELECT_MODE_NONE = 0;
+    final int SELECT_MODE_SINGLE = 1;
+    final int SELECT_MODE_MULTI = 2;
+
+}
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/Time.java b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/Time.java
new file mode 100644 (file)
index 0000000..ee7b40f
--- /dev/null
@@ -0,0 +1,317 @@
+/* \r
+@ITMillApache2LicenseForJavaFiles@\r
+ */\r
+\r
+package com.itmill.toolkit.terminal.gwt.client.ui;\r
+\r
+import java.util.Date;\r
+\r
+import com.google.gwt.user.client.ui.ChangeListener;\r
+import com.google.gwt.user.client.ui.FlowPanel;\r
+import com.google.gwt.user.client.ui.ListBox;\r
+import com.google.gwt.user.client.ui.Widget;\r
+\r
+public class Time extends FlowPanel implements ChangeListener {\r
+\r
+    private final IDateField datefield;\r
+\r
+    private ListBox hours;\r
+\r
+    private ListBox mins;\r
+\r
+    private ListBox sec;\r
+\r
+    private ListBox msec;\r
+\r
+    private ListBox ampm;\r
+\r
+    private int resolution = IDateField.RESOLUTION_HOUR;\r
+\r
+    private boolean readonly;\r
+\r
+    public Time(IDateField parent) {\r
+        super();\r
+        datefield = parent;\r
+        setStyleName(IDateField.CLASSNAME + "-time");\r
+    }\r
+\r
+    private void buildTime(boolean redraw) {\r
+        final boolean thc = datefield.getDateTimeService().isTwelveHourClock();\r
+        if (redraw) {\r
+            clear();\r
+            final int numHours = thc ? 12 : 24;\r
+            hours = new ListBox();\r
+            hours.setStyleName(INativeSelect.CLASSNAME);\r
+            for (int i = 0; i < numHours; i++) {\r
+                hours.addItem((i < 10) ? "0" + i : "" + i);\r
+            }\r
+            hours.addChangeListener(this);\r
+            if (thc) {\r
+                ampm = new ListBox();\r
+                ampm.setStyleName(INativeSelect.CLASSNAME);\r
+                final String[] ampmText = datefield.getDateTimeService()\r
+                        .getAmPmStrings();\r
+                ampm.addItem(ampmText[0]);\r
+                ampm.addItem(ampmText[1]);\r
+                ampm.addChangeListener(this);\r
+            }\r
+\r
+            if (datefield.getCurrentResolution() >= IDateField.RESOLUTION_MIN) {\r
+                mins = new ListBox();\r
+                mins.setStyleName(INativeSelect.CLASSNAME);\r
+                for (int i = 0; i < 60; i++) {\r
+                    mins.addItem((i < 10) ? "0" + i : "" + i);\r
+                }\r
+                mins.addChangeListener(this);\r
+            }\r
+            if (datefield.getCurrentResolution() >= IDateField.RESOLUTION_SEC) {\r
+                sec = new ListBox();\r
+                sec.setStyleName(INativeSelect.CLASSNAME);\r
+                for (int i = 0; i < 60; i++) {\r
+                    sec.addItem((i < 10) ? "0" + i : "" + i);\r
+                }\r
+                sec.addChangeListener(this);\r
+            }\r
+            if (datefield.getCurrentResolution() == IDateField.RESOLUTION_MSEC) {\r
+                msec = new ListBox();\r
+                msec.setStyleName(INativeSelect.CLASSNAME);\r
+                for (int i = 0; i < 1000; i++) {\r
+                    if (i < 10) {\r
+                        msec.addItem("00" + i);\r
+                    } else if (i < 100) {\r
+                        msec.addItem("0" + i);\r
+                    } else {\r
+                        msec.addItem("" + i);\r
+                    }\r
+                }\r
+                msec.addChangeListener(this);\r
+            }\r
+\r
+            final String delimiter = datefield.getDateTimeService()\r
+                    .getClockDelimeter();\r
+            final boolean ro = datefield.isReadonly();\r
+\r
+            if (ro) {\r
+                int h = 0;\r
+                if (datefield.getCurrentDate() != null) {\r
+                    h = datefield.getCurrentDate().getHours();\r
+                }\r
+                if (thc) {\r
+                    h -= h < 12 ? 0 : 12;\r
+                }\r
+                add(new ILabel(h < 10 ? "0" + h : "" + h));\r
+            } else {\r
+                add(hours);\r
+            }\r
+\r
+            if (datefield.getCurrentResolution() >= IDateField.RESOLUTION_MIN) {\r
+                add(new ILabel(delimiter));\r
+                if (ro) {\r
+                    final int m = mins.getSelectedIndex();\r
+                    add(new ILabel(m < 10 ? "0" + m : "" + m));\r
+                } else {\r
+                    add(mins);\r
+                }\r
+            }\r
+            if (datefield.getCurrentResolution() >= IDateField.RESOLUTION_SEC) {\r
+                add(new ILabel(delimiter));\r
+                if (ro) {\r
+                    final int s = sec.getSelectedIndex();\r
+                    add(new ILabel(s < 10 ? "0" + s : "" + s));\r
+                } else {\r
+                    add(sec);\r
+                }\r
+            }\r
+            if (datefield.getCurrentResolution() == IDateField.RESOLUTION_MSEC) {\r
+                add(new ILabel("."));\r
+                if (ro) {\r
+                    final int m = datefield.getMilliseconds();\r
+                    final String ms = m < 100 ? "0" + m : "" + m;\r
+                    add(new ILabel(m < 10 ? "0" + ms : ms));\r
+                } else {\r
+                    add(msec);\r
+                }\r
+            }\r
+            if (datefield.getCurrentResolution() == IDateField.RESOLUTION_HOUR) {\r
+                add(new ILabel(delimiter + "00")); // o'clock\r
+            }\r
+            if (thc) {\r
+                add(new ILabel("&nbsp;"));\r
+                if (ro) {\r
+                    add(new ILabel(ampm.getItemText(datefield.getCurrentDate()\r
+                            .getHours() < 12 ? 0 : 1)));\r
+                } else {\r
+                    add(ampm);\r
+                }\r
+            }\r
+\r
+            if (ro) {\r
+                return;\r
+            }\r
+        }\r
+\r
+        // Update times\r
+        Date cdate = datefield.getCurrentDate();\r
+        boolean selected = true;\r
+        if (cdate == null) {\r
+            cdate = new Date();\r
+            selected = false;\r
+        }\r
+        if (thc) {\r
+            int h = cdate.getHours();\r
+            ampm.setSelectedIndex(h < 12 ? 0 : 1);\r
+            h -= ampm.getSelectedIndex() * 12;\r
+            hours.setSelectedIndex(h);\r
+        } else {\r
+            hours.setSelectedIndex(cdate.getHours());\r
+        }\r
+        if (datefield.getCurrentResolution() >= IDateField.RESOLUTION_MIN) {\r
+            mins.setSelectedIndex(cdate.getMinutes());\r
+        }\r
+        if (datefield.getCurrentResolution() >= IDateField.RESOLUTION_SEC) {\r
+            sec.setSelectedIndex(cdate.getSeconds());\r
+        }\r
+        if (datefield.getCurrentResolution() == IDateField.RESOLUTION_MSEC) {\r
+            if (selected) {\r
+                msec.setSelectedIndex(datefield.getMilliseconds());\r
+            } else {\r
+                msec.setSelectedIndex(0);\r
+            }\r
+        }\r
+        if (thc) {\r
+            ampm.setSelectedIndex(cdate.getHours() < 12 ? 0 : 1);\r
+        }\r
+\r
+        if (datefield.isReadonly() && !redraw) {\r
+            // Do complete redraw when in read-only status\r
+            clear();\r
+            final String delimiter = datefield.getDateTimeService()\r
+                    .getClockDelimeter();\r
+\r
+            int h = cdate.getHours();\r
+            if (thc) {\r
+                h -= h < 12 ? 0 : 12;\r
+            }\r
+            add(new ILabel(h < 10 ? "0" + h : "" + h));\r
+\r
+            if (datefield.getCurrentResolution() >= IDateField.RESOLUTION_MIN) {\r
+                add(new ILabel(delimiter));\r
+                final int m = mins.getSelectedIndex();\r
+                add(new ILabel(m < 10 ? "0" + m : "" + m));\r
+            }\r
+            if (datefield.getCurrentResolution() >= IDateField.RESOLUTION_SEC) {\r
+                add(new ILabel(delimiter));\r
+                final int s = sec.getSelectedIndex();\r
+                add(new ILabel(s < 10 ? "0" + s : "" + s));\r
+            }\r
+            if (datefield.getCurrentResolution() == IDateField.RESOLUTION_MSEC) {\r
+                add(new ILabel("."));\r
+                final int m = datefield.getMilliseconds();\r
+                final String ms = m < 100 ? "0" + m : "" + m;\r
+                add(new ILabel(m < 10 ? "0" + ms : ms));\r
+            }\r
+            if (datefield.getCurrentResolution() == IDateField.RESOLUTION_HOUR) {\r
+                add(new ILabel(delimiter + "00")); // o'clock\r
+            }\r
+            if (thc) {\r
+                add(new ILabel("&nbsp;"));\r
+                add(new ILabel(ampm.getItemText(cdate.getHours() < 12 ? 0 : 1)));\r
+            }\r
+        }\r
+\r
+        final boolean enabled = datefield.isEnabled();\r
+        hours.setEnabled(enabled);\r
+        if (mins != null) {\r
+            mins.setEnabled(enabled);\r
+        }\r
+        if (sec != null) {\r
+            sec.setEnabled(enabled);\r
+        }\r
+        if (msec != null) {\r
+            msec.setEnabled(enabled);\r
+        }\r
+        if (ampm != null) {\r
+            ampm.setEnabled(enabled);\r
+        }\r
+\r
+    }\r
+\r
+    public void updateTime(boolean redraw) {\r
+        buildTime(redraw || resolution != datefield.getCurrentResolution()\r
+                || readonly != datefield.isReadonly());\r
+        if (datefield instanceof ITextualDate) {\r
+            ((ITextualDate) datefield).buildDate();\r
+        }\r
+        resolution = datefield.getCurrentResolution();\r
+        readonly = datefield.isReadonly();\r
+    }\r
+\r
+    public void onChange(Widget sender) {\r
+        if (datefield.getCurrentDate() == null) {\r
+            // was null on server, need to set\r
+            Date now = datefield.getShowingDate();\r
+            if (now == null) {\r
+                now = new Date();\r
+                datefield.setShowingDate(now);\r
+            }\r
+            datefield.setCurrentDate(new Date(now.getTime()));\r
+\r
+            // Init variables with current time\r
+            datefield.getClient().updateVariable(datefield.getId(), "year",\r
+                    now.getYear() + 1900, false);\r
+            datefield.getClient().updateVariable(datefield.getId(), "month",\r
+                    now.getMonth() + 1, false);\r
+            datefield.getClient().updateVariable(datefield.getId(), "day",\r
+                    now.getDate(), false);\r
+            datefield.getClient().updateVariable(datefield.getId(), "hour",\r
+                    now.getHours(), false);\r
+            datefield.getClient().updateVariable(datefield.getId(), "min",\r
+                    now.getMinutes(), false);\r
+            datefield.getClient().updateVariable(datefield.getId(), "sec",\r
+                    now.getSeconds(), false);\r
+            datefield.getClient().updateVariable(datefield.getId(), "msec",\r
+                    datefield.getMilliseconds(), false);\r
+        }\r
+        if (sender == hours) {\r
+            int h = hours.getSelectedIndex();\r
+            if (datefield.getDateTimeService().isTwelveHourClock()) {\r
+                h = h + ampm.getSelectedIndex() * 12;\r
+            }\r
+            datefield.getCurrentDate().setHours(h);\r
+            datefield.getShowingDate().setHours(h);\r
+            datefield.getClient().updateVariable(datefield.getId(), "hour", h,\r
+                    datefield.isImmediate());\r
+            updateTime(false);\r
+        } else if (sender == mins) {\r
+            final int m = mins.getSelectedIndex();\r
+            datefield.getCurrentDate().setMinutes(m);\r
+            datefield.getShowingDate().setMinutes(m);\r
+            datefield.getClient().updateVariable(datefield.getId(), "min", m,\r
+                    datefield.isImmediate());\r
+            updateTime(false);\r
+        } else if (sender == sec) {\r
+            final int s = sec.getSelectedIndex();\r
+            datefield.getCurrentDate().setSeconds(s);\r
+            datefield.getShowingDate().setSeconds(s);\r
+            datefield.getClient().updateVariable(datefield.getId(), "sec", s,\r
+                    datefield.isImmediate());\r
+            updateTime(false);\r
+        } else if (sender == msec) {\r
+            final int ms = msec.getSelectedIndex();\r
+            datefield.setMilliseconds(ms);\r
+            datefield.setShowingMilliseconds(ms);\r
+            datefield.getClient().updateVariable(datefield.getId(), "msec", ms,\r
+                    datefield.isImmediate());\r
+            updateTime(false);\r
+        } else if (sender == ampm) {\r
+            final int h = hours.getSelectedIndex() + ampm.getSelectedIndex()\r
+                    * 12;\r
+            datefield.getCurrentDate().setHours(h);\r
+            datefield.getShowingDate().setHours(h);\r
+            datefield.getClient().updateVariable(datefield.getId(), "hour", h,\r
+                    datefield.isImmediate());\r
+            updateTime(false);\r
+        }\r
+    }\r
+\r
+}\r
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/ToolkitOverlay.java b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/ToolkitOverlay.java
new file mode 100644 (file)
index 0000000..a56cd24
--- /dev/null
@@ -0,0 +1,38 @@
+/* 
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.itmill.toolkit.terminal.gwt.client.ui;
+
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.ui.PopupPanel;
+
+/**
+ * In Toolkit UI this Overlay should always be used for all elements that
+ * temporary float over other components like context menus etc. This is to deal
+ * stacking order correctly with IWindow objects.
+ */
+public class ToolkitOverlay extends PopupPanel {
+
+    public static final int Z_INDEX = 20000;
+
+    public ToolkitOverlay() {
+        super();
+        adjustZIndex();
+    }
+
+    public ToolkitOverlay(boolean autoHide) {
+        super(autoHide);
+        adjustZIndex();
+    }
+
+    public ToolkitOverlay(boolean autoHide, boolean modal) {
+        super(autoHide, modal);
+        adjustZIndex();
+    }
+
+    private void adjustZIndex() {
+        DOM.setStyleAttribute(getElement(), "zIndex", "" + (Z_INDEX));
+    }
+
+}
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/TreeAction.java b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/TreeAction.java
new file mode 100644 (file)
index 0000000..4ecc477
--- /dev/null
@@ -0,0 +1,55 @@
+/* 
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.itmill.toolkit.terminal.gwt.client.ui;
+
+/**
+ * This class is used for "row actions" in ITree and ITable
+ */
+public class TreeAction extends Action {
+
+    String targetKey = "";
+    String actionKey = "";
+
+    public TreeAction(ActionOwner owner) {
+        super(owner);
+    }
+
+    public TreeAction(ActionOwner owner, String target, String action) {
+        this(owner);
+        targetKey = target;
+        actionKey = action;
+    }
+
+    /**
+     * Sends message to server that this action has been fired. Messages are
+     * "standard" Toolkit messages whose value is comma separated pair of
+     * targetKey (row, treeNod ...) and actions id.
+     * 
+     * Variablename is always "action".
+     * 
+     * Actions are always sent immediatedly to server.
+     */
+    public void execute() {
+        owner.getClient().updateVariable(owner.getPaintableId(), "action",
+                targetKey + "," + actionKey, true);
+        owner.getClient().getContextMenu().hide();
+    }
+
+    public String getActionKey() {
+        return actionKey;
+    }
+
+    public void setActionKey(String actionKey) {
+        this.actionKey = actionKey;
+    }
+
+    public String getTargetKey() {
+        return targetKey;
+    }
+
+    public void setTargetKey(String targetKey) {
+        this.targetKey = targetKey;
+    }
+}
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/TreeImages.java b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/TreeImages.java
new file mode 100644 (file)
index 0000000..83e0e4c
--- /dev/null
@@ -0,0 +1,27 @@
+/* \r
+@ITMillApache2LicenseForJavaFiles@\r
+ */\r
+\r
+package com.itmill.toolkit.terminal.gwt.client.ui;\r
+\r
+import com.google.gwt.user.client.ui.AbstractImagePrototype;\r
+\r
+public interface TreeImages extends com.google.gwt.user.client.ui.TreeImages {\r
+\r
+    /**\r
+     * An image indicating an open branch.\r
+     * \r
+     * @return a prototype of this image\r
+     * @gwt.resource com/itmill/toolkit/terminal/gwt/public/default/tree/img/expanded.png\r
+     */\r
+    AbstractImagePrototype treeOpen();\r
+\r
+    /**\r
+     * An image indicating a closed branch.\r
+     * \r
+     * @return a prototype of this image\r
+     * @gwt.resource com/itmill/toolkit/terminal/gwt/public/default/tree/img/collapsed.png\r
+     */\r
+    AbstractImagePrototype treeClosed();\r
+\r
+}\r
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/absolutegrid/AbsoluteGrid.java b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/absolutegrid/AbsoluteGrid.java
new file mode 100644 (file)
index 0000000..a2592fd
--- /dev/null
@@ -0,0 +1,305 @@
+package com.itmill.toolkit.terminal.gwt.client.ui.absolutegrid;
+
+import java.util.HashMap;
+import java.util.Iterator;
+
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.Element;
+import com.google.gwt.user.client.ui.AbsolutePanel;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.SimplePanel;
+import com.google.gwt.user.client.ui.Widget;
+import com.itmill.toolkit.terminal.gwt.client.Caption;
+import com.itmill.toolkit.terminal.gwt.client.ContainerResizedListener;
+import com.itmill.toolkit.terminal.gwt.client.Util;
+import com.itmill.toolkit.terminal.gwt.client.ui.AlignmentInfo;
+
+/**
+ * Prototype helper widget to implement complex sized Toolkit layouts like
+ * GridLayout and OrderedLayout. Supports size, margins, spacing, but has bit
+ * expensive layout function.
+ */
+public class AbsoluteGrid extends Composite implements ContainerResizedListener {
+
+    protected HashMap cells = new HashMap();
+
+    private int cols = 1;
+    private int rows = 1;
+
+    private AbsolutePanel ap;
+
+    protected int marginTop;
+    protected int marginBottom;
+    protected int marginLeft;
+    protected int marginRight;
+
+    private int offsetWidth;
+
+    private int offsetHeight;
+
+    public AbsoluteGrid() {
+        ap = new AbsolutePanel();
+        initWidget(ap);
+    }
+
+    public AbsoluteGridCell getCell(int col, int row) {
+        AbsoluteGridCell p = (AbsoluteGridCell) cells.get(col + "." + row);
+        if (p == null) {
+            p = new AbsoluteGridCell(col, row);
+            cells.put(col + "." + row, p);
+            ap.add(p);
+        }
+        return p;
+    }
+
+    public void clear() {
+        ap.clear();
+        cells.clear();
+    }
+
+    public Iterator getCellIterator() {
+        return cells.values().iterator();
+    }
+
+    private float getCellWidth(int colspan) {
+        int total = ap.getOffsetWidth();
+        total -= getMarginWidth();
+        total -= getSpacingSize() * (cols - colspan);
+        if (total < 0) {
+            return 0;
+        }
+        return total * colspan / (float) cols;
+    }
+
+    /**
+     * 
+     * @return space used by left and right margin
+     */
+    private int getMarginWidth() {
+        return marginLeft + marginRight;
+    }
+
+    /**
+     * @return pixels reserved for space between components
+     */
+    protected int getSpacingSize() {
+        return 0;
+    }
+
+    private float getCellHeight(int rowspan) {
+        int total = ap.getOffsetHeight();
+        total -= getMarginHeight();
+        total -= getSpacingSize() * (rows - rowspan);
+        if (total < 0) {
+            return 0;
+        }
+        return total * rowspan / (float) rows;
+    }
+
+    /**
+     * 
+     * @return space used by top and bottom margin
+     */
+    private int getMarginHeight() {
+        return marginBottom + marginTop;
+    }
+
+    /**
+     * TODO contains Caption (which is a widget) in a very bad way, cannot be
+     * simple panel
+     */
+    public class AbsoluteGridCell extends SimplePanel {
+
+        int rowIndex;
+        int colIndex;
+        int colSpan = 1;
+        int rowSpan = 1;
+        private Element container = DOM.createDiv();
+
+        private Caption caption;
+        private AlignmentInfo alignmentInfo = new AlignmentInfo(
+                AlignmentInfo.ALIGNMENT_TOP + AlignmentInfo.ALIGNMENT_LEFT);
+
+        AbsoluteGridCell(int colIndex, int rowIndex) {
+            super();
+            DOM.appendChild(getElement(), container);
+            this.rowIndex = rowIndex;
+            this.colIndex = colIndex;
+        }
+
+        public void clear() {
+            super.clear();
+            if (caption != null) {
+                DOM.removeChild(getElement(), caption.getElement());
+                caption = null;
+            }
+        }
+
+        protected Element getContainerElement() {
+            return container;
+        }
+
+        void setColSpan(int s) {
+            // TODO Should remove possibly collapsing cells
+            colSpan = s;
+        }
+
+        void setRowSpan(int s) {
+            // TODO Should remove possibly collapsing cells
+            rowSpan = s;
+        }
+
+        private int getLeft() {
+            int left = marginLeft;
+            left += colIndex * getCellWidth(1);
+            left += getSpacingSize() * colIndex;
+            return left;
+        }
+
+        private int getTop() {
+            int top = marginTop;
+            top += rowIndex * getCellHeight(1);
+            top += getSpacingSize() * rowIndex;
+            return top;
+        }
+
+        public void render() {
+            setPixelSize((int) getCellWidth(colSpan),
+                    (int) getCellHeight(rowSpan));
+            ap.setWidgetPosition(this, getLeft(), getTop());
+        }
+
+        /**
+         * Does vertical positioning based on DOM values
+         */
+        public void vAling() {
+            DOM.setStyleAttribute(getElement(), "paddingTop", "0");
+            if (!alignmentInfo.isTop()) {
+                Widget c = getWidget();
+                if (c != null) {
+
+                    int oh = getOffsetHeight();
+                    int wt = DOM.getElementPropertyInt(container, "offsetTop");
+                    int wh = c.getOffsetHeight();
+
+                    int freeSpace = getOffsetHeight()
+                            - (DOM
+                                    .getElementPropertyInt(container,
+                                            "offsetTop") + c.getOffsetHeight());
+                    if (Util.isIE()) {
+                        freeSpace -= DOM.getElementPropertyInt(c.getElement(),
+                                "offsetTop");
+                    }
+                    if (freeSpace < 0) {
+                        freeSpace = 0; // clipping rest of contents when object
+                        // larger than reserved area
+                    }
+                    if (alignmentInfo.isVerticalCenter()) {
+                        DOM.setStyleAttribute(getElement(), "paddingTop",
+                                (freeSpace / 2) + "px");
+                    } else {
+                        DOM.setStyleAttribute(getElement(), "paddingTop",
+                                (freeSpace) + "px");
+                    }
+                }
+            }
+        }
+
+        public void setPixelSize(int width, int height) {
+            super.setPixelSize(width, height);
+            DOM.setStyleAttribute(container, "width", width + "px");
+            int contHeight = height - getCaptionHeight();
+            if (contHeight < 0) {
+                contHeight = 0;
+            }
+            DOM.setStyleAttribute(container, "height", contHeight + "px");
+        }
+
+        private int getCaptionHeight() {
+            // remove hard coded caption height
+            return (caption == null) ? 0 : caption.getOffsetHeight();
+        }
+
+        public Caption getCaption() {
+            return caption;
+        }
+
+        public void setCaption(Caption newCaption) {
+            // TODO check for existing, shouldn't happen though
+            caption = newCaption;
+            DOM.insertChild(getElement(), caption.getElement(), 0);
+        }
+
+        public void setAlignment(int bitmask) {
+            if (alignmentInfo.getBitMask() != bitmask) {
+                alignmentInfo = new AlignmentInfo(bitmask);
+                setHorizontalAling();
+                // vertical align is set in render() method
+            }
+        }
+
+        private void setHorizontalAling() {
+            DOM.setStyleAttribute(getElement(), "textAlign", alignmentInfo
+                    .getHorizontalAlignment());
+            if (getWidget() != null) {
+                Element el = getWidget().getElement();
+                if (alignmentInfo.isHorizontalCenter()
+                        || alignmentInfo.isRight()) {
+                    DOM.setStyleAttribute(el, "marginLeft", "auto");
+                } else {
+                    DOM.setStyleAttribute(el, "marginLeft", "");
+                }
+                if (alignmentInfo.isHorizontalCenter()
+                        || alignmentInfo.isLeft()) {
+                    DOM.setStyleAttribute(el, "marginRight", "auto");
+                } else {
+                    DOM.setStyleAttribute(el, "marginRight", "");
+                }
+            }
+        }
+    }
+
+    public void iLayout() {
+        boolean sizeChanged = false;
+        int newWidth = getOffsetWidth();
+        if (offsetWidth != newWidth) {
+            offsetWidth = newWidth;
+            sizeChanged = true;
+        }
+        int newHeight = getOffsetHeight();
+        if (offsetHeight != newHeight) {
+            offsetHeight = newHeight;
+            sizeChanged = true;
+        }
+        if (sizeChanged) {
+            for (Iterator it = cells.values().iterator(); it.hasNext();) {
+                AbsoluteGridCell cell = (AbsoluteGridCell) it.next();
+                cell.render();
+                cell.vAling();
+            }
+            Util.runDescendentsLayout(ap);
+        }
+    }
+
+    public int getCols() {
+        return cols;
+    }
+
+    public void setCols(int cols) {
+        this.cols = cols;
+        // force relayout
+        offsetHeight = 0;
+        offsetWidth = 0;
+    }
+
+    public int getRows() {
+        return rows;
+    }
+
+    public void setRows(int rows) {
+        this.rows = rows;
+        // force relayout
+        offsetHeight = 0;
+        offsetWidth = 0;
+    }
+}
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/absolutegrid/ISizeableGridLayout.java b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/absolutegrid/ISizeableGridLayout.java
new file mode 100644 (file)
index 0000000..f149fea
--- /dev/null
@@ -0,0 +1,166 @@
+package com.itmill.toolkit.terminal.gwt.client.ui.absolutegrid;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+
+import com.google.gwt.user.client.ui.Widget;
+import com.itmill.toolkit.terminal.gwt.client.ApplicationConnection;
+import com.itmill.toolkit.terminal.gwt.client.Caption;
+import com.itmill.toolkit.terminal.gwt.client.Container;
+import com.itmill.toolkit.terminal.gwt.client.Paintable;
+import com.itmill.toolkit.terminal.gwt.client.UIDL;
+import com.itmill.toolkit.terminal.gwt.client.ui.MarginInfo;
+
+/**
+ * Proto level implementation of GridLayout.
+ * 
+ * All cell's will be equally sized.
+ * 
+ */
+public class ISizeableGridLayout extends AbsoluteGrid implements Paintable,
+        Container {
+    public static final String CLASSNAME = "i-gridlayout";
+    private int spacing;
+    private HashMap paintableToCellMap = new HashMap();
+    private ApplicationConnection client;
+
+    public ISizeableGridLayout() {
+        super();
+        setStyleName(CLASSNAME);
+    }
+
+    protected int getSpacingSize() {
+        return spacing;
+    }
+
+    public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+        this.client = client;
+
+        if (client.updateComponent(this, uidl, false)) {
+            return;
+        }
+
+        if (uidl.hasAttribute("caption")) {
+            setTitle(uidl.getStringAttribute("caption"));
+        }
+        int row = 0, column = 0;
+
+        final ArrayList oldCells = new ArrayList();
+        for (final Iterator iterator = getCellIterator(); iterator.hasNext();) {
+            oldCells.add(iterator.next());
+        }
+        clear();
+
+        setCols(uidl.getIntAttribute("w"));
+        setRows(uidl.getIntAttribute("h"));
+
+        handleMargins(uidl);
+        spacing = uidl.getBooleanAttribute("spacing") ? detectSpacingSize() : 0;
+
+        final int[] alignments = uidl.getIntArrayAttribute("alignments");
+        int alignmentIndex = 0;
+
+        for (final Iterator i = uidl.getChildIterator(); i.hasNext();) {
+            final UIDL r = (UIDL) i.next();
+            if ("gr".equals(r.getTag())) {
+                column = 0;
+                for (final Iterator j = r.getChildIterator(); j.hasNext();) {
+                    final UIDL c = (UIDL) j.next();
+                    if ("gc".equals(c.getTag())) {
+
+                        // Set cell width
+                        int colSpan;
+                        if (c.hasAttribute("w")) {
+                            colSpan = c.getIntAttribute("w");
+                        } else {
+                            colSpan = 1;
+                        }
+
+                        // Set cell height
+                        int rowSpan;
+                        if (c.hasAttribute("h")) {
+                            rowSpan = c.getIntAttribute("h");
+                        } else {
+                            rowSpan = 1;
+                        }
+
+                        final UIDL u = c.getChildUIDL(0);
+                        if (u != null) {
+                            final Paintable child = client.getPaintable(u);
+                            AbsoluteGridCell cell = getCell(column, row);
+                            paintableToCellMap.put(child, cell);
+                            cell.rowSpan = rowSpan;
+                            cell.colSpan = colSpan;
+
+                            oldCells.remove(cell);
+
+                            cell.setAlignment(alignments[alignmentIndex++]);
+
+                            cell.render();
+
+                            cell.setWidget((Widget) child);
+
+                            if (!u.getBooleanAttribute("cached")) {
+                                child.updateFromUIDL(u, client);
+                            }
+
+                            cell.vAling();
+                        }
+                        column += colSpan;
+                    }
+                }
+                row++;
+            }
+        }
+
+        // loop oldWidgetWrappers that where not re-attached and unregister them
+        for (final Iterator it = oldCells.iterator(); it.hasNext();) {
+            final AbsoluteGridCell w = (AbsoluteGridCell) it.next();
+            client.unregisterPaintable((Paintable) w.getWidget());
+            w.removeFromParent();
+            paintableToCellMap.remove(w.getWidget());
+        }
+
+    }
+
+    protected void handleMargins(UIDL uidl) {
+        final MarginInfo margins = new MarginInfo(uidl
+                .getIntAttribute("margins"));
+        // TODO build CSS detector to make margins configurable through css
+        marginTop = margins.hasTop() ? 15 : 0;
+        marginRight = margins.hasRight() ? 15 : 0;
+        marginBottom = margins.hasBottom() ? 15 : 0;
+        marginLeft = margins.hasLeft() ? 15 : 0;
+    }
+
+    private int detectSpacingSize() {
+        // TODO Auto-generated method stub
+        return 15;
+    }
+
+    public boolean hasChildComponent(Widget component) {
+        if (paintableToCellMap.containsKey(component)) {
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    public void replaceChildComponent(Widget oldComponent, Widget newComponent) {
+        // TODO Auto-generated method stub
+
+    }
+
+    public void updateCaption(Paintable component, UIDL uidl) {
+        AbsoluteGridCell cell = (AbsoluteGridCell) paintableToCellMap
+                .get(component);
+        Caption c = cell.getCaption();
+        if (c == null) {
+            c = new Caption(component, client);
+            cell.setCaption(c);
+        }
+        c.updateCaption(uidl);
+    }
+
+}
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/absolutegrid/ISizeableOrderedLayout.java b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/absolutegrid/ISizeableOrderedLayout.java
new file mode 100644 (file)
index 0000000..3c5b81f
--- /dev/null
@@ -0,0 +1,192 @@
+package com.itmill.toolkit.terminal.gwt.client.ui.absolutegrid;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+
+import com.google.gwt.user.client.ui.Widget;
+import com.itmill.toolkit.terminal.gwt.client.ApplicationConnection;
+import com.itmill.toolkit.terminal.gwt.client.Caption;
+import com.itmill.toolkit.terminal.gwt.client.Container;
+import com.itmill.toolkit.terminal.gwt.client.Paintable;
+import com.itmill.toolkit.terminal.gwt.client.UIDL;
+import com.itmill.toolkit.terminal.gwt.client.ui.MarginInfo;
+
+/**
+ * Proto level implementation of GridLayout.
+ * 
+ * All cell's will be equally sized.
+ * 
+ */
+public class ISizeableOrderedLayout extends AbsoluteGrid implements Paintable,
+        Container {
+    public static final String CLASSNAME = "i-orderedlayout";
+    private static final int ORIENTETION_HORIZONTAL = 1;
+    private int spacing;
+    private HashMap paintableToCellMap = new HashMap();
+    private ApplicationConnection client;
+    private int orientation;
+
+    public ISizeableOrderedLayout() {
+        super();
+        setStyleName(CLASSNAME);
+    }
+
+    protected int getSpacingSize() {
+        return spacing;
+    }
+
+    public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+        this.client = client;
+
+        if (client.updateComponent(this, uidl, false)) {
+            return;
+        }
+
+        orientation = (uidl.hasAttribute("orientation") ? ORIENTETION_HORIZONTAL
+                : 0);
+
+        if (uidl.hasAttribute("caption")) {
+            setTitle(uidl.getStringAttribute("caption"));
+        }
+
+        handleMargins(uidl);
+        spacing = uidl.getBooleanAttribute("spacing") ? detectSpacingSize() : 0;
+
+        final int[] alignments = uidl.getIntArrayAttribute("alignments");
+        int alignmentIndex = 0;
+
+        // Update contained components
+
+        final ArrayList uidlWidgets = new ArrayList();
+        for (final Iterator it = uidl.getChildIterator(); it.hasNext();) {
+            final UIDL uidlForChild = (UIDL) it.next();
+            final Paintable child = client.getPaintable(uidlForChild);
+            uidlWidgets.add(child);
+        }
+
+        if (orientation == ORIENTETION_HORIZONTAL) {
+            setCols(uidlWidgets.size());
+            setRows(1);
+        } else {
+            setCols(1);
+            setRows(uidlWidgets.size());
+        }
+
+        final ArrayList oldWidgets = getPaintables();
+
+        final HashMap oldCaptions = new HashMap();
+
+        final Iterator newIt = uidlWidgets.iterator();
+        final Iterator newUidl = uidl.getChildIterator();
+
+        int row = 0, column = 0;
+        while (newIt.hasNext()) {
+            final Widget child = (Widget) newIt.next();
+            final UIDL childUidl = (UIDL) newUidl.next();
+
+            AbsoluteGridCell cell = getCell(column, row);
+
+            Widget oldChild = cell.getWidget();
+            if (oldChild != null) {
+                if (oldChild != child) {
+                    oldCaptions.put(oldChild, cell.getCaption());
+                    cell.clear();
+                    cell.setWidget(child);
+                    paintableToCellMap.remove(oldChild);
+                    Caption newCaption = (Caption) oldCaptions.get(child);
+                    if (newCaption == null) {
+                        newCaption = new Caption((Paintable) child, client);
+                    }
+                    cell.setCaption(newCaption);
+                }
+            } else {
+                cell.setWidget(child);
+            }
+
+            paintableToCellMap.put(child, cell);
+
+            cell.setAlignment(alignments[alignmentIndex++]);
+
+            cell.render();
+
+            ((Paintable) child).updateFromUIDL(childUidl, client);
+
+            cell.vAling();
+
+            if (orientation == ORIENTETION_HORIZONTAL) {
+                column++;
+            } else {
+                row++;
+            }
+            oldWidgets.remove(child);
+        }
+        // remove possibly remaining old Paintable object which were not updated
+        Iterator oldIt = oldWidgets.iterator();
+        while (oldIt.hasNext()) {
+            final Paintable p = (Paintable) oldIt.next();
+            if (!uidlWidgets.contains(p)) {
+                removePaintable(p);
+            }
+        }
+    }
+
+    private void removePaintable(Paintable oldChild) {
+        AbsoluteGridCell cell = (AbsoluteGridCell) paintableToCellMap
+                .get(oldChild);
+        if (cell != null) {
+            cell.clear();
+        }
+        client.unregisterPaintable(oldChild);
+    }
+
+    private ArrayList getPaintables() {
+        ArrayList paintables = new ArrayList();
+        Iterator it = paintableToCellMap.keySet().iterator();
+        while (it.hasNext()) {
+            Paintable p = (Paintable) it.next();
+            paintables.add(p);
+        }
+        return paintables;
+    }
+
+    protected void handleMargins(UIDL uidl) {
+        final MarginInfo margins = new MarginInfo(uidl
+                .getIntAttribute("margins"));
+        // TODO build CSS detector to make margins configurable through css
+        marginTop = margins.hasTop() ? 15 : 0;
+        marginRight = margins.hasRight() ? 15 : 0;
+        marginBottom = margins.hasBottom() ? 15 : 0;
+        marginLeft = margins.hasLeft() ? 15 : 0;
+    }
+
+    private int detectSpacingSize() {
+        // TODO Auto-generated method stub
+        return 15;
+    }
+
+    public boolean hasChildComponent(Widget component) {
+        if (paintableToCellMap.containsKey(component)) {
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    public void replaceChildComponent(Widget oldComponent, Widget newComponent) {
+        // TODO Auto-generated method stub
+
+    }
+
+    public void updateCaption(Paintable component, UIDL uidl) {
+        AbsoluteGridCell cell = (AbsoluteGridCell) paintableToCellMap
+                .get(component);
+        Caption c = cell.getCaption();
+        if (c == null) {
+            c = new Caption(component, client);
+            cell.setCaption(c);
+        }
+        c.updateCaption(uidl);
+    }
+
+}
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/richtextarea/IRichTextArea.java b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/richtextarea/IRichTextArea.java
new file mode 100644 (file)
index 0000000..f0ea0c0
--- /dev/null
@@ -0,0 +1,111 @@
+/* 
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.itmill.toolkit.terminal.gwt.client.ui.richtextarea;
+
+import com.google.gwt.user.client.ui.ChangeListener;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.FocusListener;
+import com.google.gwt.user.client.ui.HTML;
+import com.google.gwt.user.client.ui.RichTextArea;
+import com.google.gwt.user.client.ui.Widget;
+import com.itmill.toolkit.terminal.gwt.client.ApplicationConnection;
+import com.itmill.toolkit.terminal.gwt.client.Paintable;
+import com.itmill.toolkit.terminal.gwt.client.UIDL;
+import com.itmill.toolkit.terminal.gwt.client.ui.Field;
+
+/**
+ * This class represents a basic text input field with one row.
+ * 
+ * @author IT Mill Ltd.
+ * 
+ */
+public class IRichTextArea extends Composite implements Paintable, Field,
+        ChangeListener, FocusListener {
+
+    /**
+     * The input node CSS classname.
+     */
+    public static final String CLASSNAME = "i-richtextarea";
+
+    protected String id;
+
+    protected ApplicationConnection client;
+
+    private boolean immediate = false;
+
+    private RichTextArea rta = new RichTextArea();
+
+    private RichTextToolbar formatter = new RichTextToolbar(rta);
+
+    private HTML html = new HTML();
+
+    private final FlowPanel fp = new FlowPanel();
+
+    private boolean enabled = true;
+
+    public IRichTextArea() {
+        fp.add(formatter);
+
+        rta.setWidth("100%");
+        rta.addFocusListener(this);
+
+        fp.add(rta);
+
+        initWidget(fp);
+        setStyleName(CLASSNAME);
+
+    }
+
+    public void setEnabled(boolean enabled) {
+        if (this.enabled != enabled) {
+            rta.setEnabled(enabled);
+            if (enabled) {
+                fp.remove(html);
+                fp.add(rta);
+            } else {
+                html.setHTML(rta.getHTML());
+                fp.remove(rta);
+                fp.add(html);
+            }
+
+            this.enabled = enabled;
+        }
+    }
+
+    public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+        this.client = client;
+        id = uidl.getId();
+
+        if (uidl.hasVariable("text")) {
+            rta.setHTML(uidl.getStringVariable("text"));
+        }
+        setEnabled(!uidl.getBooleanAttribute("disabled"));
+
+        if (client.updateComponent(this, uidl, true)) {
+            return;
+        }
+
+        immediate = uidl.getBooleanAttribute("immediate");
+
+    }
+
+    public void onChange(Widget sender) {
+        if (client != null && id != null) {
+            client.updateVariable(id, "text", rta.getText(), immediate);
+        }
+    }
+
+    public void onFocus(Widget sender) {
+
+    }
+
+    public void onLostFocus(Widget sender) {
+        final String html = rta.getHTML();
+        client.updateVariable(id, "text", html, immediate);
+
+    }
+
+}
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/richtextarea/RichTextToolbar$Strings.properties b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/richtextarea/RichTextToolbar$Strings.properties
new file mode 100644 (file)
index 0000000..363b704
--- /dev/null
@@ -0,0 +1,35 @@
+bold = Toggle Bold
+createLink = Create Link
+hr = Insert Horizontal Rule
+indent = Indent Right
+insertImage = Insert Image
+italic = Toggle Italic
+justifyCenter = Center
+justifyLeft = Left Justify
+justifyRight = Right Justify
+ol = Insert Ordered List
+outdent = Indent Left
+removeFormat = Remove Formatting
+removeLink = Remove Link
+strikeThrough = Toggle Strikethrough
+subscript = Toggle Subscript
+superscript = Toggle Superscript
+ul = Insert Unordered List
+underline = Toggle Underline
+color = Color
+black = Black
+white = White
+red = Red
+green = Green
+yellow = Yellow
+blue = Blue
+font = Font
+normal = Normal
+size = Size
+xxsmall = XX-Small
+xsmall = X-Small
+small = Small
+medium = Medium
+large = Large
+xlarge = X-Large
+xxlarge = XX-Large
\ No newline at end of file
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/richtextarea/RichTextToolbar.java b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/richtextarea/RichTextToolbar.java
new file mode 100644 (file)
index 0000000..4d3d11d
--- /dev/null
@@ -0,0 +1,509 @@
+/*
+ * Copyright 2007 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.itmill.toolkit.terminal.gwt.client.ui.richtextarea;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.i18n.client.Constants;
+import com.google.gwt.user.client.Window;
+import com.google.gwt.user.client.ui.AbstractImagePrototype;
+import com.google.gwt.user.client.ui.ChangeListener;
+import com.google.gwt.user.client.ui.ClickListener;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.HorizontalPanel;
+import com.google.gwt.user.client.ui.ImageBundle;
+import com.google.gwt.user.client.ui.KeyboardListener;
+import com.google.gwt.user.client.ui.ListBox;
+import com.google.gwt.user.client.ui.PushButton;
+import com.google.gwt.user.client.ui.RichTextArea;
+import com.google.gwt.user.client.ui.ToggleButton;
+import com.google.gwt.user.client.ui.VerticalPanel;
+import com.google.gwt.user.client.ui.Widget;
+
+/**
+ * A sample toolbar for use with {@link RichTextArea}. It provides a simple UI
+ * for all rich text formatting, dynamically displayed only for the available
+ * functionality.
+ */
+public class RichTextToolbar extends Composite {
+
+    /**
+     * This {@link ImageBundle} is used for all the button icons. Using an image
+     * bundle allows all of these images to be packed into a single image, which
+     * saves a lot of HTTP requests, drastically improving startup time.
+     */
+    public interface Images extends ImageBundle {
+
+        /**
+         * @gwt.resource bold.gif
+         */
+        AbstractImagePrototype bold();
+
+        /**
+         * @gwt.resource createLink.gif
+         */
+        AbstractImagePrototype createLink();
+
+        /**
+         * @gwt.resource hr.gif
+         */
+        AbstractImagePrototype hr();
+
+        /**
+         * @gwt.resource indent.gif
+         */
+        AbstractImagePrototype indent();
+
+        /**
+         * @gwt.resource insertImage.gif
+         */
+        AbstractImagePrototype insertImage();
+
+        /**
+         * @gwt.resource italic.gif
+         */
+        AbstractImagePrototype italic();
+
+        /**
+         * @gwt.resource justifyCenter.gif
+         */
+        AbstractImagePrototype justifyCenter();
+
+        /**
+         * @gwt.resource justifyLeft.gif
+         */
+        AbstractImagePrototype justifyLeft();
+
+        /**
+         * @gwt.resource justifyRight.gif
+         */
+        AbstractImagePrototype justifyRight();
+
+        /**
+         * @gwt.resource ol.gif
+         */
+        AbstractImagePrototype ol();
+
+        /**
+         * @gwt.resource outdent.gif
+         */
+        AbstractImagePrototype outdent();
+
+        /**
+         * @gwt.resource removeFormat.gif
+         */
+        AbstractImagePrototype removeFormat();
+
+        /**
+         * @gwt.resource removeLink.gif
+         */
+        AbstractImagePrototype removeLink();
+
+        /**
+         * @gwt.resource strikeThrough.gif
+         */
+        AbstractImagePrototype strikeThrough();
+
+        /**
+         * @gwt.resource subscript.gif
+         */
+        AbstractImagePrototype subscript();
+
+        /**
+         * @gwt.resource superscript.gif
+         */
+        AbstractImagePrototype superscript();
+
+        /**
+         * @gwt.resource ul.gif
+         */
+        AbstractImagePrototype ul();
+
+        /**
+         * @gwt.resource underline.gif
+         */
+        AbstractImagePrototype underline();
+    }
+
+    /**
+     * This {@link Constants} interface is used to make the toolbar's strings
+     * internationalizable.
+     */
+    public interface Strings extends Constants {
+
+        String black();
+
+        String blue();
+
+        String bold();
+
+        String color();
+
+        String createLink();
+
+        String font();
+
+        String green();
+
+        String hr();
+
+        String indent();
+
+        String insertImage();
+
+        String italic();
+
+        String justifyCenter();
+
+        String justifyLeft();
+
+        String justifyRight();
+
+        String large();
+
+        String medium();
+
+        String normal();
+
+        String ol();
+
+        String outdent();
+
+        String red();
+
+        String removeFormat();
+
+        String removeLink();
+
+        String size();
+
+        String small();
+
+        String strikeThrough();
+
+        String subscript();
+
+        String superscript();
+
+        String ul();
+
+        String underline();
+
+        String white();
+
+        String xlarge();
+
+        String xsmall();
+
+        String xxlarge();
+
+        String xxsmall();
+
+        String yellow();
+    }
+
+    /**
+     * We use an inner EventListener class to avoid exposing event methods on
+     * the RichTextToolbar itself.
+     */
+    private class EventListener implements ClickListener, ChangeListener,
+            KeyboardListener {
+
+        public void onChange(Widget sender) {
+            if (sender == backColors) {
+                basic.setBackColor(backColors.getValue(backColors
+                        .getSelectedIndex()));
+                backColors.setSelectedIndex(0);
+            } else if (sender == foreColors) {
+                basic.setForeColor(foreColors.getValue(foreColors
+                        .getSelectedIndex()));
+                foreColors.setSelectedIndex(0);
+            } else if (sender == fonts) {
+                basic.setFontName(fonts.getValue(fonts.getSelectedIndex()));
+                fonts.setSelectedIndex(0);
+            } else if (sender == fontSizes) {
+                basic.setFontSize(fontSizesConstants[fontSizes
+                        .getSelectedIndex() - 1]);
+                fontSizes.setSelectedIndex(0);
+            }
+        }
+
+        public void onClick(Widget sender) {
+            if (sender == bold) {
+                basic.toggleBold();
+            } else if (sender == italic) {
+                basic.toggleItalic();
+            } else if (sender == underline) {
+                basic.toggleUnderline();
+            } else if (sender == subscript) {
+                basic.toggleSubscript();
+            } else if (sender == superscript) {
+                basic.toggleSuperscript();
+            } else if (sender == strikethrough) {
+                extended.toggleStrikethrough();
+            } else if (sender == indent) {
+                extended.rightIndent();
+            } else if (sender == outdent) {
+                extended.leftIndent();
+            } else if (sender == justifyLeft) {
+                basic.setJustification(RichTextArea.Justification.LEFT);
+            } else if (sender == justifyCenter) {
+                basic.setJustification(RichTextArea.Justification.CENTER);
+            } else if (sender == justifyRight) {
+                basic.setJustification(RichTextArea.Justification.RIGHT);
+            } else if (sender == insertImage) {
+                final String url = Window.prompt("Enter an image URL:",
+                        "http://");
+                if (url != null) {
+                    extended.insertImage(url);
+                }
+            } else if (sender == createLink) {
+                final String url = Window
+                        .prompt("Enter a link URL:", "http://");
+                if (url != null) {
+                    extended.createLink(url);
+                }
+            } else if (sender == removeLink) {
+                extended.removeLink();
+            } else if (sender == hr) {
+                extended.insertHorizontalRule();
+            } else if (sender == ol) {
+                extended.insertOrderedList();
+            } else if (sender == ul) {
+                extended.insertUnorderedList();
+            } else if (sender == removeFormat) {
+                extended.removeFormat();
+            } else if (sender == richText) {
+                // We use the RichTextArea's onKeyUp event to update the toolbar
+                // status.
+                // This will catch any cases where the user moves the cursur
+                // using the
+                // keyboard, or uses one of the browser's built-in keyboard
+                // shortcuts.
+                updateStatus();
+            }
+        }
+
+        public void onKeyDown(Widget sender, char keyCode, int modifiers) {
+        }
+
+        public void onKeyPress(Widget sender, char keyCode, int modifiers) {
+        }
+
+        public void onKeyUp(Widget sender, char keyCode, int modifiers) {
+            if (sender == richText) {
+                // We use the RichTextArea's onKeyUp event to update the toolbar
+                // status.
+                // This will catch any cases where the user moves the cursur
+                // using the
+                // keyboard, or uses one of the browser's built-in keyboard
+                // shortcuts.
+                updateStatus();
+            }
+        }
+    }
+
+    private static final RichTextArea.FontSize[] fontSizesConstants = new RichTextArea.FontSize[] {
+            RichTextArea.FontSize.XX_SMALL, RichTextArea.FontSize.X_SMALL,
+            RichTextArea.FontSize.SMALL, RichTextArea.FontSize.MEDIUM,
+            RichTextArea.FontSize.LARGE, RichTextArea.FontSize.X_LARGE,
+            RichTextArea.FontSize.XX_LARGE };
+
+    private final Images images = (Images) GWT.create(Images.class);
+    private final Strings strings = (Strings) GWT.create(Strings.class);
+    private final EventListener listener = new EventListener();
+
+    private final RichTextArea richText;
+    private final RichTextArea.BasicFormatter basic;
+    private final RichTextArea.ExtendedFormatter extended;
+
+    private final VerticalPanel outer = new VerticalPanel();
+    private final HorizontalPanel topPanel = new HorizontalPanel();
+    private final HorizontalPanel bottomPanel = new HorizontalPanel();
+    private ToggleButton bold;
+    private ToggleButton italic;
+    private ToggleButton underline;
+    private ToggleButton subscript;
+    private ToggleButton superscript;
+    private ToggleButton strikethrough;
+    private PushButton indent;
+    private PushButton outdent;
+    private PushButton justifyLeft;
+    private PushButton justifyCenter;
+    private PushButton justifyRight;
+    private PushButton hr;
+    private PushButton ol;
+    private PushButton ul;
+    private PushButton insertImage;
+    private PushButton createLink;
+    private PushButton removeLink;
+    private PushButton removeFormat;
+
+    private ListBox backColors;
+    private ListBox foreColors;
+    private ListBox fonts;
+    private ListBox fontSizes;
+
+    /**
+     * Creates a new toolbar that drives the given rich text area.
+     * 
+     * @param richText
+     *                the rich text area to be controlled
+     */
+    public RichTextToolbar(RichTextArea richText) {
+        this.richText = richText;
+        basic = richText.getBasicFormatter();
+        extended = richText.getExtendedFormatter();
+
+        outer.add(topPanel);
+        outer.add(bottomPanel);
+        topPanel.setWidth("100%");
+        bottomPanel.setWidth("100%");
+
+        initWidget(outer);
+        setStyleName("gwt-RichTextToolbar");
+
+        if (basic != null) {
+            topPanel.add(bold = createToggleButton(images.bold(), strings
+                    .bold()));
+            topPanel.add(italic = createToggleButton(images.italic(), strings
+                    .italic()));
+            topPanel.add(underline = createToggleButton(images.underline(),
+                    strings.underline()));
+            topPanel.add(subscript = createToggleButton(images.subscript(),
+                    strings.subscript()));
+            topPanel.add(superscript = createToggleButton(images.superscript(),
+                    strings.superscript()));
+            topPanel.add(justifyLeft = createPushButton(images.justifyLeft(),
+                    strings.justifyLeft()));
+            topPanel.add(justifyCenter = createPushButton(images
+                    .justifyCenter(), strings.justifyCenter()));
+            topPanel.add(justifyRight = createPushButton(images.justifyRight(),
+                    strings.justifyRight()));
+        }
+
+        if (extended != null) {
+            topPanel.add(strikethrough = createToggleButton(images
+                    .strikeThrough(), strings.strikeThrough()));
+            topPanel.add(indent = createPushButton(images.indent(), strings
+                    .indent()));
+            topPanel.add(outdent = createPushButton(images.outdent(), strings
+                    .outdent()));
+            topPanel.add(hr = createPushButton(images.hr(), strings.hr()));
+            topPanel.add(ol = createPushButton(images.ol(), strings.ol()));
+            topPanel.add(ul = createPushButton(images.ul(), strings.ul()));
+            topPanel.add(insertImage = createPushButton(images.insertImage(),
+                    strings.insertImage()));
+            topPanel.add(createLink = createPushButton(images.createLink(),
+                    strings.createLink()));
+            topPanel.add(removeLink = createPushButton(images.removeLink(),
+                    strings.removeLink()));
+            topPanel.add(removeFormat = createPushButton(images.removeFormat(),
+                    strings.removeFormat()));
+        }
+
+        if (basic != null) {
+            bottomPanel.add(backColors = createColorList("Background"));
+            bottomPanel.add(foreColors = createColorList("Foreground"));
+            bottomPanel.add(fonts = createFontList());
+            bottomPanel.add(fontSizes = createFontSizes());
+
+            // We only use these listeners for updating status, so don't hook
+            // them up
+            // unless at least basic editing is supported.
+            richText.addKeyboardListener(listener);
+            richText.addClickListener(listener);
+        }
+    }
+
+    private ListBox createColorList(String caption) {
+        final ListBox lb = new ListBox();
+        lb.addChangeListener(listener);
+        lb.setVisibleItemCount(1);
+
+        lb.addItem(caption);
+        lb.addItem(strings.white(), "white");
+        lb.addItem(strings.black(), "black");
+        lb.addItem(strings.red(), "red");
+        lb.addItem(strings.green(), "green");
+        lb.addItem(strings.yellow(), "yellow");
+        lb.addItem(strings.blue(), "blue");
+        return lb;
+    }
+
+    private ListBox createFontList() {
+        final ListBox lb = new ListBox();
+        lb.addChangeListener(listener);
+        lb.setVisibleItemCount(1);
+
+        lb.addItem(strings.font(), "");
+        lb.addItem(strings.normal(), "");
+        lb.addItem("Times New Roman", "Times New Roman");
+        lb.addItem("Arial", "Arial");
+        lb.addItem("Courier New", "Courier New");
+        lb.addItem("Georgia", "Georgia");
+        lb.addItem("Trebuchet", "Trebuchet");
+        lb.addItem("Verdana", "Verdana");
+        return lb;
+    }
+
+    private ListBox createFontSizes() {
+        final ListBox lb = new ListBox();
+        lb.addChangeListener(listener);
+        lb.setVisibleItemCount(1);
+
+        lb.addItem(strings.size());
+        lb.addItem(strings.xxsmall());
+        lb.addItem(strings.xsmall());
+        lb.addItem(strings.small());
+        lb.addItem(strings.medium());
+        lb.addItem(strings.large());
+        lb.addItem(strings.xlarge());
+        lb.addItem(strings.xxlarge());
+        return lb;
+    }
+
+    private PushButton createPushButton(AbstractImagePrototype img, String tip) {
+        final PushButton pb = new PushButton(img.createImage());
+        pb.addClickListener(listener);
+        pb.setTitle(tip);
+        return pb;
+    }
+
+    private ToggleButton createToggleButton(AbstractImagePrototype img,
+            String tip) {
+        final ToggleButton tb = new ToggleButton(img.createImage());
+        tb.addClickListener(listener);
+        tb.setTitle(tip);
+        return tb;
+    }
+
+    /**
+     * Updates the status of all the stateful buttons.
+     */
+    private void updateStatus() {
+        if (basic != null) {
+            bold.setDown(basic.isBold());
+            italic.setDown(basic.isItalic());
+            underline.setDown(basic.isUnderlined());
+            subscript.setDown(basic.isSubscript());
+            superscript.setDown(basic.isSuperscript());
+        }
+
+        if (extended != null) {
+            strikethrough.setDown(extended.isStrikethrough());
+        }
+    }
+}
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/richtextarea/backColors.gif b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/richtextarea/backColors.gif
new file mode 100644 (file)
index 0000000..ddfc1ce
Binary files /dev/null and b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/richtextarea/backColors.gif differ
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/richtextarea/bold.gif b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/richtextarea/bold.gif
new file mode 100644 (file)
index 0000000..249e5af
Binary files /dev/null and b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/richtextarea/bold.gif differ
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/richtextarea/createLink.gif b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/richtextarea/createLink.gif
new file mode 100644 (file)
index 0000000..3ab9e59
Binary files /dev/null and b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/richtextarea/createLink.gif differ
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/richtextarea/fontSizes.gif b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/richtextarea/fontSizes.gif
new file mode 100644 (file)
index 0000000..c2f4c8c
Binary files /dev/null and b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/richtextarea/fontSizes.gif differ
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/richtextarea/fonts.gif b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/richtextarea/fonts.gif
new file mode 100644 (file)
index 0000000..1629cab
Binary files /dev/null and b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/richtextarea/fonts.gif differ
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/richtextarea/foreColors.gif b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/richtextarea/foreColors.gif
new file mode 100644 (file)
index 0000000..2bb89ef
Binary files /dev/null and b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/richtextarea/foreColors.gif differ
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/richtextarea/gwtLogo.png b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/richtextarea/gwtLogo.png
new file mode 100644 (file)
index 0000000..8072818
Binary files /dev/null and b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/richtextarea/gwtLogo.png differ
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/richtextarea/hr.gif b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/richtextarea/hr.gif
new file mode 100644 (file)
index 0000000..3fb1607
Binary files /dev/null and b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/richtextarea/hr.gif differ
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/richtextarea/indent.gif b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/richtextarea/indent.gif
new file mode 100644 (file)
index 0000000..8b837f0
Binary files /dev/null and b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/richtextarea/indent.gif differ
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/richtextarea/insertImage.gif b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/richtextarea/insertImage.gif
new file mode 100644 (file)
index 0000000..db61c9a
Binary files /dev/null and b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/richtextarea/insertImage.gif differ
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/richtextarea/italic.gif b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/richtextarea/italic.gif
new file mode 100644 (file)
index 0000000..2b0a5a0
Binary files /dev/null and b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/richtextarea/italic.gif differ
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/richtextarea/justifyCenter.gif b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/richtextarea/justifyCenter.gif
new file mode 100644 (file)
index 0000000..7d22640
Binary files /dev/null and b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/richtextarea/justifyCenter.gif differ
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/richtextarea/justifyLeft.gif b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/richtextarea/justifyLeft.gif
new file mode 100644 (file)
index 0000000..3c0f350
Binary files /dev/null and b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/richtextarea/justifyLeft.gif differ
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/richtextarea/justifyRight.gif b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/richtextarea/justifyRight.gif
new file mode 100644 (file)
index 0000000..99ee258
Binary files /dev/null and b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/richtextarea/justifyRight.gif differ
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/richtextarea/ol.gif b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/richtextarea/ol.gif
new file mode 100644 (file)
index 0000000..833bb40
Binary files /dev/null and b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/richtextarea/ol.gif differ
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/richtextarea/outdent.gif b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/richtextarea/outdent.gif
new file mode 100644 (file)
index 0000000..be86624
Binary files /dev/null and b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/richtextarea/outdent.gif differ
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/richtextarea/removeFormat.gif b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/richtextarea/removeFormat.gif
new file mode 100644 (file)
index 0000000..a4339c0
Binary files /dev/null and b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/richtextarea/removeFormat.gif differ
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/richtextarea/removeLink.gif b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/richtextarea/removeLink.gif
new file mode 100644 (file)
index 0000000..522ab4b
Binary files /dev/null and b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/richtextarea/removeLink.gif differ
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/richtextarea/strikeThrough.gif b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/richtextarea/strikeThrough.gif
new file mode 100644 (file)
index 0000000..6b174c8
Binary files /dev/null and b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/richtextarea/strikeThrough.gif differ
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/richtextarea/subscript.gif b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/richtextarea/subscript.gif
new file mode 100644 (file)
index 0000000..04bba05
Binary files /dev/null and b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/richtextarea/subscript.gif differ
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/richtextarea/superscript.gif b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/richtextarea/superscript.gif
new file mode 100644 (file)
index 0000000..ac478ee
Binary files /dev/null and b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/richtextarea/superscript.gif differ
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/richtextarea/ul.gif b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/richtextarea/ul.gif
new file mode 100644 (file)
index 0000000..01380db
Binary files /dev/null and b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/richtextarea/ul.gif differ
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/richtextarea/underline.gif b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/richtextarea/underline.gif
new file mode 100644 (file)
index 0000000..82bae11
Binary files /dev/null and b/src/com/itmill/toolkit/terminal/terminal/gwt/client/ui/richtextarea/underline.gif differ
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/server/ApplicationPortlet.java b/src/com/itmill/toolkit/terminal/terminal/gwt/server/ApplicationPortlet.java
new file mode 100644 (file)
index 0000000..894eba5
--- /dev/null
@@ -0,0 +1,91 @@
+package com.itmill.toolkit.terminal.gwt.server;\r
+\r
+import java.io.IOException;\r
+import java.io.PrintWriter;\r
+\r
+import javax.portlet.ActionRequest;\r
+import javax.portlet.ActionResponse;\r
+import javax.portlet.Portlet;\r
+import javax.portlet.PortletConfig;\r
+import javax.portlet.PortletException;\r
+import javax.portlet.PortletRequestDispatcher;\r
+import javax.portlet.PortletSession;\r
+import javax.portlet.RenderRequest;\r
+import javax.portlet.RenderResponse;\r
+\r
+import com.itmill.toolkit.Application;\r
+\r
+public class ApplicationPortlet implements Portlet {\r
+    // The application to show\r
+    protected String app = null;\r
+    // some applications might require forced height (and, more seldom, width)\r
+    protected String style = null; // e.g "height:500px;"\r
+    protected String widgetset = null;\r
+\r
+    public void destroy() {\r
+\r
+    }\r
+\r
+    public void init(PortletConfig config) throws PortletException {\r
+        app = config.getInitParameter("application");\r
+        if (app == null) {\r
+            app = "PortletDemo";\r
+        }\r
+        style = config.getInitParameter("style");\r
+        widgetset = config.getInitParameter("widgetset");\r
+    }\r
+\r
+    public void processAction(ActionRequest request, ActionResponse response)\r
+            throws PortletException, IOException {\r
+        PortletApplicationContext.dispatchRequest(this, request, response);\r
+    }\r
+\r
+    public void render(RenderRequest request, RenderResponse response)\r
+            throws PortletException, IOException {\r
+\r
+        // display the IT Mill Toolkit application\r
+        writeAjaxWindow(request, response);\r
+    }\r
+\r
+    protected void writeAjaxWindow(RenderRequest request,\r
+            RenderResponse response) throws IOException {\r
+\r
+        response.setContentType("text/html");\r
+        if (app != null) {\r
+            PortletSession sess = request.getPortletSession();\r
+            PortletApplicationContext ctx = PortletApplicationContext\r
+                    .getApplicationContext(sess);\r
+\r
+            PortletRequestDispatcher dispatcher = sess.getPortletContext()\r
+                    .getRequestDispatcher("/" + app);\r
+\r
+            try {\r
+                request.setAttribute(ApplicationServlet.REQUEST_FRAGMENT,\r
+                        "true");\r
+                if (widgetset != null) {\r
+                    request.setAttribute(ApplicationServlet.REQUEST_WIDGETSET,\r
+                            widgetset);\r
+                }\r
+                if (style != null) {\r
+                    request.setAttribute(ApplicationServlet.REQUEST_APPSTYLE,\r
+                            style);\r
+                }\r
+                dispatcher.include(request, response);\r
+\r
+            } catch (PortletException e) {\r
+                PrintWriter out = response.getWriter();\r
+                out.print("<h1>Servlet include failed!</h1>");\r
+                out.print("<div>" + e + "</div>");\r
+                ctx.setPortletApplication(this, null);\r
+                return;\r
+            }\r
+\r
+            Application app = (Application) request\r
+                    .getAttribute(Application.class.getName());\r
+            ctx.setPortletApplication(this, app);\r
+            ctx.firePortletRenderRequest(this, request, response);\r
+\r
+        }\r
+    }\r
+\r
+}\r
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/server/ApplicationServlet.java b/src/com/itmill/toolkit/terminal/terminal/gwt/server/ApplicationServlet.java
new file mode 100644 (file)
index 0000000..86312ad
--- /dev/null
@@ -0,0 +1,1642 @@
+/* 
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.itmill.toolkit.terminal.gwt.server;
+
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.io.Writer;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.Collection;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Properties;
+
+import javax.servlet.ServletContext;
+import javax.servlet.ServletException;
+import javax.servlet.ServletOutputStream;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+
+import org.xml.sax.SAXException;
+
+import com.itmill.toolkit.Application;
+import com.itmill.toolkit.Application.SystemMessages;
+import com.itmill.toolkit.external.org.apache.commons.fileupload.servlet.ServletFileUpload;
+import com.itmill.toolkit.service.FileTypeResolver;
+import com.itmill.toolkit.terminal.DownloadStream;
+import com.itmill.toolkit.terminal.ParameterHandler;
+import com.itmill.toolkit.terminal.ThemeResource;
+import com.itmill.toolkit.terminal.URIHandler;
+import com.itmill.toolkit.ui.Window;
+
+/**
+ * This servlet connects IT Mill Toolkit Application to Web.
+ * 
+ * @author IT Mill Ltd.
+ * @version
+ * @VERSION@
+ * @since 5.0
+ */
+
+public class ApplicationServlet extends HttpServlet {
+
+    private static final long serialVersionUID = -4937882979845826574L;
+
+    /**
+     * Version number of this release. For example "5.0.0".
+     */
+    public static final String VERSION;
+
+    /**
+     * Major version number. For example 5 in 5.1.0.
+     */
+    public static final int VERSION_MAJOR;
+
+    /**
+     * Minor version number. For example 1 in 5.1.0.
+     */
+    public static final int VERSION_MINOR;
+
+    /**
+     * Builds number. For example 0-custom_tag in 5.0.0-custom_tag.
+     */
+    public static final String VERSION_BUILD;
+
+    /* Initialize version numbers from string replaced by build-script. */
+    static {
+        if ("@VERSION@".equals("@" + "VERSION" + "@")) {
+            VERSION = "5.9.9-INTERNAL-NONVERSIONED-DEBUG-BUILD";
+        } else {
+            VERSION = "@VERSION@";
+        }
+        final String[] digits = VERSION.split("\\.");
+        VERSION_MAJOR = Integer.parseInt(digits[0]);
+        VERSION_MINOR = Integer.parseInt(digits[1]);
+        VERSION_BUILD = digits[2];
+    }
+
+    /**
+     * If the attribute is present in the request, a html fragment will be
+     * written instead of a whole page.
+     */
+    public static final String REQUEST_FRAGMENT = ApplicationServlet.class
+            .getName()
+            + ".fragment";
+    /**
+     * This request attribute forces widgetset used; e.g for portlets that can
+     * not have different widgetsets.
+     */
+    public static final String REQUEST_WIDGETSET = ApplicationServlet.class
+            .getName()
+            + ".widgetset";
+    /**
+     * This request attribute is used to add styles to the main element. E.g
+     * "height:500px" generates a style="height:500px" to the main element,
+     * useful from some embedding situations (e.g portlet include.)
+     */
+    public static final String REQUEST_APPSTYLE = ApplicationServlet.class
+            .getName()
+            + ".style";
+
+    // Configurable parameter names
+    private static final String PARAMETER_DEBUG = "Debug";
+
+    private static final String PARAMETER_ITMILL_RESOURCES = "Resources";
+
+    private static final int DEFAULT_BUFFER_SIZE = 32 * 1024;
+
+    private static final int MAX_BUFFER_SIZE = 64 * 1024;
+
+    // TODO This is session specific not servlet wide data. No need to store
+    // this here, move it to Session from where it can be queried when required
+    protected static HashMap applicationToAjaxAppMgrMap = new HashMap();
+
+    private static final String RESOURCE_URI = "/RES/";
+
+    private static final String AJAX_UIDL_URI = "/UIDL";
+
+    static final String THEME_DIRECTORY_PATH = "ITMILL/themes/";
+
+    private static final int DEFAULT_THEME_CACHETIME = 1000 * 60 * 60 * 24;
+
+    static final String WIDGETSET_DIRECTORY_PATH = "ITMILL/widgetsets/";
+
+    // Name of the default widget set, used if not specified in web.xml
+    private static final String DEFAULT_WIDGETSET = "com.itmill.toolkit.terminal.gwt.DefaultWidgetSet";
+
+    // Widget set parameter name
+    private static final String PARAMETER_WIDGETSET = "widgetset";
+
+    // Private fields
+    private Class applicationClass;
+
+    private Properties applicationProperties;
+
+    private String resourcePath = null;
+
+    private String debugMode = "";
+
+    // Is this servlet application runner
+    private boolean isApplicationRunnerServlet = false;
+
+    // If servlet is application runner, store request's classname
+    private String applicationRunnerClassname = null;
+
+    private ClassLoader classLoader;
+
+    private boolean testingToolsActive = false;
+
+    private String testingToolsServerUri = null;
+
+    /**
+     * Called by the servlet container to indicate to a servlet that the servlet
+     * is being placed into service.
+     * 
+     * @param servletConfig
+     *                the object containing the servlet's configuration and
+     *                initialization parameters
+     * @throws javax.servlet.ServletException
+     *                 if an exception has occurred that interferes with the
+     *                 servlet's normal operation.
+     */
+    public void init(javax.servlet.ServletConfig servletConfig)
+            throws javax.servlet.ServletException {
+        super.init(servletConfig);
+
+        // Get applicationRunner
+        final String applicationRunner = servletConfig
+                .getInitParameter("applicationRunner");
+        if (applicationRunner != null) {
+            if ("true".equals(applicationRunner)) {
+                isApplicationRunnerServlet = true;
+            } else if ("false".equals(applicationRunner)) {
+                isApplicationRunnerServlet = false;
+            } else {
+                throw new ServletException(
+                        "If applicationRunner parameter is given for an application, it must be 'true' or 'false'");
+            }
+        }
+
+        // Stores the application parameters into Properties object
+        applicationProperties = new Properties();
+        for (final Enumeration e = servletConfig.getInitParameterNames(); e
+                .hasMoreElements();) {
+            final String name = (String) e.nextElement();
+            applicationProperties.setProperty(name, servletConfig
+                    .getInitParameter(name));
+        }
+
+        // Overrides with server.xml parameters
+        final ServletContext context = servletConfig.getServletContext();
+        for (final Enumeration e = context.getInitParameterNames(); e
+                .hasMoreElements();) {
+            final String name = (String) e.nextElement();
+            applicationProperties.setProperty(name, context
+                    .getInitParameter(name));
+        }
+
+        // Gets the debug window parameter
+        final String debug = getApplicationOrSystemProperty(PARAMETER_DEBUG, "")
+                .toLowerCase();
+
+        // Enables application specific debug
+        if (!"".equals(debug) && !"true".equals(debug)
+                && !"false".equals(debug)) {
+            throw new ServletException(
+                    "If debug parameter is given for an application, it must be 'true' or 'false'");
+        }
+        debugMode = debug;
+
+        // Gets Testing Tools parameters if feature is activated
+        if (getApplicationOrSystemProperty("testingToolsActive", "false")
+                .equals("true")) {
+            testingToolsActive = true;
+            testingToolsServerUri = getApplicationOrSystemProperty(
+                    "testingToolsServerUri", null);
+        }
+
+        // Gets custom class loader
+        final String classLoaderName = getApplicationOrSystemProperty(
+                "ClassLoader", null);
+        ClassLoader classLoader;
+        if (classLoaderName == null) {
+            classLoader = getClass().getClassLoader();
+        } else {
+            try {
+                final Class classLoaderClass = getClass().getClassLoader()
+                        .loadClass(classLoaderName);
+                final Constructor c = classLoaderClass
+                        .getConstructor(new Class[] { ClassLoader.class });
+                classLoader = (ClassLoader) c
+                        .newInstance(new Object[] { getClass().getClassLoader() });
+            } catch (final Exception e) {
+                System.err.println("Could not find specified class loader: "
+                        + classLoaderName);
+                throw new ServletException(e);
+            }
+        }
+        this.classLoader = classLoader;
+
+        // Loads the application class using the same class loader
+        // as the servlet itself
+        if (!isApplicationRunnerServlet) {
+            // Gets the application class name
+            final String applicationClassName = servletConfig
+                    .getInitParameter("application");
+            if (applicationClassName == null) {
+                throw new ServletException(
+                        "Application not specified in servlet parameters");
+            }
+            try {
+                applicationClass = classLoader.loadClass(applicationClassName);
+            } catch (final ClassNotFoundException e) {
+                throw new ServletException("Failed to load application class: "
+                        + applicationClassName);
+            }
+        } else {
+            // This servlet is in application runner mode, it uses classloader
+            // later to create Applications based on URL
+        }
+
+    }
+
+    /**
+     * Gets an application or system property value.
+     * 
+     * @param parameterName
+     *                the Name or the parameter.
+     * @param defaultValue
+     *                the Default to be used.
+     * @return String value or default if not found
+     */
+    private String getApplicationOrSystemProperty(String parameterName,
+            String defaultValue) {
+
+        // Try application properties
+        String val = applicationProperties.getProperty(parameterName);
+        if (val != null) {
+            return val;
+        }
+
+        // Try lowercased application properties for backward compability with
+        // 3.0.2 and earlier
+        val = applicationProperties.getProperty(parameterName.toLowerCase());
+        if (val != null) {
+            return val;
+        }
+
+        // Try system properties
+        String pkgName;
+        final Package pkg = getClass().getPackage();
+        if (pkg != null) {
+            pkgName = pkg.getName();
+        } else {
+            final String className = getClass().getName();
+            pkgName = new String(className.toCharArray(), 0, className
+                    .lastIndexOf('.'));
+        }
+        val = System.getProperty(pkgName + "." + parameterName);
+        if (val != null) {
+            return val;
+        }
+
+        // Try lowercased system properties
+        val = System.getProperty(pkgName + "." + parameterName.toLowerCase());
+        if (val != null) {
+            return val;
+        }
+
+        return defaultValue;
+    }
+
+    /**
+     * Receives standard HTTP requests from the public service method and
+     * dispatches them.
+     * 
+     * @param request
+     *                the object that contains the request the client made of
+     *                the servlet.
+     * @param response
+     *                the object that contains the response the servlet returns
+     *                to the client.
+     * @throws ServletException
+     *                 if an input or output error occurs while the servlet is
+     *                 handling the TRACE request.
+     * @throws IOException
+     *                 if the request for the TRACE cannot be handled.
+     */
+    protected void service(HttpServletRequest request,
+            HttpServletResponse response) throws ServletException, IOException {
+
+        // check if we should serve static files (widgetsets, themes)
+        if ((request.getPathInfo() != null)
+                && (request.getPathInfo().length() > 10)) {
+            if ((request.getContextPath() != null)
+                    && (request.getRequestURI().startsWith("/ITMILL/"))) {
+                serveStaticResourcesInITMILL(request.getRequestURI(), response);
+                return;
+            } else if (request.getRequestURI().startsWith(
+                    request.getContextPath() + "/ITMILL/")) {
+                serveStaticResourcesInITMILL(request.getRequestURI().substring(
+                        request.getContextPath().length()), response);
+                return;
+            }
+        }
+
+        Application application = null;
+        boolean UIDLrequest = false;
+        try {
+            // handle file upload if multipart request
+            if (ServletFileUpload.isMultipartContent(request)) {
+                application = getExistingApplication(request, response);
+                if (application == null) {
+                    throw new SessionExpired();
+                }
+                // Invokes context transaction listeners
+                // note: endTransaction is called on finalize below
+                ((WebApplicationContext) application.getContext())
+                        .startTransaction(application, request);
+                getApplicationManager(application).handleFileUpload(request,
+                        response);
+                return;
+            }
+
+            // Update browser details
+            final WebBrowser browser = WebApplicationContext
+                    .getApplicationContext(request.getSession()).getBrowser();
+            browser.updateBrowserProperties(request);
+            // TODO Add screen height and width to the GWT client
+
+            // Handles AJAX UIDL requests
+            if (request.getPathInfo() != null) {
+
+                String compare = AJAX_UIDL_URI;
+                if (isApplicationRunnerServlet) {
+                    final String[] URIparts = getApplicationRunnerURIs(request);
+                    applicationRunnerClassname = URIparts[4];
+                    compare = "/" + applicationRunnerClassname + AJAX_UIDL_URI;
+                }
+
+                if (request.getPathInfo().startsWith(compare + "/")
+                        || request.getPathInfo().endsWith(compare)) {
+                    UIDLrequest = true;
+                    application = getExistingApplication(request, response);
+                    if (application == null) {
+                        // No existing applications found
+                        final String repaintAll = request
+                                .getParameter("repaintAll");
+                        if ((repaintAll != null) && (repaintAll.equals("1"))) {
+                            // UIDL request contains valid repaintAll=1 event,
+                            // probably user wants to initiate new application
+                            // through custom index.html without writeAjaxPage
+                            application = getNewApplication(request, response);
+                        } else {
+                            // UIDL request refers to non-existing application
+                            throw new SessionExpired();
+                        }
+                    }
+
+                    // Invokes context transaction listeners
+                    // note: endTransaction is called on finalize below
+                    ((WebApplicationContext) application.getContext())
+                            .startTransaction(application, request);
+
+                    // Handle UIDL request
+                    getApplicationManager(application).handleUidlRequest(
+                            request, response, this);
+                    return;
+                }
+            }
+
+            // Get existing application
+            application = getExistingApplication(request, response);
+            if (application == null
+                    || request.getParameter("restartApplication") != null
+                    || request.getParameter("closeApplication") != null) {
+                if (application != null) {
+                    application.close();
+                    final HttpSession session = request.getSession(false);
+                    if (session != null) {
+                        ApplicationServlet.applicationToAjaxAppMgrMap
+                                .remove(application);
+                        WebApplicationContext.getApplicationContext(session)
+                                .removeApplication(application);
+                    }
+                }
+                if (request.getParameter("closeApplication") != null) {
+                    return;
+                }
+                // Not found, creating new application
+                application = getNewApplication(request, response);
+            }
+
+            // Invokes context transaction listeners
+            // note: endTransaction is called on finalize below
+            ((WebApplicationContext) application.getContext())
+                    .startTransaction(application, request);
+
+            // Removes application if it has stopped
+            if (!application.isRunning()) {
+                endApplication(request, response, application);
+                return;
+            }
+
+            // Finds the window within the application
+            Window window = null;
+            window = getApplicationWindow(request, application);
+
+            // Handle parameters
+            final Map parameters = request.getParameterMap();
+            if (window != null && parameters != null) {
+                window.handleParameters(parameters);
+            }
+
+            // Is this a download request from application
+            DownloadStream download = null;
+
+            // Handles the URI if the application is still running
+            download = handleURI(application, request, response);
+
+            // If this is not a download request
+            if (download == null) {
+
+                // Sets terminal type for the window, if not already set
+                if (window.getTerminal() == null) {
+                    window.setTerminal(browser);
+                }
+
+                // Finds theme name
+                String themeName = window.getTheme();
+                if (request.getParameter("theme") != null) {
+                    themeName = request.getParameter("theme");
+                }
+
+                if (themeName == null) {
+                    themeName = "default";
+                }
+
+                // Handles resource requests
+                if (handleResourceRequest(request, response, themeName)) {
+                    return;
+                }
+
+                // Send initial AJAX page that kickstarts Toolkit application
+                writeAjaxPage(request, response, window, themeName, application);
+
+            } else {
+                // Client downloads an resource
+                handleDownload(download, request, response);
+            }
+
+        } catch (final SessionExpired e) {
+            // Session has expired, notify user
+            Application.SystemMessages ci = getSystemMessages();
+            if (!UIDLrequest) {
+                // 'plain' http req - e.g. browser reload;
+                // just go ahead redirect the browser
+                response.sendRedirect(ci.getSessionExpiredURL());
+            } else {
+                // send uidl redirect
+                criticalNotification(request, response, ci
+                        .getSessionExpiredCaption(), ci
+                        .getSessionExpiredMessage(), ci.getSessionExpiredURL());
+            }
+
+        } catch (final Throwable e) {
+            e.printStackTrace();
+            // if this was an UIDL request, response UIDL back to client
+            if (UIDLrequest) {
+                Application.SystemMessages ci = getSystemMessages();
+                criticalNotification(request, response, ci
+                        .getInternalErrorCaption(), ci
+                        .getInternalErrorMessage(), ci.getInternalErrorURL());
+            } else {
+                // Re-throw other exceptions
+                throw new ServletException(e);
+            }
+        } finally {
+            // Notifies transaction end
+            if (application != null) {
+                ((WebApplicationContext) application.getContext())
+                        .endTransaction(application, request);
+            }
+        }
+    }
+
+    /** Get system messages from the current application class */
+    private SystemMessages getSystemMessages() {
+        try {
+            Class appCls = applicationClass;
+            if (isApplicationRunnerServlet) {
+                appCls = getClass().getClassLoader().loadClass(
+                        applicationRunnerClassname);
+            }
+            Method m = appCls.getMethod("getSystemMessages", null);
+            return (Application.SystemMessages) m.invoke(null, null);
+        } catch (ClassNotFoundException e) {
+            // This should never happen
+            e.printStackTrace();
+        } catch (SecurityException e) {
+            e.printStackTrace();
+            System.out
+                    .print("Error: getSystemMessage() should be static public");
+        } catch (NoSuchMethodException e) {
+            // This is completely ok and should be silently ignored
+        } catch (IllegalArgumentException e) {
+            // This should never happen
+            e.printStackTrace();
+        } catch (IllegalAccessException e) {
+            e.printStackTrace();
+            System.out
+                    .print("Error: getSystemMessage() should be static public");
+        } catch (InvocationTargetException e) {
+            // This should never happen
+            e.printStackTrace();
+        }
+        return Application.getSystemMessages();
+    }
+
+    /**
+     * Serve resources in ITMILL directory if requested.
+     * 
+     * @param request
+     * @param response
+     * @throws IOException
+     */
+    private void serveStaticResourcesInITMILL(String filename,
+            HttpServletResponse response) throws IOException {
+
+        final ServletContext sc = getServletContext();
+        InputStream is = sc.getResourceAsStream(filename);
+        if (is == null) {
+            // try if requested file is found from classloader
+            try {
+                // strip leading "/" otherwise stream from JAR wont work
+                filename = filename.substring(1);
+                is = classLoader.getResourceAsStream(filename);
+            } catch (final Exception e) {
+                e.printStackTrace();
+            }
+            if (is == null) {
+                // cannot serve requested file
+                System.err
+                        .println("Requested resource ["
+                                + filename
+                                + "] not found from filesystem or through class loader."
+                                + " Add widgetset and/or theme JAR to your classpath or add files to WebContent/ITMILL folder.");
+                response.setStatus(404);
+                return;
+            }
+        }
+        final String mimetype = sc.getMimeType(filename);
+        if (mimetype != null) {
+            response.setContentType(mimetype);
+        }
+        final OutputStream os = response.getOutputStream();
+        final byte buffer[] = new byte[20000];
+        int bytes;
+        while ((bytes = is.read(buffer)) >= 0) {
+            os.write(buffer, 0, bytes);
+        }
+    }
+
+    /**
+     * Send notification to client's application. Used to notify client of
+     * critical errors and session expiration due to long inactivity. Server has
+     * no knowledge of what application client refers to.
+     * 
+     * @param request
+     *                the HTTP request instance.
+     * @param response
+     *                the HTTP response to write to.
+     * @param caption
+     *                for the notification
+     * @param message
+     *                for the notification
+     * @param url
+     *                url to load after message, null for current page
+     * @throws IOException
+     *                 if the writing failed due to input/output error.
+     */
+    void criticalNotification(HttpServletRequest request,
+            HttpServletResponse response, String caption, String message,
+            String url) throws IOException {
+
+        // clients JS app is still running, but server application either
+        // no longer exists or it might fail to perform reasonably.
+        // send a notification to client's application and link how
+        // to "restart" application.
+
+        if (caption != null) {
+            caption = "\"" + caption + "\"";
+        }
+        if (message != null) {
+            message = "\"" + message + "\"";
+        }
+        if (url != null) {
+            url = "\"" + url + "\"";
+        }
+
+        // Set the response type
+        response.setContentType("application/json; charset=UTF-8");
+        final ServletOutputStream out = response.getOutputStream();
+        final PrintWriter outWriter = new PrintWriter(new BufferedWriter(
+                new OutputStreamWriter(out, "UTF-8")));
+        outWriter.print("for(;;);[{\"changes\":[], \"meta\" : {"
+                + "\"appError\": {" + "\"caption\":" + caption + ","
+                + "\"message\" : " + message + "," + "\"url\" : " + url
+                + "}}, \"resources\": {}, \"locales\":[]}]");
+        outWriter.flush();
+        outWriter.close();
+        out.flush();
+    }
+
+    /**
+     * Resolve application URL and widgetset URL. Widgetset is not application
+     * specific.
+     * 
+     * @param request
+     * @return string array consisting of application url first and then
+     *         widgetset url.
+     */
+    private String[] getAppAndWidgetUrl(HttpServletRequest request) {
+        // don't use server and port in uri. It may cause problems with some
+        // virtual server configurations which lose the server name
+        String appUrl = null;
+        String widgetsetUrl = null;
+        if (isApplicationRunnerServlet) {
+            final String[] URIparts = getApplicationRunnerURIs(request);
+            widgetsetUrl = URIparts[0];
+            if (widgetsetUrl.equals("/")) {
+                widgetsetUrl = "";
+            }
+            appUrl = URIparts[1];
+        } else {
+            String[] urlParts;
+            try {
+                urlParts = getApplicationUrl(request).toString().split("\\/");
+                appUrl = "";
+                widgetsetUrl = "";
+                // if context is specified add it to widgetsetUrl
+                String ctxPath = request.getContextPath();
+                if (ctxPath.length() == 0
+                        && request
+                                .getAttribute("javax.servlet.include.context_path") != null) {
+                    // include request (e.g portlet), get contex path from
+                    // attribute
+                    ctxPath = (String) request
+                            .getAttribute("javax.servlet.include.context_path");
+                }
+                if (urlParts.length > 3
+                        && urlParts[3].equals(ctxPath.replaceAll("\\/", ""))) {
+                    widgetsetUrl += "/" + urlParts[3];
+                }
+                for (int i = 3; i < urlParts.length; i++) {
+                    appUrl += "/" + urlParts[i];
+                }
+                if (appUrl.endsWith("/")) {
+                    appUrl = appUrl.substring(0, appUrl.length() - 1);
+                }
+            } catch (final MalformedURLException e) {
+                e.printStackTrace();
+            }
+
+        }
+        return new String[] { appUrl, widgetsetUrl };
+    }
+
+    /**
+     * 
+     * @param request
+     *                the HTTP request.
+     * @param response
+     *                the HTTP response to write to.
+     * @param out
+     * @param unhandledParameters
+     * @param window
+     * @param terminalType
+     * @param theme
+     * @throws IOException
+     *                 if the writing failed due to input/output error.
+     * @throws MalformedURLException
+     *                 if the application is denied access the persistent data
+     *                 store represented by the given URL.
+     */
+    private void writeAjaxPage(HttpServletRequest request,
+            HttpServletResponse response, Window window, String themeName,
+            Application application) throws IOException, MalformedURLException {
+
+        // e.g portlets only want a html fragment
+        boolean fragment = (request.getAttribute(REQUEST_FRAGMENT) != null);
+        if (fragment) {
+            request.setAttribute(Application.class.getName(), application);
+        }
+
+        final BufferedWriter page = new BufferedWriter(new OutputStreamWriter(
+                response.getOutputStream()));
+        final String pathInfo = request.getPathInfo() == null ? "/" : request
+                .getPathInfo();
+        String title = ((window == null || window.getCaption() == null) ? "IT Mill Toolkit 5"
+                : window.getCaption());
+
+        String widgetset = null;
+        // request widgetset takes precedence (e.g portlet include)
+        Object reqParam = request.getAttribute(REQUEST_WIDGETSET);
+        try {
+            widgetset = (String) reqParam;
+        } catch (Exception e) {
+            System.err.println("Warning: request param " + REQUEST_WIDGETSET
+                    + " could not be used (not a String?)" + e);
+        }
+        if (widgetset == null) {
+            widgetset = applicationProperties.getProperty(PARAMETER_WIDGETSET);
+        }
+        if (widgetset == null) {
+            widgetset = DEFAULT_WIDGETSET;
+        }
+        final String[] urls = getAppAndWidgetUrl(request);
+        final String appUrl = urls[0];
+        final String widgetsetUrl = urls[1];
+
+        final String staticFilePath = getApplicationOrSystemProperty(
+                PARAMETER_ITMILL_RESOURCES, widgetsetUrl);
+
+        // Default theme does not use theme URI
+        String themeUri = null;
+        if (themeName != null) {
+            // Using custom theme
+            themeUri = staticFilePath + "/" + THEME_DIRECTORY_PATH + themeName;
+        }
+
+        boolean testingApplication = testingToolsActive
+                && request.getParameter("TT") != null;
+
+        if (!fragment) {
+            // Window renders are not cacheable
+            response.setCharacterEncoding("utf-8");
+            response.setHeader("Cache-Control", "no-cache");
+            response.setHeader("Pragma", "no-cache");
+            response.setDateHeader("Expires", 0);
+            response.setContentType("text/html");
+
+            // write html header
+            page.write("<!DOCTYPE html PUBLIC \"-//W3C//DTD "
+                    + "XHTML 1.0 Transitional//EN\" "
+                    + "\"http://www.w3.org/TR/xhtml1/"
+                    + "DTD/xhtml1-transitional.dtd\">\n");
+
+            page.write("<html xmlns=\"http://www.w3.org/1999/xhtml\""
+                    + ">\n<head>\n");
+            page.write("<style type=\"text/css\">"
+                    + "html, body {height:100%;}</style>");
+            page.write("<title>" + title + "</title>");
+
+            if (testingApplication) {
+                // TT script needs to be in head as it needs to be the first
+                // to hook capturing event listeners
+                writeTestingToolsScripts(page, request);
+            }
+
+            page
+                    .write("\n</head>\n<body scroll=\"auto\" class=\"i-generated-body\">\n");
+        }
+
+        String appId = appUrl;
+        if ("".equals(appUrl)) {
+            appId = "ROOT";
+        }
+        appId = appId.replaceAll("[^a-zA-Z0-9]", "");
+
+        if (isGecko17(request)) {
+            // special start page for gecko 1.7 versions. Firefox 1.0 is not
+            // supported, but the hack is make it possible to use linux and
+            // hosted mode browser for debugging. Note that due this hack,
+            // debugging gwt code in portals with linux will be problematic if
+            // there are multiple toolkit portlets visible at the same time.
+            // TODO remove this when hosted mode on linux gets newer gecko
+
+            page.write("<iframe id=\"__gwt_historyFrame\" "
+                    + "style=\"width:0;height:0;border:0;overflow:"
+                    + "hidden\" src=\"javascript:false\"></iframe>\n");
+            page.write("<script language='javascript' src='" + staticFilePath
+                    + "/" + WIDGETSET_DIRECTORY_PATH + widgetset + "/"
+                    + widgetset + ".nocache.js'></script>\n");
+            page.write("<script type=\"text/javascript\">\n");
+            page.write("//<![CDATA[\n");
+            page.write("if(!itmill || !itmill.toolkitConfigurations) {\n "
+                    + "if(!itmill) { var itmill = {}} \n"
+                    + "itmill.toolkitConfigurations = {};\n"
+                    + "itmill.themesLoaded = {}};\n");
+
+            page.write("itmill.toolkitConfigurations[\"" + appId + "\"] = {");
+            page.write("appUri:'" + appUrl + "', ");
+            page.write("pathInfo: '" + pathInfo + "', ");
+            page.write("themeUri:");
+            page.write(themeUri != null ? "'" + themeUri + "'" : "null");
+            if (testingApplication) {
+                page.write(", versionInfo : {toolkitVersion:\"");
+                page.write(VERSION);
+                page.write("\",applicationVersion:\"");
+                page.write(application.getVersion());
+                page.write("\"}");
+            }
+            page.write("};\n//]]>\n</script>\n");
+
+            if (themeName != null) {
+                // Custom theme's stylesheet, load only once, in different
+                // script
+                // tag to be dominate styles injected by widget
+                // set
+                page.write("<script type=\"text/javascript\">\n");
+                page.write("//<![CDATA[\n");
+                page.write("if(!itmill.themesLoaded['" + themeName + "']) {\n");
+                page
+                        .write("var stylesheet = document.createElement('link');\n");
+                page.write("stylesheet.setAttribute('rel', 'stylesheet');\n");
+                page.write("stylesheet.setAttribute('type', 'text/css');\n");
+                page.write("stylesheet.setAttribute('href', '" + themeUri
+                        + "/styles.css');\n");
+                page
+                        .write("document.getElementsByTagName('head')[0].appendChild(stylesheet);\n");
+                page.write("itmill.themesLoaded['" + themeName
+                        + "'] = true;\n}\n");
+                page.write("//]]>\n</script>\n");
+            }
+
+        } else {
+            page.write("<script type=\"text/javascript\">\n");
+            page.write("//<![CDATA[\n");
+            page.write("if(!itmill || !itmill.toolkitConfigurations) {\n "
+                    + "if(!itmill) { var itmill = {}} \n"
+                    + "itmill.toolkitConfigurations = {};\n"
+                    + "itmill.themesLoaded = {};\n");
+            page.write("document.write('<iframe id=\"__gwt_historyFrame\" "
+                    + "style=\"width:0;height:0;border:0;overflow:"
+                    + "hidden\" src=\"javascript:false\"></iframe>');\n");
+            page.write("document.write(\"<script language='javascript' src='"
+                    + staticFilePath + "/" + WIDGETSET_DIRECTORY_PATH
+                    + widgetset + "/" + widgetset
+                    + ".nocache.js'><\\/script>\");\n}\n");
+
+            page.write("itmill.toolkitConfigurations[\"" + appId + "\"] = {");
+            page.write("appUri:'" + appUrl + "', ");
+            page.write("pathInfo: '" + pathInfo + "', ");
+            page.write("themeUri:");
+            page.write(themeUri != null ? "'" + themeUri + "'" : "null");
+            if (testingApplication) {
+                page.write(", versionInfo : {toolkitVersion:\"");
+                page.write(VERSION);
+                page.write("\",applicationVersion:\"");
+                page.write(application.getVersion());
+                page.write("\"}");
+            }
+            page.write("};\n//]]>\n</script>\n");
+
+            if (themeName != null) {
+                // Custom theme's stylesheet, load only once, in different
+                // script
+                // tag to be dominate styles injected by widget
+                // set
+                page.write("<script type=\"text/javascript\">\n");
+                page.write("//<![CDATA[\n");
+                page.write("if(!itmill.themesLoaded['" + themeName + "']) {\n");
+                page
+                        .write("var stylesheet = document.createElement('link');\n");
+                page.write("stylesheet.setAttribute('rel', 'stylesheet');\n");
+                page.write("stylesheet.setAttribute('type', 'text/css');\n");
+                page.write("stylesheet.setAttribute('href', '" + themeUri
+                        + "/styles.css');\n");
+                page
+                        .write("document.getElementsByTagName('head')[0].appendChild(stylesheet);\n");
+                page.write("itmill.themesLoaded['" + themeName
+                        + "'] = true;\n}\n");
+                page.write("//]]>\n</script>\n");
+            }
+        }
+
+        String style = null;
+        reqParam = request.getAttribute(REQUEST_APPSTYLE);
+        if (reqParam != null) {
+            style = "style=\"" + reqParam + "\"";
+        }
+        page.write("<div id=\"" + appId + "\" class=\"i-app\" "
+                + (style != null ? style : "") + "></div>\n");
+
+        if (!fragment) {
+            page.write("</body>\n</html>\n");
+        }
+        page.close();
+
+    }
+
+    private boolean isGecko17(HttpServletRequest request) {
+        final WebBrowser browser = WebApplicationContext.getApplicationContext(
+                request.getSession()).getBrowser();
+        if (browser != null && browser.getBrowserApplication() != null) {
+            if (browser.getBrowserApplication().indexOf("rv:1.7.") > 0
+                    && browser.getBrowserApplication().indexOf("Gecko") > 0) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private void writeTestingToolsScripts(Writer page,
+            HttpServletRequest request) throws IOException {
+        // Testing Tools script and CSS files are served from Testing Tools
+        // Server
+        String ext = getTestingToolsUri(request);
+        ext = ext.substring(0, ext.lastIndexOf('/'));
+        page.write("<script src=\"" + ext + "/ext/TT.js"
+                + "\" type=\"text/javascript\"></script>\n");
+        page.write("<link rel=\"stylesheet\" href=\"" + ext + "/ext/TT.css"
+                + "\" type=\"text/css\" />\n");
+
+    }
+
+    private String getTestingToolsUri(HttpServletRequest request) {
+        if (testingToolsServerUri == null) {
+            // Default behavior is that Testing Tools Server application exists
+            // on same host as current application does in port 8099.
+            testingToolsServerUri = "http" + "://" + request.getServerName()
+                    + ":8099" + "/TestingToolsServer";
+        }
+        return testingToolsServerUri;
+    }
+
+    /**
+     * Handles the requested URI. An application can add handlers to do special
+     * processing, when a certain URI is requested. The handlers are invoked
+     * before any windows URIs are processed and if a DownloadStream is returned
+     * it is sent to the client.
+     * 
+     * @param application
+     *                the Application owning the URI.
+     * @param request
+     *                the HTTP request instance.
+     * @param response
+     *                the HTTP response to write to.
+     * @return boolean <code>true</code> if the request was handled and
+     *         further processing should be suppressed, <code>false</code>
+     *         otherwise.
+     * @see com.itmill.toolkit.terminal.URIHandler
+     */
+    private DownloadStream handleURI(Application application,
+            HttpServletRequest request, HttpServletResponse response) {
+
+        String uri = request.getPathInfo();
+
+        // If no URI is available
+        if (uri == null) {
+            uri = "";
+        }
+
+        // Removes the leading /
+        while (uri.startsWith("/") && uri.length() > 0) {
+            uri = uri.substring(1);
+        }
+
+        // If using application runner, remove package and class name
+        if (isApplicationRunnerServlet) {
+            uri = uri.replaceFirst(applicationRunnerClassname + "/", "");
+        }
+
+        // Handles the uri
+        DownloadStream stream = null;
+        try {
+            stream = application.handleURI(application.getURL(), uri);
+        } catch (final Throwable t) {
+            application.terminalError(new URIHandlerErrorImpl(application, t));
+        }
+
+        return stream;
+    }
+
+    /**
+     * Handles the requested URI. An application can add handlers to do special
+     * processing, when a certain URI is requested. The handlers are invoked
+     * before any windows URIs are processed and if a DownloadStream is returned
+     * it is sent to the client.
+     * 
+     * @param stream
+     *                the download stream.
+     * 
+     * @param request
+     *                the HTTP request instance.
+     * @param response
+     *                the HTTP response to write to.
+     * 
+     * @see com.itmill.toolkit.terminal.URIHandler
+     */
+    private void handleDownload(DownloadStream stream,
+            HttpServletRequest request, HttpServletResponse response) {
+
+        if (stream.getParameter("Location") != null) {
+            response.setStatus(HttpServletResponse.SC_FOUND);
+            response.addHeader("Location", stream.getParameter("Location"));
+            return;
+        }
+
+        // Download from given stream
+        final InputStream data = stream.getStream();
+        if (data != null) {
+
+            // Sets content type
+            response.setContentType(stream.getContentType());
+
+            // Sets cache headers
+            final long cacheTime = stream.getCacheTime();
+            if (cacheTime <= 0) {
+                response.setHeader("Cache-Control", "no-cache");
+                response.setHeader("Pragma", "no-cache");
+                response.setDateHeader("Expires", 0);
+            } else {
+                response.setHeader("Cache-Control", "max-age=" + cacheTime
+                        / 1000);
+                response.setDateHeader("Expires", System.currentTimeMillis()
+                        + cacheTime);
+                response.setHeader("Pragma", "cache"); // Required to apply
+                // caching in some
+                // Tomcats
+            }
+
+            // Copy download stream parameters directly
+            // to HTTP headers.
+            final Iterator i = stream.getParameterNames();
+            if (i != null) {
+                while (i.hasNext()) {
+                    final String param = (String) i.next();
+                    response.setHeader(param, stream.getParameter(param));
+                }
+            }
+
+            int bufferSize = stream.getBufferSize();
+            if (bufferSize <= 0 || bufferSize > MAX_BUFFER_SIZE) {
+                bufferSize = DEFAULT_BUFFER_SIZE;
+            }
+            final byte[] buffer = new byte[bufferSize];
+            int bytesRead = 0;
+
+            try {
+                final OutputStream out = response.getOutputStream();
+
+                while ((bytesRead = data.read(buffer)) > 0) {
+                    out.write(buffer, 0, bytesRead);
+                    out.flush();
+                }
+                out.close();
+            } catch (final IOException ignored) {
+                System.err
+                        .println("Warning: ApplicationServlet.handleDownload()"
+                                + " threw IOException.");
+            }
+
+        }
+
+    }
+
+    /**
+     * Handles theme resource file requests. Resources supplied with the themes
+     * are provided by the WebAdapterServlet.
+     * 
+     * @param request
+     *                the HTTP request.
+     * @param response
+     *                the HTTP response.
+     * @return boolean <code>true</code> if the request was handled and
+     *         further processing should be suppressed, <code>false</code>
+     *         otherwise.
+     * @throws ServletException
+     *                 if an exception has occurred that interferes with the
+     *                 servlet's normal operation.
+     */
+    private boolean handleResourceRequest(HttpServletRequest request,
+            HttpServletResponse response, String themeName)
+            throws ServletException {
+
+        // If the resource path is unassigned, initialize it
+        if (resourcePath == null) {
+            resourcePath = request.getContextPath() + request.getServletPath()
+                    + RESOURCE_URI;
+            // WebSphere Application Server related fix
+            resourcePath = resourcePath.replaceAll("//", "/");
+        }
+
+        String resourceId = request.getPathInfo();
+
+        // Checks if this really is a resource request
+        if (resourceId == null || !resourceId.startsWith(RESOURCE_URI)) {
+            return false;
+        }
+
+        // Checks the resource type
+        resourceId = resourceId.substring(RESOURCE_URI.length());
+        InputStream data = null;
+
+        // Gets theme resources
+        try {
+            data = getServletContext().getResourceAsStream(
+                    THEME_DIRECTORY_PATH + themeName + "/" + resourceId);
+        } catch (final Exception e) {
+            e.printStackTrace();
+            data = null;
+        }
+
+        // Writes the response
+        try {
+            if (data != null) {
+                response.setContentType(FileTypeResolver
+                        .getMIMEType(resourceId));
+
+                // Use default cache time for theme resources
+                response.setHeader("Cache-Control", "max-age="
+                        + DEFAULT_THEME_CACHETIME / 1000);
+                response.setDateHeader("Expires", System.currentTimeMillis()
+                        + DEFAULT_THEME_CACHETIME);
+                response.setHeader("Pragma", "cache"); // Required to apply
+                // caching in some
+                // Tomcats
+
+                // Writes the data to client
+                final byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
+                int bytesRead = 0;
+                final OutputStream out = response.getOutputStream();
+                while ((bytesRead = data.read(buffer)) > 0) {
+                    out.write(buffer, 0, bytesRead);
+                }
+                out.close();
+                data.close();
+            } else {
+                response.sendError(HttpServletResponse.SC_NOT_FOUND);
+            }
+
+        } catch (final java.io.IOException e) {
+            System.err.println("Resource transfer failed:  "
+                    + request.getRequestURI() + ". (" + e.getMessage() + ")");
+        }
+
+        return true;
+    }
+
+    /**
+     * Gets the current application URL from request.
+     * 
+     * @param request
+     *                the HTTP request.
+     * @throws MalformedURLException
+     *                 if the application is denied access to the persistent
+     *                 data store represented by the given URL.
+     */
+    private URL getApplicationUrl(HttpServletRequest request)
+            throws MalformedURLException {
+
+        URL applicationUrl;
+        try {
+            final URL reqURL = new URL(
+                    (request.isSecure() ? "https://" : "http://")
+                            + request.getServerName()
+                            + ((request.isSecure() && request.getServerPort() == 443)
+                                    || (!request.isSecure() && request
+                                            .getServerPort() == 80) ? "" : ":"
+                                    + request.getServerPort())
+                            + request.getRequestURI());
+            String servletPath = "";
+            if (request.getAttribute("javax.servlet.include.servlet_path") != null) {
+                // this is an include request
+                servletPath = request.getAttribute(
+                        "javax.servlet.include.context_path").toString()
+                        + request
+                                .getAttribute("javax.servlet.include.servlet_path");
+
+            } else {
+                servletPath = request.getContextPath()
+                        + request.getServletPath();
+            }
+            if (servletPath.length() == 0
+                    || servletPath.charAt(servletPath.length() - 1) != '/') {
+                servletPath = servletPath + "/";
+            }
+            applicationUrl = new URL(reqURL, servletPath);
+        } catch (final MalformedURLException e) {
+            System.err.println("Error constructing application url "
+                    + request.getRequestURI() + " (" + e + ")");
+            throw e;
+        }
+
+        return applicationUrl;
+    }
+
+    /**
+     * Parses application runner URIs.
+     * 
+     * If request URL is e.g.
+     * http://localhost:8080/itmill/run/com.itmill.toolkit.demo.Calc then
+     * <ul>
+     * <li>context=itmill</li>
+     * <li>Runner servlet=run</li>
+     * <li>Toolkit application=com.itmill.toolkit.demo.Calc</li>
+     * </ul>
+     * 
+     * @param request
+     * @return string array containing widgetset URI, application URI and
+     *         context, runner, application classname
+     */
+    private String[] getApplicationRunnerURIs(HttpServletRequest request) {
+        final String[] urlParts = request.getRequestURI().toString().split(
+                "\\/");
+        String context = null;
+        String runner = null;
+        String applicationClassname = null;
+        if (urlParts[1].equals(request.getContextPath().replaceAll("\\/", ""))) {
+            // class name comes after web context and runner application
+            context = urlParts[1];
+            runner = urlParts[2];
+            applicationClassname = urlParts[3];
+            return new String[] { "/" + context,
+                    "/" + context + "/" + runner + "/" + applicationClassname,
+                    context, runner, applicationClassname };
+        } else {
+            // no context
+            context = "";
+            runner = urlParts[1];
+            applicationClassname = urlParts[2];
+            return new String[] { "/",
+                    "/" + runner + "/" + applicationClassname, context, runner,
+                    applicationClassname };
+        }
+    }
+
+    /**
+     * Gets the existing application for given request. Looks for application
+     * instance for given request based on the requested URL.
+     * 
+     * @param request
+     *                the HTTP request.
+     * @param response
+     * @return Application instance, or null if the URL does not map to valid
+     *         application.
+     * @throws MalformedURLException
+     *                 if the application is denied access to the persistent
+     *                 data store represented by the given URL.
+     * @throws SAXException
+     * @throws IllegalAccessException
+     * @throws InstantiationException
+     */
+    private Application getExistingApplication(HttpServletRequest request,
+            HttpServletResponse response) throws MalformedURLException,
+            SAXException, IllegalAccessException, InstantiationException {
+
+        // Ensures that the session is still valid
+        final HttpSession session = request.getSession(true);
+
+        // Gets application list for the session.
+        final Collection applications = WebApplicationContext
+                .getApplicationContext(session).getApplications();
+
+        // Search for the application (using the application URI) from the list
+        for (final Iterator i = applications.iterator(); i.hasNext();) {
+            final Application a = (Application) i.next();
+            final String aPath = a.getURL().getPath();
+            String servletPath = "";
+            if (isApplicationRunnerServlet) {
+                final String[] URIparts = getApplicationRunnerURIs(request);
+                servletPath = URIparts[1] + "/";
+            } else {
+                servletPath = request.getContextPath()
+                        + request.getServletPath();
+                if (servletPath.length() < aPath.length()) {
+                    servletPath += "/";
+                }
+            }
+            if (servletPath.equals(aPath)) {
+                // Found a running application
+                if (a.isRunning()) {
+                    return a;
+                }
+                // Application has stopped, so remove it before creating a new
+                // application
+                WebApplicationContext.getApplicationContext(session)
+                        .removeApplication(a);
+                break;
+            }
+        }
+
+        // Existing application not found
+        return null;
+    }
+
+    /**
+     * Creates new application for given request.
+     * 
+     * @param request
+     *                the HTTP request.
+     * @param response
+     * @return Application instance, or null if the URL does not map to valid
+     *         application.
+     * @throws MalformedURLException
+     *                 if the application is denied access to the persistent
+     *                 data store represented by the given URL.
+     * @throws SAXException
+     * @throws IllegalAccessException
+     * @throws InstantiationException
+     */
+    private Application getNewApplication(HttpServletRequest request,
+            HttpServletResponse response) throws MalformedURLException,
+            SAXException, IllegalAccessException, InstantiationException {
+
+        // Create application
+        final WebApplicationContext context = WebApplicationContext
+                .getApplicationContext(request.getSession());
+        final URL applicationUrl;
+
+        if (isApplicationRunnerServlet) {
+            final String[] URIparts = getApplicationRunnerURIs(request);
+            final String applicationClassname = URIparts[4];
+            applicationUrl = new URL(getApplicationUrl(request).toString()
+                    + applicationClassname + "/");
+            try {
+                applicationClass = classLoader.loadClass(applicationClassname);
+            } catch (final ClassNotFoundException e) {
+                throw new InstantiationException(
+                        "Failed to load application class: "
+                                + applicationClassname);
+            }
+        } else {
+            applicationUrl = getApplicationUrl(request);
+        }
+
+        // Creates new application and start it
+        try {
+            final Application application = (Application) applicationClass
+                    .newInstance();
+            context.addApplication(application);
+
+            // Sets initial locale from the request
+            application.setLocale(request.getLocale());
+
+            // Starts application
+            application.start(applicationUrl, applicationProperties, context);
+
+            return application;
+
+        } catch (final IllegalAccessException e) {
+            System.err.println("Illegal access to application class "
+                    + applicationClass.getName());
+            throw e;
+        } catch (final InstantiationException e) {
+            System.err.println("Failed to instantiate application class: "
+                    + applicationClass.getName());
+            throw e;
+        }
+    }
+
+    /**
+     * Ends the application.
+     * 
+     * @param request
+     *                the HTTP request.
+     * @param response
+     *                the HTTP response to write to.
+     * @param application
+     *                the application to end.
+     * @throws IOException
+     *                 if the writing failed due to input/output error.
+     */
+    private void endApplication(HttpServletRequest request,
+            HttpServletResponse response, Application application)
+            throws IOException {
+
+        String logoutUrl = application.getLogoutURL();
+        if (logoutUrl == null) {
+            logoutUrl = application.getURL().toString();
+        }
+
+        final HttpSession session = request.getSession();
+        if (session != null) {
+            WebApplicationContext.getApplicationContext(session)
+                    .removeApplication(application);
+        }
+
+        response.sendRedirect(response.encodeRedirectURL(logoutUrl));
+    }
+
+    /**
+     * Gets the existing application or create a new one. Get a window within an
+     * application based on the requested URI.
+     * 
+     * @param request
+     *                the HTTP Request.
+     * @param application
+     *                the Application to query for window.
+     * @return Window matching the given URI or null if not found.
+     * @throws ServletException
+     *                 if an exception has occurred that interferes with the
+     *                 servlet's normal operation.
+     */
+    private Window getApplicationWindow(HttpServletRequest request,
+            Application application) throws ServletException {
+
+        Window window = null;
+
+        // Finds the window where the request is handled
+        String path = request.getPathInfo();
+
+        // Main window as the URI is empty
+        if (path == null || path.length() == 0 || path.equals("/")) {
+            window = application.getMainWindow();
+        } else {
+            String windowName = null;
+            if (path.charAt(0) == '/') {
+                path = path.substring(1);
+            }
+            final int index = path.indexOf('/');
+            if (index < 0) {
+                windowName = path;
+                path = "";
+            } else {
+                windowName = path.substring(0, index);
+                path = path.substring(index + 1);
+            }
+            window = application.getWindow(windowName);
+
+            if (window == null) {
+                // By default, we use main window
+                window = application.getMainWindow();
+            } else if (!window.isVisible()) {
+                // Implicitly painting without actually invoking paint()
+                window.requestRepaintRequests();
+
+                // If the window is invisible send a blank page
+                return null;
+            }
+        }
+
+        return window;
+    }
+
+    /**
+     * Gets relative location of a theme resource.
+     * 
+     * @param theme
+     *                the Theme name.
+     * @param resource
+     *                the Theme resource.
+     * @return External URI specifying the resource
+     */
+    public String getResourceLocation(String theme, ThemeResource resource) {
+
+        if (resourcePath == null) {
+            return resource.getResourceId();
+        }
+        return resourcePath + theme + "/" + resource.getResourceId();
+    }
+
+    /**
+     * Checks if web adapter is in debug mode. Extra output is generated to log
+     * when debug mode is enabled.
+     * 
+     * @param parameters
+     * @return <code>true</code> if the web adapter is in debug mode.
+     *         otherwise <code>false</code>.
+     */
+    public boolean isDebugMode(Map parameters) {
+        if (parameters != null) {
+            final Object[] debug = (Object[]) parameters.get("debug");
+            if (debug != null && !"false".equals(debug[0].toString())
+                    && !"false".equals(debugMode)) {
+                return true;
+            }
+        }
+        return "true".equals(debugMode);
+    }
+
+    /**
+     * Implementation of ParameterHandler.ErrorEvent interface.
+     */
+    public class ParameterHandlerErrorImpl implements
+            ParameterHandler.ErrorEvent {
+
+        private ParameterHandler owner;
+
+        private Throwable throwable;
+
+        /**
+         * Gets the contained throwable.
+         * 
+         * @see com.itmill.toolkit.terminal.Terminal.ErrorEvent#getThrowable()
+         */
+        public Throwable getThrowable() {
+            return throwable;
+        }
+
+        /**
+         * Gets the source ParameterHandler.
+         * 
+         * @see com.itmill.toolkit.terminal.ParameterHandler.ErrorEvent#getParameterHandler()
+         */
+        public ParameterHandler getParameterHandler() {
+            return owner;
+        }
+
+    }
+
+    /**
+     * Implementation of URIHandler.ErrorEvent interface.
+     */
+    public class URIHandlerErrorImpl implements URIHandler.ErrorEvent {
+
+        private final URIHandler owner;
+
+        private final Throwable throwable;
+
+        /**
+         * 
+         * @param owner
+         * @param throwable
+         */
+        private URIHandlerErrorImpl(URIHandler owner, Throwable throwable) {
+            this.owner = owner;
+            this.throwable = throwable;
+        }
+
+        /**
+         * Gets the contained throwable.
+         * 
+         * @see com.itmill.toolkit.terminal.Terminal.ErrorEvent#getThrowable()
+         */
+        public Throwable getThrowable() {
+            return throwable;
+        }
+
+        /**
+         * Gets the source URIHandler.
+         * 
+         * @see com.itmill.toolkit.terminal.URIHandler.ErrorEvent#getURIHandler()
+         */
+        public URIHandler getURIHandler() {
+            return owner;
+        }
+    }
+
+    /**
+     * Gets communication manager for an application.
+     * 
+     * If this application has not been running before, new manager is created.
+     * 
+     * @param application
+     * @return CommunicationManager
+     */
+    private CommunicationManager getApplicationManager(Application application) {
+        CommunicationManager mgr = (CommunicationManager) applicationToAjaxAppMgrMap
+                .get(application);
+
+        if (mgr == null) {
+            // Creates new manager
+            mgr = new CommunicationManager(application);
+            applicationToAjaxAppMgrMap.put(application, mgr);
+        }
+        return mgr;
+    }
+
+    /**
+     * Gets resource path using different implementations. Required to
+     * supporting different servlet container implementations (application
+     * servers).
+     * 
+     * @param servletContext
+     * @param path
+     *                the resource path.
+     * @return the resource path.
+     */
+    protected static String getResourcePath(ServletContext servletContext,
+            String path) {
+        String resultPath = null;
+        resultPath = servletContext.getRealPath(path);
+        if (resultPath != null) {
+            return resultPath;
+        } else {
+            try {
+                final URL url = servletContext.getResource(path);
+                resultPath = url.getFile();
+            } catch (final Exception e) {
+                e.printStackTrace();
+            }
+        }
+        return resultPath;
+    }
+
+}
\ No newline at end of file
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/server/CommunicationManager.java b/src/com/itmill/toolkit/terminal/terminal/gwt/server/CommunicationManager.java
new file mode 100644 (file)
index 0000000..86bbb39
--- /dev/null
@@ -0,0 +1,1099 @@
+/* 
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.itmill.toolkit.terminal.gwt.server;
+
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.lang.reflect.Method;
+import java.net.SocketException;
+import java.text.DateFormatSymbols;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.GregorianCalendar;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+
+import javax.servlet.ServletException;
+import javax.servlet.ServletInputStream;
+import javax.servlet.ServletOutputStream;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import com.itmill.toolkit.Application;
+import com.itmill.toolkit.Application.SystemMessages;
+import com.itmill.toolkit.external.org.apache.commons.fileupload.FileItemIterator;
+import com.itmill.toolkit.external.org.apache.commons.fileupload.FileItemStream;
+import com.itmill.toolkit.external.org.apache.commons.fileupload.FileUploadException;
+import com.itmill.toolkit.external.org.apache.commons.fileupload.ProgressListener;
+import com.itmill.toolkit.external.org.apache.commons.fileupload.servlet.ServletFileUpload;
+import com.itmill.toolkit.terminal.Paintable;
+import com.itmill.toolkit.terminal.URIHandler;
+import com.itmill.toolkit.terminal.UploadStream;
+import com.itmill.toolkit.terminal.VariableOwner;
+import com.itmill.toolkit.terminal.Paintable.RepaintRequestEvent;
+import com.itmill.toolkit.ui.Component;
+import com.itmill.toolkit.ui.Upload;
+import com.itmill.toolkit.ui.Window;
+
+/**
+ * Application manager processes changes and paints for single application
+ * instance.
+ * 
+ * @author IT Mill Ltd.
+ * @version
+ * @VERSION@
+ * @since 5.0
+ */
+public class CommunicationManager implements Paintable.RepaintRequestListener {
+
+    private static String GET_PARAM_REPAINT_ALL = "repaintAll";
+
+    /* Variable records indexes */
+    private static final int VAR_PID = 1;
+    private static final int VAR_NAME = 2;
+    private static final int VAR_TYPE = 3;
+    private static final int VAR_VALUE = 0;
+
+    private static final String VAR_RECORD_SEPARATOR = "\u001e";
+
+    private static final String VAR_FIELD_SEPARATOR = "\u001f";
+
+    private static final int MAX_BUFFER_SIZE = 64 * 1024;
+
+    private final ArrayList dirtyPaintabletSet = new ArrayList();
+
+    private final HashMap paintableIdMap = new HashMap();
+
+    private final HashMap idPaintableMap = new HashMap();
+
+    private int idSequence = 0;
+
+    private final Application application;
+
+    private List locales;
+
+    private int pendingLocalesIndex;
+
+    public CommunicationManager(Application application) {
+        this.application = application;
+        requireLocale(application.getLocale().toString());
+    }
+
+    /**
+     * Handles file upload request submitted via Upload component.
+     * 
+     * @param request
+     * @param response
+     * @throws IOException
+     */
+    public void handleFileUpload(HttpServletRequest request,
+            HttpServletResponse response) throws IOException {
+        // Create a new file upload handler
+        final ServletFileUpload upload = new ServletFileUpload();
+
+        final UploadProgressListener pl = new UploadProgressListener();
+
+        upload.setProgressListener(pl);
+
+        // Parse the request
+        FileItemIterator iter;
+
+        try {
+            iter = upload.getItemIterator(request);
+            /*
+             * ATM this loop is run only once as we are uploading one file per
+             * request.
+             */
+            while (iter.hasNext()) {
+                final FileItemStream item = iter.next();
+                final String name = item.getFieldName();
+                final String filename = item.getName();
+                final String mimeType = item.getContentType();
+                final InputStream stream = item.openStream();
+                if (item.isFormField()) {
+                    // ignored, upload requests contains only files
+                } else {
+                    final String pid = name.split("_")[0];
+                    final Upload uploadComponent = (Upload) idPaintableMap
+                            .get(pid);
+                    if (uploadComponent.isReadOnly()) {
+                        throw new FileUploadException(
+                                "Warning: ignored file upload because upload component is set as read-only");
+                    }
+                    if (uploadComponent == null) {
+                        throw new FileUploadException(
+                                "Upload component not found");
+                    }
+                    synchronized (application) {
+                        // put upload component into receiving state
+                        uploadComponent.startUpload();
+                    }
+                    final UploadStream upstream = new UploadStream() {
+
+                        public String getContentName() {
+                            return filename;
+                        }
+
+                        public String getContentType() {
+                            return mimeType;
+                        }
+
+                        public InputStream getStream() {
+                            return stream;
+                        }
+
+                        public String getStreamName() {
+                            return "stream";
+                        }
+
+                    };
+
+                    // tell UploadProgressListener which component is receiving
+                    // file
+                    pl.setUpload(uploadComponent);
+
+                    uploadComponent.receiveUpload(upstream);
+                }
+            }
+        } catch (final FileUploadException e) {
+            e.printStackTrace();
+        }
+
+        // Send short response to acknowledge client that request was done
+        response.setContentType("text/html");
+        final OutputStream out = response.getOutputStream();
+        final PrintWriter outWriter = new PrintWriter(new BufferedWriter(
+                new OutputStreamWriter(out, "UTF-8")));
+        outWriter.print("<html><body>download handled</body></html>");
+        outWriter.flush();
+        out.close();
+    }
+
+    /**
+     * Handles UIDL request
+     * 
+     * @param request
+     * @param response
+     * @throws IOException
+     */
+    public void handleUidlRequest(HttpServletRequest request,
+            HttpServletResponse response, ApplicationServlet applicationServlet)
+            throws IOException {
+
+        // repaint requested or session has timed out and new one is created
+        boolean repaintAll = (request.getParameter(GET_PARAM_REPAINT_ALL) != null)
+                || request.getSession().isNew();
+
+        final OutputStream out = response.getOutputStream();
+        final PrintWriter outWriter = new PrintWriter(new BufferedWriter(
+                new OutputStreamWriter(out, "UTF-8")));
+
+        try {
+
+            // The rest of the process is synchronized with the application
+            // in order to guarantee that no parallel variable handling is
+            // made
+            synchronized (application) {
+
+                // Finds the window within the application
+                Window window = null;
+                if (application.isRunning()) {
+                    window = getApplicationWindow(request, application);
+                    // Returns if no window found
+                    if (window == null) {
+                        // This should not happen, no windows exists but
+                        // application is still open.
+                        System.err
+                                .println("Warning, could not get window for application with request URI "
+                                        + request.getRequestURI());
+                        return;
+                    }
+                } else {
+                    // application has been closed
+                    endApplication(request, response, application);
+                    return;
+                }
+
+                // Change all variables based on request parameters
+                if (!handleVariables(request, application)) {
+                    // var inconsistency; the client is probably out-of-sync
+                    SystemMessages ci = null;
+                    try {
+                        Method m = application.getClass().getMethod(
+                                "getSystemMessages", null);
+                        ci = (Application.SystemMessages) m.invoke(null, null);
+                    } catch (Exception e2) {
+                        // Not critical, but something is still wrong; print
+                        // stacktrace
+                        e2.printStackTrace();
+                    }
+                    if (ci != null) {
+                        String msg = ci.getOutOfSyncMessage();
+                        String cap = ci.getOutOfSyncCaption();
+                        if (msg != null || cap != null) {
+                            applicationServlet.criticalNotification(request,
+                                    response, cap, msg, ci.getOutOfSyncURL());
+                            // will reload page after this
+                            return;
+                        }
+                    }
+                    // No message to show, let's just repaint all.
+                    System.err
+                            .println("Warning: variable inconsistency - client is probably out-of-sync, repainting all.");
+                    repaintAll = true;
+
+                }
+
+                // If repaint is requested, clean all ids in this root window
+                if (repaintAll) {
+                    for (final Iterator it = idPaintableMap.keySet().iterator(); it
+                            .hasNext();) {
+                        final Component c = (Component) idPaintableMap.get(it
+                                .next());
+                        if (isChildOf(window, c)) {
+                            it.remove();
+                            paintableIdMap.remove(c);
+                        }
+                    }
+                }
+
+                // Removes application if it has stopped during variable changes
+                if (!application.isRunning()) {
+                    endApplication(request, response, application);
+                    return;
+                }
+
+                // Sets the response type
+                response.setContentType("application/json; charset=UTF-8");
+                // some dirt to prevent cross site scripting
+                outWriter.print("for(;;);[{");
+
+                outWriter.print("\"changes\":[");
+
+                // re-get mainwindow - may have been changed
+                Window newWindow = getApplicationWindow(request, application);
+                if (newWindow != window) {
+                    window = newWindow;
+                    repaintAll = true;
+                }
+
+                JsonPaintTarget paintTarget = new JsonPaintTarget(this,
+                        outWriter, !repaintAll);
+
+                // Paints components
+                ArrayList paintables;
+                if (repaintAll) {
+                    paintables = new ArrayList();
+                    paintables.add(window);
+
+                    // Reset sent locales
+                    locales = null;
+                    requireLocale(application.getLocale().toString());
+
+                } else {
+                    // remove detached components from paintableIdMap so they
+                    // can be GC'ed
+                    for (Iterator it = paintableIdMap.keySet().iterator(); it
+                            .hasNext();) {
+                        Component p = (Component) it.next();
+                        if (p.getApplication() == null) {
+                            idPaintableMap.remove(paintableIdMap.get(p));
+                            it.remove();
+                            dirtyPaintabletSet.remove(p);
+                            p.removeListener(this);
+                        }
+                    }
+                    paintables = getDirtyComponents(window);
+                }
+                if (paintables != null) {
+
+                    // We need to avoid painting children before parent.
+                    // This is ensured by ordering list by depth in component
+                    // tree
+                    Collections.sort(paintables, new Comparator() {
+                        public int compare(Object o1, Object o2) {
+                            Component c1 = (Component) o1;
+                            Component c2 = (Component) o2;
+                            int d1 = 0;
+                            while (c1.getParent() != null) {
+                                d1++;
+                                c1 = c1.getParent();
+                            }
+                            int d2 = 0;
+                            while (c2.getParent() != null) {
+                                d2++;
+                                c2 = c2.getParent();
+                            }
+                            if (d1 < d2) {
+                                return -1;
+                            }
+                            if (d1 > d2) {
+                                return 1;
+                            }
+                            return 0;
+                        }
+                    });
+
+                    for (final Iterator i = paintables.iterator(); i.hasNext();) {
+                        final Paintable p = (Paintable) i.next();
+
+                        // TODO CLEAN
+                        if (p instanceof Window) {
+                            final Window w = (Window) p;
+                            if (w.getTerminal() == null) {
+                                w.setTerminal(application.getMainWindow()
+                                        .getTerminal());
+                            }
+                        }
+                        /*
+                         * This does not seem to happen in tk5, but remember
+                         * this case: else if (p instanceof Component) { if
+                         * (((Component) p).getParent() == null || ((Component)
+                         * p).getApplication() == null) { // Component requested
+                         * repaint, but is no // longer attached: skip
+                         * paintablePainted(p); continue; } }
+                         */
+
+                        // TODO we may still get changes that have been
+                        // rendered already (changes with only cached flag)
+                        if (paintTarget.needsToBePainted(p)) {
+                            paintTarget.startTag("change");
+                            paintTarget.addAttribute("format", "uidl");
+                            final String pid = getPaintableId(p);
+                            paintTarget.addAttribute("pid", pid);
+
+                            p.paint(paintTarget);
+
+                            paintTarget.endTag("change");
+                        }
+                        paintablePainted(p);
+                    }
+                }
+
+                paintTarget.close();
+                outWriter.print("]"); // close changes
+
+                outWriter.print(", \"meta\" : {");
+                boolean metaOpen = false;
+
+                if (repaintAll) {
+                    metaOpen = true;
+                    outWriter.write("\"repaintAll\":true");
+                }
+
+                // add meta instruction for client to set focus if it is set
+                final Paintable f = (Paintable) application.consumeFocus();
+                if (f != null) {
+                    if (metaOpen) {
+                        outWriter.write(",");
+                    }
+                    outWriter.write("\"focus\":\"" + getPaintableId(f) + "\"");
+                }
+
+                outWriter.print("}, \"resources\" : {");
+
+                // Precache custom layouts
+                String themeName = window.getTheme();
+                if (request.getParameter("theme") != null) {
+                    themeName = request.getParameter("theme");
+                }
+                if (themeName == null) {
+                    themeName = "default";
+                }
+
+                // TODO We should only precache the layouts that are not
+                // cached already
+                int resourceIndex = 0;
+                for (final Iterator i = paintTarget.getPreCachedResources()
+                        .iterator(); i.hasNext();) {
+                    final String resource = (String) i.next();
+                    InputStream is = null;
+                    try {
+                        is = applicationServlet
+                                .getServletContext()
+                                .getResourceAsStream(
+                                        "/"
+                                                + ApplicationServlet.THEME_DIRECTORY_PATH
+                                                + themeName + "/" + resource);
+                    } catch (final Exception e) {
+                        e.printStackTrace();
+                    }
+                    if (is != null) {
+
+                        outWriter.print((resourceIndex++ > 0 ? ", " : "")
+                                + "\"" + resource + "\" : ");
+                        final StringBuffer layout = new StringBuffer();
+
+                        try {
+                            final InputStreamReader r = new InputStreamReader(
+                                    is);
+                            final char[] buffer = new char[20000];
+                            int charsRead = 0;
+                            while ((charsRead = r.read(buffer)) > 0) {
+                                layout.append(buffer, 0, charsRead);
+                            }
+                            r.close();
+                        } catch (final java.io.IOException e) {
+                            System.err.println("Resource transfer failed:  "
+                                    + request.getRequestURI() + ". ("
+                                    + e.getMessage() + ")");
+                        }
+                        outWriter.print("\""
+                                + JsonPaintTarget.escapeJSON(layout.toString())
+                                + "\"");
+                    } else {
+                        System.err.println("CustomLayout " + "/"
+                                + ApplicationServlet.THEME_DIRECTORY_PATH
+                                + themeName + "/" + resource + " not found!");
+                    }
+                }
+                outWriter.print("}");
+
+                printLocaleDeclarations(outWriter);
+
+                outWriter.print("}]");
+
+                outWriter.flush();
+                outWriter.close();
+            }
+
+            out.flush();
+            out.close();
+        } catch (SocketException e) {
+            // Most likely client browser closed socket
+            System.err
+                    .println("Warning: SocketException in CommunicationManager."
+                            + " Most likely client (browser) closed socket.");
+        } catch (final Throwable e) {
+            e.printStackTrace();
+            // Writes the error report to client
+            // FIXME breaks UIDL response, security shouldn't reveal stack trace
+            // to client side
+            final OutputStreamWriter w = new OutputStreamWriter(out);
+            final PrintWriter err = new PrintWriter(w);
+            err
+                    .write("<html><head><title>Application Internal Error</title></head><body>");
+            err.write("<h1>" + e.toString() + "</h1><pre>\n");
+            e.printStackTrace(new PrintWriter(err));
+            err.write("\n</pre></body></html>");
+            err.close();
+        }
+    }
+
+    /**
+     * If this method returns false, something was submitted that we did not
+     * expect; this is probably due to the client being out-of-sync and sending
+     * variable changes for non-existing pids
+     * 
+     * @param request
+     * @param application2
+     * @return true if successful, false if there was an inconsistency
+     * @throws IOException
+     */
+    private boolean handleVariables(HttpServletRequest request,
+            Application application2) throws IOException {
+        boolean success = true;
+
+        if (request.getContentLength() > 0) {
+
+            byte[] buffer = new byte[request.getContentLength()];
+            ServletInputStream inputStream = request.getInputStream();
+            int totalBytesRead = 0;
+            int bytesRead;
+            while ((bytesRead = inputStream.read(buffer, totalBytesRead,
+                    MAX_BUFFER_SIZE)) != -1) {
+                totalBytesRead += bytesRead;
+            }
+
+            String changes = new String(buffer, "utf-8");
+            // extract variables to two dim string array
+            final String[] tmp = changes.split(VAR_RECORD_SEPARATOR);
+            final String[][] variableRecords = new String[tmp.length][4];
+            for (int i = 0; i < tmp.length; i++) {
+                variableRecords[i] = tmp[i].split(VAR_FIELD_SEPARATOR);
+            }
+
+            for (int i = 0; i < variableRecords.length; i++) {
+                String[] variable = variableRecords[i];
+                String[] nextVariable = null;
+                if (i + 1 < variableRecords.length) {
+                    nextVariable = variableRecords[i + 1];
+                }
+                final VariableOwner owner = (VariableOwner) idPaintableMap
+                        .get(variable[VAR_PID]);
+                if (owner != null && owner.isEnabled()) {
+                    Map m;
+                    if (nextVariable != null
+                            && variable[VAR_PID].equals(nextVariable[VAR_PID])) {
+                        // we have more than one value changes in row for one
+                        // variable owner, collect em in HashMap
+                        m = new HashMap();
+                        m.put(variable[VAR_NAME], convertVariableValue(
+                                variable[VAR_TYPE].charAt(0),
+                                variable[VAR_VALUE]));
+                    } else {
+                        // use optimized single value map
+                        m = new SingleValueMap(variable[VAR_NAME],
+                                convertVariableValue(variable[VAR_TYPE]
+                                        .charAt(0), variable[VAR_VALUE]));
+                    }
+
+                    // collect following variable changes for this owner
+                    while (nextVariable != null
+                            && variable[VAR_PID].equals(nextVariable[VAR_PID])) {
+                        i++;
+                        variable = nextVariable;
+                        if (i + 1 < variableRecords.length) {
+                            nextVariable = variableRecords[i + 1];
+                        } else {
+                            nextVariable = null;
+                        }
+                        m.put(variable[VAR_NAME], convertVariableValue(
+                                variable[VAR_TYPE].charAt(0),
+                                variable[VAR_VALUE]));
+                    }
+                    owner.changeVariables(request, m);
+                } else {
+                    // Ignore variable change
+                    String msg = "Warning: Ignoring variable change for ";
+                    if (owner != null) {
+                        msg += "disabled component " + owner.getClass();
+                        String caption = ((Component) owner).getCaption();
+                        if (caption != null) {
+                            msg += ", caption=" + caption;
+                        }
+                    } else {
+                        msg += "non-existent component, VAR_PID="
+                                + variable[VAR_PID];
+                        success = false;
+                    }
+                    System.err.println(msg);
+                    continue;
+                }
+            }
+        }
+        return success;
+    }
+
+    private Object convertVariableValue(char variableType, String strValue) {
+        Object val = null;
+        switch (variableType) {
+        case 'a':
+            val = strValue.split(",");
+            break;
+        case 's':
+            val = strValue;
+            break;
+        case 'i':
+            val = Integer.valueOf(strValue);
+            break;
+        case 'l':
+            val = Long.valueOf(strValue);
+        case 'f':
+            val = Float.valueOf(strValue);
+            break;
+        case 'd':
+            val = Double.valueOf(strValue);
+            break;
+        case 'b':
+            val = Boolean.valueOf(strValue);
+            break;
+        }
+
+        return val;
+    }
+
+    private void printLocaleDeclarations(PrintWriter outWriter) {
+        /*
+         * ----------------------------- Sending Locale sensitive date
+         * -----------------------------
+         */
+
+        // Store JVM default locale for later restoration
+        // (we'll have to change the default locale for a while)
+        final Locale jvmDefault = Locale.getDefault();
+
+        // Send locale informations to client
+        outWriter.print(", \"locales\":[");
+        for (; pendingLocalesIndex < locales.size(); pendingLocalesIndex++) {
+
+            final Locale l = generateLocale((String) locales
+                    .get(pendingLocalesIndex));
+            // Locale name
+            outWriter.print("{\"name\":\"" + l.toString() + "\",");
+
+            /*
+             * Month names (both short and full)
+             */
+            final DateFormatSymbols dfs = new DateFormatSymbols(l);
+            final String[] short_months = dfs.getShortMonths();
+            final String[] months = dfs.getMonths();
+            outWriter.print("\"smn\":[\""
+                    + // ShortMonthNames
+                    short_months[0] + "\",\"" + short_months[1] + "\",\""
+                    + short_months[2] + "\",\"" + short_months[3] + "\",\""
+                    + short_months[4] + "\",\"" + short_months[5] + "\",\""
+                    + short_months[6] + "\",\"" + short_months[7] + "\",\""
+                    + short_months[8] + "\",\"" + short_months[9] + "\",\""
+                    + short_months[10] + "\",\"" + short_months[11] + "\""
+                    + "],");
+            outWriter.print("\"mn\":[\""
+                    + // MonthNames
+                    months[0] + "\",\"" + months[1] + "\",\"" + months[2]
+                    + "\",\"" + months[3] + "\",\"" + months[4] + "\",\""
+                    + months[5] + "\",\"" + months[6] + "\",\"" + months[7]
+                    + "\",\"" + months[8] + "\",\"" + months[9] + "\",\""
+                    + months[10] + "\",\"" + months[11] + "\"" + "],");
+
+            /*
+             * Weekday names (both short and full)
+             */
+            final String[] short_days = dfs.getShortWeekdays();
+            final String[] days = dfs.getWeekdays();
+            outWriter.print("\"sdn\":[\""
+                    + // ShortDayNames
+                    short_days[1] + "\",\"" + short_days[2] + "\",\""
+                    + short_days[3] + "\",\"" + short_days[4] + "\",\""
+                    + short_days[5] + "\",\"" + short_days[6] + "\",\""
+                    + short_days[7] + "\"" + "],");
+            outWriter.print("\"dn\":[\""
+                    + // DayNames
+                    days[1] + "\",\"" + days[2] + "\",\"" + days[3] + "\",\""
+                    + days[4] + "\",\"" + days[5] + "\",\"" + days[6] + "\",\""
+                    + days[7] + "\"" + "],");
+
+            /*
+             * First day of week (0 = sunday, 1 = monday)
+             */
+            final Calendar cal = new GregorianCalendar(l);
+            outWriter.print("\"fdow\":" + (cal.getFirstDayOfWeek() - 1) + ",");
+
+            /*
+             * Date formatting (MM/DD/YYYY etc.)
+             */
+            // Force our locale as JVM default for a while (SimpleDateFormat
+            // uses JVM default)
+            Locale.setDefault(l);
+            final String df = new SimpleDateFormat().toPattern();
+            int timeStart = df.indexOf("H");
+            if (timeStart < 0) {
+                timeStart = df.indexOf("h");
+            }
+            final int ampm_first = df.indexOf("a");
+            // E.g. in Korean locale AM/PM is before h:mm
+            // TODO should take that into consideration on client-side as well,
+            // now always h:mm a
+            if (ampm_first > 0 && ampm_first < timeStart) {
+                timeStart = ampm_first;
+            }
+            final String dateformat = df.substring(0, timeStart - 1);
+
+            outWriter.print("\"df\":\"" + dateformat.trim() + "\",");
+
+            /*
+             * Time formatting (24 or 12 hour clock and AM/PM suffixes)
+             */
+            final String timeformat = df.substring(timeStart, df.length()); // Doesn't
+            // return
+            // second
+            // or
+            // milliseconds
+            // We use timeformat to determine 12/24-hour clock
+            final boolean twelve_hour_clock = timeformat.indexOf("a") > -1;
+            // TODO there are other possibilities as well, like 'h' in french
+            // (ignore them, too complicated)
+            final String hour_min_delimiter = timeformat.indexOf(".") > -1 ? "."
+                    : ":";
+            // outWriter.print("\"tf\":\"" + timeformat + "\",");
+            outWriter.print("\"thc\":" + twelve_hour_clock + ",");
+            outWriter.print("\"hmd\":\"" + hour_min_delimiter + "\"");
+            if (twelve_hour_clock) {
+                final String[] ampm = dfs.getAmPmStrings();
+                outWriter.print(",\"ampm\":[\"" + ampm[0] + "\",\"" + ampm[1]
+                        + "\"]");
+            }
+            outWriter.print("}");
+            if (pendingLocalesIndex < locales.size() - 1) {
+                outWriter.print(",");
+            }
+        }
+        outWriter.print("]"); // Close locales
+
+        // Restore JVM default locale
+        Locale.setDefault(jvmDefault);
+    }
+
+    /**
+     * Gets the existing application or create a new one. Get a window within an
+     * application based on the requested URI.
+     * 
+     * @param request
+     *                the HTTP Request.
+     * @param application
+     *                the Application to query for window.
+     * @return Window mathing the given URI or null if not found.
+     * @throws ServletException
+     *                 if an exception has occurred that interferes with the
+     *                 servlet's normal operation.
+     */
+    private Window getApplicationWindow(HttpServletRequest request,
+            Application application) throws ServletException {
+
+        Window window = null;
+
+        // Find the window where the request is handled
+        String path = request.getPathInfo();
+
+        // Remove UIDL from the path
+        path = path.substring("/UIDL".length());
+
+        // Main window as the URI is empty
+        if (path == null || path.length() == 0 || path.equals("/")) {
+            window = application.getMainWindow();
+        } else {
+            String windowName = null;
+            if (path.charAt(0) == '/') {
+                path = path.substring(1);
+            }
+            final int index = path.indexOf('/');
+            if (index < 0) {
+                windowName = path;
+                path = "";
+            } else {
+                windowName = path.substring(0, index);
+                path = path.substring(index + 1);
+            }
+            window = application.getWindow(windowName);
+
+            // By default, we use main window
+            if (window == null) {
+                window = application.getMainWindow();
+            }
+        }
+
+        return window;
+    }
+
+    /**
+     * Ends the Application.
+     * 
+     * @param request
+     *                the HTTP request instance.
+     * @param response
+     *                the HTTP response to write to.
+     * @param application
+     *                the Application to end.
+     * @throws IOException
+     *                 if the writing failed due to input/output error.
+     */
+    private void endApplication(HttpServletRequest request,
+            HttpServletResponse response, Application application)
+            throws IOException {
+
+        String logoutUrl = application.getLogoutURL();
+        if (logoutUrl == null) {
+            logoutUrl = application.getURL().toString();
+        }
+        // clients JS app is still running, send a special json file to
+        // tell client that application has quit and where to point browser now
+        // Set the response type
+        response.setContentType("application/json; charset=UTF-8");
+        final ServletOutputStream out = response.getOutputStream();
+        final PrintWriter outWriter = new PrintWriter(new BufferedWriter(
+                new OutputStreamWriter(out, "UTF-8")));
+        outWriter.print("for(;;);[{");
+        outWriter.print("\"redirect\":{");
+        outWriter.write("\"url\":\"" + logoutUrl + "\"}}]");
+        outWriter.flush();
+        outWriter.close();
+        out.flush();
+    }
+
+    /**
+     * Gets the Paintable Id. If Paintable has debug id set it will be used
+     * prefixed with "PID_S". Otherwise a sequenced ID is created.
+     * 
+     * @param paintable
+     * @return the paintable Id.
+     */
+    public String getPaintableId(Paintable paintable) {
+
+        String id = (String) paintableIdMap.get(paintable);
+        if (id == null) {
+            // use testing identifier as id if set
+            id = paintable.getDebugId();
+            if (id == null) {
+                id = "PID" + Integer.toString(idSequence++);
+            } else {
+                id = "PID_S" + id;
+            }
+            paintableIdMap.put(paintable, id);
+            idPaintableMap.put(id, paintable);
+        }
+
+        return id;
+    }
+
+    public boolean hasPaintableId(Paintable paintable) {
+        return paintableIdMap.containsKey(paintable);
+    }
+
+    /**
+     * @param w
+     *                root window for which dirty components is to be fetched
+     * @return
+     */
+    private ArrayList getDirtyComponents(Window w) {
+        final ArrayList resultset = new ArrayList(dirtyPaintabletSet);
+
+        // The following algorithm removes any components that would be painted
+        // as
+        // a direct descendant of other components from the dirty components
+        // list.
+        // The result is that each component should be painted exactly once and
+        // any unmodified components will be painted as "cached=true".
+
+        for (final Iterator i = dirtyPaintabletSet.iterator(); i.hasNext();) {
+            final Paintable p = (Paintable) i.next();
+            if (p instanceof Component) {
+                final Component component = (Component) p;
+                if (component.getApplication() == null) {
+                    // component is detached after requestRepaint is called
+                    resultset.remove(p);
+                    i.remove();
+                } else {
+                    Window componentsRoot = component.getWindow();
+                    if (componentsRoot.getParent() != null) {
+                        // this is a subwindow
+                        componentsRoot = (Window) componentsRoot.getParent();
+                    }
+                    if (componentsRoot != w) {
+                        resultset.remove(p);
+                    }
+                }
+            }
+        }
+
+        return resultset;
+    }
+
+    /**
+     * @see com.itmill.toolkit.terminal.Paintable.RepaintRequestListener#repaintRequested(com.itmill.toolkit.terminal.Paintable.RepaintRequestEvent)
+     */
+    public void repaintRequested(RepaintRequestEvent event) {
+        final Paintable p = event.getPaintable();
+        if (!dirtyPaintabletSet.contains(p)) {
+            dirtyPaintabletSet.add(p);
+        }
+    }
+
+    /**
+     * 
+     * @param p
+     */
+    private void paintablePainted(Paintable p) {
+        dirtyPaintabletSet.remove(p);
+        p.requestRepaintRequests();
+    }
+
+    private final class SingleValueMap implements Map {
+        private final String name;
+
+        private final Object value;
+
+        private SingleValueMap(String name, Object value) {
+            this.name = name;
+            this.value = value;
+        }
+
+        public void clear() {
+            throw new UnsupportedOperationException();
+        }
+
+        public boolean containsKey(Object key) {
+            if (name == null) {
+                return key == null;
+            }
+            return name.equals(key);
+        }
+
+        public boolean containsValue(Object v) {
+            if (value == null) {
+                return v == null;
+            }
+            return value.equals(v);
+        }
+
+        public Set entrySet() {
+            final Set s = new HashSet();
+            s.add(new Map.Entry() {
+
+                public Object getKey() {
+                    return name;
+                }
+
+                public Object getValue() {
+                    return value;
+                }
+
+                public Object setValue(Object value) {
+                    throw new UnsupportedOperationException();
+                }
+            });
+            return s;
+        }
+
+        public Object get(Object key) {
+            if (!name.equals(key)) {
+                return null;
+            }
+            return value;
+        }
+
+        public boolean isEmpty() {
+            return false;
+        }
+
+        public Set keySet() {
+            final Set s = new HashSet();
+            s.add(name);
+            return s;
+        }
+
+        public Object put(Object key, Object value) {
+            throw new UnsupportedOperationException();
+        }
+
+        public void putAll(Map t) {
+            throw new UnsupportedOperationException();
+        }
+
+        public Object remove(Object key) {
+            throw new UnsupportedOperationException();
+        }
+
+        public int size() {
+            return 1;
+        }
+
+        public Collection values() {
+            final LinkedList s = new LinkedList();
+            s.add(value);
+            return s;
+
+        }
+    }
+
+    /**
+     * Implementation of URIHandler.ErrorEvent interface.
+     */
+    public class URIHandlerErrorImpl implements URIHandler.ErrorEvent {
+
+        private final URIHandler owner;
+
+        private final Throwable throwable;
+
+        /**
+         * 
+         * @param owner
+         * @param throwable
+         */
+        private URIHandlerErrorImpl(URIHandler owner, Throwable throwable) {
+            this.owner = owner;
+            this.throwable = throwable;
+        }
+
+        /**
+         * @see com.itmill.toolkit.terminal.Terminal.ErrorEvent#getThrowable()
+         */
+        public Throwable getThrowable() {
+            return throwable;
+        }
+
+        /**
+         * @see com.itmill.toolkit.terminal.URIHandler.ErrorEvent#getURIHandler()
+         */
+        public URIHandler getURIHandler() {
+            return owner;
+        }
+    }
+
+    public void requireLocale(String value) {
+        if (locales == null) {
+            locales = new ArrayList();
+            locales.add(application.getLocale().toString());
+            pendingLocalesIndex = 0;
+        }
+        if (!locales.contains(value)) {
+            locales.add(value);
+        }
+    }
+
+    private Locale generateLocale(String value) {
+        final String[] temp = value.split("_");
+        if (temp.length == 1) {
+            return new Locale(temp[0]);
+        } else if (temp.length == 2) {
+            return new Locale(temp[0], temp[1]);
+        } else {
+            return new Locale(temp[0], temp[1], temp[2]);
+        }
+    }
+
+    /*
+     * Upload progress listener notifies upload component once when Jakarta
+     * FileUpload can determine content length. Used to detect files total size,
+     * uploads progress can be tracked inside upload.
+     */
+    private class UploadProgressListener implements ProgressListener {
+        Upload uploadComponent;
+
+        boolean updated = false;
+
+        public void setUpload(Upload u) {
+            uploadComponent = u;
+        }
+
+        public void update(long bytesRead, long contentLength, int items) {
+            if (!updated && uploadComponent != null) {
+                uploadComponent.setUploadSize(contentLength);
+                updated = true;
+            }
+        }
+    }
+
+    /**
+     * Helper method to test if a component contains another
+     * 
+     * @param parent
+     * @param child
+     */
+    private static boolean isChildOf(Component parent, Component child) {
+        Component p = child.getParent();
+        while (p != null) {
+            if (parent == p) {
+                return true;
+            }
+            p = p.getParent();
+        }
+        return false;
+    }
+
+}
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/server/HttpUploadStream.java b/src/com/itmill/toolkit/terminal/terminal/gwt/server/HttpUploadStream.java
new file mode 100644 (file)
index 0000000..2888907
--- /dev/null
@@ -0,0 +1,90 @@
+/* 
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.itmill.toolkit.terminal.gwt.server;
+
+import java.io.InputStream;
+
+/**
+ * AjaxAdapter implementation of the UploadStream interface.
+ * 
+ * @author IT Mill Ltd.
+ * @version
+ * @VERSION@
+ * @since 5.0
+ */
+public class HttpUploadStream implements
+        com.itmill.toolkit.terminal.UploadStream {
+
+    /**
+     * Holds value of property variableName.
+     */
+    private final String streamName;
+
+    private final String contentName;
+
+    private final String contentType;
+
+    /**
+     * Holds value of property variableValue.
+     */
+    private final InputStream stream;
+
+    /**
+     * Creates a new instance of UploadStreamImpl.
+     * 
+     * @param name
+     *                the name of the stream.
+     * @param stream
+     *                the input stream.
+     * @param contentName
+     *                the name of the content.
+     * @param contentType
+     *                the type of the content.
+     */
+    public HttpUploadStream(String name, InputStream stream,
+            String contentName, String contentType) {
+        streamName = name;
+        this.stream = stream;
+        this.contentName = contentName;
+        this.contentType = contentType;
+    }
+
+    /**
+     * Gets the name of the stream.
+     * 
+     * @return the name of the stream.
+     */
+    public String getStreamName() {
+        return streamName;
+    }
+
+    /**
+     * Gets the input stream.
+     * 
+     * @return the Input stream.
+     */
+    public InputStream getStream() {
+        return stream;
+    }
+
+    /**
+     * Gets the input stream content type.
+     * 
+     * @return the content type of the input stream.
+     */
+    public String getContentType() {
+        return contentType;
+    }
+
+    /**
+     * Gets the stream content name. Stream content name usually differs from
+     * the actual stream name. It is used to identify the content of the stream.
+     * 
+     * @return the Name of the stream content.
+     */
+    public String getContentName() {
+        return contentName;
+    }
+}
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/server/JsonPaintTarget.java b/src/com/itmill/toolkit/terminal/terminal/gwt/server/JsonPaintTarget.java
new file mode 100644 (file)
index 0000000..6c62dbb
--- /dev/null
@@ -0,0 +1,1082 @@
+/* 
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.itmill.toolkit.terminal.gwt.server;
+
+import java.io.PrintWriter;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Set;
+import java.util.Stack;
+import java.util.Vector;
+
+import com.itmill.toolkit.Application;
+import com.itmill.toolkit.terminal.ApplicationResource;
+import com.itmill.toolkit.terminal.ExternalResource;
+import com.itmill.toolkit.terminal.PaintException;
+import com.itmill.toolkit.terminal.PaintTarget;
+import com.itmill.toolkit.terminal.Paintable;
+import com.itmill.toolkit.terminal.Resource;
+import com.itmill.toolkit.terminal.ThemeResource;
+import com.itmill.toolkit.terminal.VariableOwner;
+import com.itmill.toolkit.ui.Component;
+
+/**
+ * User Interface Description Language Target.
+ * 
+ * @author IT Mill Ltd.
+ * @version
+ * @VERSION@
+ * @since 5.0
+ */
+public class JsonPaintTarget implements PaintTarget {
+
+    /* Document type declarations */
+
+    private final static String UIDL_ARG_NAME = "name";
+
+    private final Stack mOpenTags;
+
+    private final Stack openJsonTags;
+
+    private final PrintWriter uidlBuffer;
+
+    private boolean closed = false;
+
+    private final CommunicationManager manager;
+
+    private int changes = 0;
+
+    Set preCachedResources = new HashSet();
+
+    private boolean customLayoutArgumentsOpen = false;
+
+    private JsonTag tag;
+
+    private int errorsOpen;
+
+    private boolean cacheEnabled = false;
+
+    private Collection paintedComponents = new HashSet();
+
+    /**
+     * Creates a new XMLPrintWriter, without automatic line flushing.
+     * 
+     * @param variableMap
+     * @param manager
+     * @param outWriter
+     *                A character-output stream.
+     * @throws PaintException
+     *                 if the paint operation failed.
+     */
+    public JsonPaintTarget(CommunicationManager manager, PrintWriter outWriter,
+            boolean cachingRequired) throws PaintException {
+
+        this.manager = manager;
+
+        // Sets the target for UIDL writing
+        uidlBuffer = outWriter;
+
+        // Initialize tag-writing
+        mOpenTags = new Stack();
+        openJsonTags = new Stack();
+        cacheEnabled = cachingRequired;
+    }
+
+    public void startTag(String tagName) throws PaintException {
+        startTag(tagName, false);
+    }
+
+    /**
+     * Prints the element start tag.
+     * 
+     * <pre>
+     *   Todo:
+     *    Checking of input values
+     *    
+     * </pre>
+     * 
+     * @param tagName
+     *                the name of the start tag.
+     * @throws PaintException
+     *                 if the paint operation failed.
+     * 
+     */
+    public void startTag(String tagName, boolean isChildNode)
+            throws PaintException {
+        // In case of null data output nothing:
+        if (tagName == null) {
+            throw new NullPointerException();
+        }
+
+        // Ensures that the target is open
+        if (closed) {
+            throw new PaintException(
+                    "Attempted to write to a closed PaintTarget.");
+        }
+
+        if (tag != null) {
+            openJsonTags.push(tag);
+        }
+        // Checks tagName and attributes here
+        mOpenTags.push(tagName);
+
+        tag = new JsonTag(tagName);
+
+        customLayoutArgumentsOpen = "customlayout".equals(tagName);
+
+        if ("error".equals(tagName)) {
+            errorsOpen++;
+        }
+    }
+
+    /**
+     * Prints the element end tag.
+     * 
+     * If the parent tag is closed before every child tag is closed an
+     * PaintException is raised.
+     * 
+     * @param tag
+     *                the name of the end tag.
+     * @throws Paintexception
+     *                 if the paint operation failed.
+     */
+    public void endTag(String tagName) throws PaintException {
+        // In case of null data output nothing:
+        if (tagName == null) {
+            throw new NullPointerException();
+        }
+
+        // Ensure that the target is open
+        if (closed) {
+            throw new PaintException(
+                    "Attempted to write to a closed PaintTarget.");
+        }
+
+        if (openJsonTags.size() > 0) {
+            final JsonTag parent = (JsonTag) openJsonTags.pop();
+
+            String lastTag = "";
+
+            lastTag = (String) mOpenTags.pop();
+            if (!tagName.equalsIgnoreCase(lastTag)) {
+                throw new PaintException("Invalid UIDL: wrong ending tag: '"
+                        + tagName + "' expected: '" + lastTag + "'.");
+            }
+
+            // simple hack which writes error uidl structure into attribute
+            if ("error".equals(lastTag)) {
+                if (errorsOpen == 1) {
+                    parent.addAttribute("\"error\":[\"error\",{}"
+                            + tag.getData() + "]");
+                } else {
+                    // sub error
+                    parent.addData(tag.getJSON());
+                }
+                errorsOpen--;
+            } else {
+                parent.addData(tag.getJSON());
+            }
+
+            tag = parent;
+        } else {
+            changes++;
+            uidlBuffer.print(((changes > 1) ? "," : "") + tag.getJSON());
+            tag = null;
+        }
+    }
+
+    /**
+     * Substitutes the XML sensitive characters with predefined XML entities.
+     * 
+     * @param xml
+     *                the String to be substituted.
+     * @return A new string instance where all occurrences of XML sensitive
+     *         characters are substituted with entities.
+     */
+    static public String escapeXML(String xml) {
+        if (xml == null || xml.length() <= 0) {
+            return "";
+        }
+        return escapeXML(new StringBuffer(xml)).toString();
+    }
+
+    /**
+     * Substitutes the XML sensitive characters with predefined XML entities.
+     * 
+     * @param xml
+     *                the String to be substituted.
+     * @return A new StringBuffer instance where all occurrences of XML
+     *         sensitive characters are substituted with entities.
+     * 
+     */
+    static public StringBuffer escapeXML(StringBuffer xml) {
+        if (xml == null || xml.length() <= 0) {
+            return new StringBuffer("");
+        }
+
+        final StringBuffer result = new StringBuffer(xml.length() * 2);
+
+        for (int i = 0; i < xml.length(); i++) {
+            final char c = xml.charAt(i);
+            final String s = toXmlChar(c);
+            if (s != null) {
+                result.append(s);
+            } else {
+                result.append(c);
+            }
+        }
+        return result;
+    }
+
+    static public String escapeJSON(String s) {
+        if (s == null) {
+            return "";
+        }
+        final StringBuffer sb = new StringBuffer();
+        for (int i = 0; i < s.length(); i++) {
+            final char ch = s.charAt(i);
+            switch (ch) {
+            case '"':
+                sb.append("\\\"");
+                break;
+            case '\\':
+                sb.append("\\\\");
+                break;
+            case '\b':
+                sb.append("\\b");
+                break;
+            case '\f':
+                sb.append("\\f");
+                break;
+            case '\n':
+                sb.append("\\n");
+                break;
+            case '\r':
+                sb.append("\\r");
+                break;
+            case '\t':
+                sb.append("\\t");
+                break;
+            case '/':
+                sb.append("\\/");
+                break;
+            default:
+                if (ch >= '\u0000' && ch <= '\u001F') {
+                    final String ss = Integer.toHexString(ch);
+                    sb.append("\\u");
+                    for (int k = 0; k < 4 - ss.length(); k++) {
+                        sb.append('0');
+                    }
+                    sb.append(ss.toUpperCase());
+                } else {
+                    sb.append(ch);
+                }
+            }
+        }
+        return sb.toString();
+    }
+
+    /**
+     * Substitutes a XML sensitive character with predefined XML entity.
+     * 
+     * @param c
+     *                the Character to be replaced with an entity.
+     * @return String of the entity or null if character is not to be replaced
+     *         with an entity.
+     */
+    private static String toXmlChar(char c) {
+        switch (c) {
+        case '&':
+            return "&amp;"; // & => &amp;
+        case '>':
+            return "&gt;"; // > => &gt;
+        case '<':
+            return "&lt;"; // < => &lt;
+        case '"':
+            return "&quot;"; // " => &quot;
+        case '\'':
+            return "&apos;"; // ' => &apos;
+        default:
+            return null;
+        }
+    }
+
+    /**
+     * Prints XML-escaped text.
+     * 
+     * @param str
+     * @throws PaintException
+     *                 if the paint operation failed.
+     * 
+     */
+    public void addText(String str) throws PaintException {
+        tag.addData("\"" + escapeJSON(str) + "\"");
+    }
+
+    /**
+     * Adds a boolean attribute to component. Atributes must be added before any
+     * content is written.
+     * 
+     * @param name
+     *                the Attribute name.
+     * @param value
+     *                the Attribute value.
+     * @throws PaintException
+     *                 if the paint operation failed.
+     */
+    public void addAttribute(String name, boolean value) throws PaintException {
+        tag.addAttribute("\"" + name + "\":" + (value ? "true" : "false"));
+    }
+
+    /**
+     * Adds a resource attribute to component. Attributes must be added before
+     * any content is written.
+     * 
+     * @param name
+     *                the Attribute name.
+     * @param value
+     *                the Attribute value.
+     * 
+     * @throws PaintException
+     *                 if the paint operation failed.
+     */
+    public void addAttribute(String name, Resource value) throws PaintException {
+
+        if (value instanceof ExternalResource) {
+            addAttribute(name, ((ExternalResource) value).getURL());
+
+        } else if (value instanceof ApplicationResource) {
+            final ApplicationResource r = (ApplicationResource) value;
+            final Application a = r.getApplication();
+            if (a == null) {
+                throw new PaintException(
+                        "Application not specified for resorce "
+                                + value.getClass().getName());
+            }
+            String uri;
+            if (a.getURL() != null) {
+                uri = a.getURL().getPath();
+            } else {
+                uri = "";
+            }
+            if (uri.length() > 0 && uri.charAt(uri.length() - 1) != '/') {
+                uri += "/";
+            }
+            uri += a.getRelativeLocation(r);
+            addAttribute(name, uri);
+
+        } else if (value instanceof ThemeResource) {
+            final String uri = "theme://"
+                    + ((ThemeResource) value).getResourceId();
+            addAttribute(name, uri);
+        } else {
+            throw new PaintException("Ajax adapter does not "
+                    + "support resources of type: "
+                    + value.getClass().getName());
+        }
+
+    }
+
+    /**
+     * Adds a integer attribute to component. Atributes must be added before any
+     * content is written.
+     * 
+     * @param name
+     *                the Attribute name.
+     * @param value
+     *                the Attribute value.
+     * 
+     * @throws PaintException
+     *                 if the paint operation failed.
+     */
+    public void addAttribute(String name, int value) throws PaintException {
+        tag.addAttribute("\"" + name + "\":" + String.valueOf(value));
+    }
+
+    /**
+     * Adds a long attribute to component. Atributes must be added before any
+     * content is written.
+     * 
+     * @param name
+     *                the Attribute name.
+     * @param value
+     *                the Attribute value.
+     * 
+     * @throws PaintException
+     *                 if the paint operation failed.
+     */
+    public void addAttribute(String name, long value) throws PaintException {
+        tag.addAttribute("\"" + name + "\":" + String.valueOf(value));
+    }
+
+    /**
+     * Adds a float attribute to component. Atributes must be added before any
+     * content is written.
+     * 
+     * @param name
+     *                the Attribute name.
+     * @param value
+     *                the Attribute value.
+     * 
+     * @throws PaintException
+     *                 if the paint operation failed.
+     */
+    public void addAttribute(String name, float value) throws PaintException {
+        tag.addAttribute("\"" + name + "\":" + String.valueOf(value));
+    }
+
+    /**
+     * Adds a double attribute to component. Atributes must be added before any
+     * content is written.
+     * 
+     * @param name
+     *                the Attribute name.
+     * @param value
+     *                the Attribute value.
+     * 
+     * @throws PaintException
+     *                 if the paint operation failed.
+     */
+    public void addAttribute(String name, double value) throws PaintException {
+        tag.addAttribute("\"" + name + "\":" + String.valueOf(value));
+    }
+
+    /**
+     * Adds a string attribute to component. Atributes must be added before any
+     * content is written.
+     * 
+     * @param name
+     *                the String attribute name.
+     * @param value
+     *                the String attribute value.
+     * 
+     * @throws PaintException
+     *                 if the paint operation failed.
+     */
+    public void addAttribute(String name, String value) throws PaintException {
+        // In case of null data output nothing:
+        if ((value == null) || (name == null)) {
+            throw new NullPointerException(
+                    "Parameters must be non-null strings");
+        }
+
+        tag.addAttribute("\"" + name + "\": \"" + escapeJSON(value) + "\"");
+
+        if (customLayoutArgumentsOpen && "template".equals(name)) {
+            getPreCachedResources().add("layouts/" + value + ".html");
+        }
+
+        if (name.equals("locale")) {
+            manager.requireLocale(value);
+        }
+
+    }
+
+    public void addAttribute(String name, Object[] values) {
+        // In case of null data output nothing:
+        if ((values == null) || (name == null)) {
+            throw new NullPointerException(
+                    "Parameters must be non-null strings");
+        }
+        final StringBuffer buf = new StringBuffer();
+        buf.append("\"" + name + "\":[");
+        for (int i = 0; i < values.length; i++) {
+            if (i > 0) {
+                buf.append(",");
+            }
+            buf.append("\"");
+            buf.append(escapeJSON(values[i].toString()));
+            buf.append("\"");
+        }
+        buf.append("]");
+        tag.addAttribute(buf.toString());
+    }
+
+    /**
+     * Adds a string type variable.
+     * 
+     * @param owner
+     *                the Listener for variable changes.
+     * @param name
+     *                the Variable name.
+     * @param value
+     *                the Variable initial value.
+     * 
+     * @throws PaintException
+     *                 if the paint operation failed.
+     */
+    public void addVariable(VariableOwner owner, String name, String value)
+            throws PaintException {
+        tag.addVariable(new StringVariable(owner, name, escapeJSON(value)));
+    }
+
+    /**
+     * Adds a int type variable.
+     * 
+     * @param owner
+     *                the Listener for variable changes.
+     * @param name
+     *                the Variable name.
+     * @param value
+     *                the Variable initial value.
+     * 
+     * @throws PaintException
+     *                 if the paint operation failed.
+     */
+    public void addVariable(VariableOwner owner, String name, int value)
+            throws PaintException {
+        tag.addVariable(new IntVariable(owner, name, value));
+    }
+
+    /**
+     * Adds a long type variable.
+     * 
+     * @param owner
+     *                the Listener for variable changes.
+     * @param name
+     *                the Variable name.
+     * @param value
+     *                the Variable initial value.
+     * 
+     * @throws PaintException
+     *                 if the paint operation failed.
+     */
+    public void addVariable(VariableOwner owner, String name, long value)
+            throws PaintException {
+        tag.addVariable(new LongVariable(owner, name, value));
+    }
+
+    /**
+     * Adds a float type variable.
+     * 
+     * @param owner
+     *                the Listener for variable changes.
+     * @param name
+     *                the Variable name.
+     * @param value
+     *                the Variable initial value.
+     * 
+     * @throws PaintException
+     *                 if the paint operation failed.
+     */
+    public void addVariable(VariableOwner owner, String name, float value)
+            throws PaintException {
+        tag.addVariable(new FloatVariable(owner, name, value));
+    }
+
+    /**
+     * Adds a double type variable.
+     * 
+     * @param owner
+     *                the Listener for variable changes.
+     * @param name
+     *                the Variable name.
+     * @param value
+     *                the Variable initial value.
+     * 
+     * @throws PaintException
+     *                 if the paint operation failed.
+     */
+    public void addVariable(VariableOwner owner, String name, double value)
+            throws PaintException {
+        tag.addVariable(new DoubleVariable(owner, name, value));
+    }
+
+    /**
+     * Adds a boolean type variable.
+     * 
+     * @param owner
+     *                the Listener for variable changes.
+     * @param name
+     *                the Variable name.
+     * @param value
+     *                the Variable initial value.
+     * 
+     * @throws PaintException
+     *                 if the paint operation failed.
+     */
+    public void addVariable(VariableOwner owner, String name, boolean value)
+            throws PaintException {
+        tag.addVariable(new BooleanVariable(owner, name, value));
+    }
+
+    /**
+     * Adds a string array type variable.
+     * 
+     * @param owner
+     *                the Listener for variable changes.
+     * @param name
+     *                the Variable name.
+     * @param value
+     *                the Variable initial value.
+     * 
+     * @throws PaintException
+     *                 if the paint operation failed.
+     */
+    public void addVariable(VariableOwner owner, String name, String[] value)
+            throws PaintException {
+        tag.addVariable(new ArrayVariable(owner, name, value));
+    }
+
+    /**
+     * Adds a upload stream type variable.
+     * 
+     * TODO not converted for JSON
+     * 
+     * @param owner
+     *                the Listener for variable changes.
+     * @param name
+     *                the Variable name.
+     * 
+     * @throws PaintException
+     *                 if the paint operation failed.
+     */
+    public void addUploadStreamVariable(VariableOwner owner, String name)
+            throws PaintException {
+        startTag("uploadstream");
+        addAttribute(UIDL_ARG_NAME, name);
+        endTag("uploadstream");
+    }
+
+    /**
+     * Prints the single text section.
+     * 
+     * Prints full text section. The section data is escaped
+     * 
+     * @param sectionTagName
+     *                the name of the tag.
+     * @param sectionData
+     *                the section data to be printed.
+     * @throws PaintException
+     *                 if the paint operation failed.
+     */
+    public void addSection(String sectionTagName, String sectionData)
+            throws PaintException {
+        tag.addData("{\"" + sectionTagName + "\":\"" + escapeJSON(sectionData)
+                + "\"}");
+    }
+
+    /**
+     * Adds XML directly to UIDL.
+     * 
+     * @param xml
+     *                the Xml to be added.
+     * @throws PaintException
+     *                 if the paint operation failed.
+     */
+    public void addUIDL(String xml) throws PaintException {
+
+        // Ensure that the target is open
+        if (closed) {
+            throw new PaintException(
+                    "Attempted to write to a closed PaintTarget.");
+        }
+
+        // Make sure that the open start tag is closed before
+        // anything is written.
+
+        // Escape and write what was given
+        if (xml != null) {
+            tag.addData("\"" + escapeJSON(xml) + "\"");
+        }
+
+    }
+
+    /**
+     * Adds XML section with namespace.
+     * 
+     * @param sectionTagName
+     *                the name of the tag.
+     * @param sectionData
+     *                the section data.
+     * @param namespace
+     *                the namespace to be added.
+     * @throws PaintException
+     *                 if the paint operation failed.
+     * 
+     * @see com.itmill.toolkit.terminal.PaintTarget#addXMLSection(String,
+     *      String, String)
+     */
+    public void addXMLSection(String sectionTagName, String sectionData,
+            String namespace) throws PaintException {
+
+        // Ensure that the target is open
+        if (closed) {
+            throw new PaintException(
+                    "Attempted to write to a closed PaintTarget.");
+        }
+
+        startTag(sectionTagName);
+        if (namespace != null) {
+            addAttribute("xmlns", namespace);
+        }
+        customLayoutArgumentsOpen = false;
+
+        if (sectionData != null) {
+            tag.addData("\"" + escapeJSON(sectionData) + "\"");
+        }
+        endTag(sectionTagName);
+    }
+
+    /**
+     * Gets the UIDL already printed to stream. Paint target must be closed
+     * before the <code>getUIDL</code> can be called.
+     * 
+     * @return the UIDL.
+     */
+    public String getUIDL() {
+        if (closed) {
+            return uidlBuffer.toString();
+        }
+        throw new IllegalStateException(
+                "Tried to read UIDL from open PaintTarget");
+    }
+
+    /**
+     * Closes the paint target. Paint target must be closed before the
+     * <code>getUIDL</code> can be called. Subsequent attempts to write to
+     * paint target. If the target was already closed, call to this function is
+     * ignored. will generate an exception.
+     * 
+     * @throws PaintException
+     *                 if the paint operation failed.
+     */
+    public void close() throws PaintException {
+        if (tag != null) {
+            uidlBuffer.write(tag.getJSON());
+        }
+        flush();
+        closed = true;
+    }
+
+    /**
+     * Method flush.
+     */
+    private void flush() {
+        uidlBuffer.flush();
+    }
+
+    /**
+     * @see com.itmill.toolkit.terminal.PaintTarget#startTag(com.itmill.toolkit.terminal.Paintable,
+     *      java.lang.String)
+     */
+    public boolean startTag(Paintable paintable, String tagName)
+            throws PaintException {
+        startTag(tagName, true);
+        final boolean isPreviouslyPainted = manager.hasPaintableId(paintable);
+        final String id = manager.getPaintableId(paintable);
+        paintable.addListener(manager);
+        addAttribute("id", id);
+        paintedComponents.add(paintable);
+        return cacheEnabled && isPreviouslyPainted;
+    }
+
+    public void paintReference(Paintable paintable, String referenceName)
+            throws PaintException {
+        final String id = manager.getPaintableId(paintable);
+        addAttribute(referenceName, id);
+    }
+
+    /**
+     * @see com.itmill.toolkit.terminal.PaintTarget#addCharacterData(java.lang.String)
+     */
+    public void addCharacterData(String text) throws PaintException {
+        if (text != null) {
+            tag.addData(text);
+        }
+    }
+
+    /**
+     * This is basically a container for UI components variables, that will be
+     * added at the end of JSON object.
+     * 
+     * @author mattitahvonen
+     * 
+     */
+    class JsonTag {
+        boolean firstField = false;
+
+        Vector variables = new Vector();
+
+        Vector children = new Vector();
+
+        Vector attr = new Vector();
+
+        StringBuffer data = new StringBuffer();
+
+        public boolean childrenArrayOpen = false;
+
+        private boolean childNode = false;
+
+        private boolean tagClosed = false;
+
+        public JsonTag(String tagName) {
+            data.append("[\"" + tagName + "\"");
+        }
+
+        private void closeTag() {
+            if (!tagClosed) {
+                data.append(attributesAsJsonObject());
+                data.append(getData());
+                // Writes the end (closing) tag
+                data.append("]");
+                tagClosed = true;
+            }
+        }
+
+        public String getJSON() {
+            if (!tagClosed) {
+                closeTag();
+            }
+            return data.toString();
+        }
+
+        public void openChildrenArray() {
+            if (!childrenArrayOpen) {
+                // append("c : [");
+                childrenArrayOpen = true;
+                // firstField = true;
+            }
+        }
+
+        public void closeChildrenArray() {
+            // append("]");
+            // firstField = false;
+        }
+
+        public void setChildNode(boolean b) {
+            childNode = b;
+        }
+
+        public boolean isChildNode() {
+            return childNode;
+        }
+
+        public String startField() {
+            if (firstField) {
+                firstField = false;
+                return "";
+            } else {
+                return ",";
+            }
+        }
+
+        /**
+         * 
+         * @param s
+         *                json string, object or array
+         */
+        public void addData(String s) {
+            children.add(s);
+        }
+
+        public String getData() {
+            final StringBuffer buf = new StringBuffer();
+            final Iterator it = children.iterator();
+            while (it.hasNext()) {
+                buf.append(startField());
+                buf.append(it.next());
+            }
+            return buf.toString();
+        }
+
+        public void addAttribute(String jsonNode) {
+            attr.add(jsonNode);
+        }
+
+        private String attributesAsJsonObject() {
+            final StringBuffer buf = new StringBuffer();
+            buf.append(startField());
+            buf.append("{");
+            for (final Iterator iter = attr.iterator(); iter.hasNext();) {
+                final String element = (String) iter.next();
+                buf.append(element);
+                if (iter.hasNext()) {
+                    buf.append(",");
+                }
+            }
+            buf.append(tag.variablesAsJsonObject());
+            buf.append("}");
+            return buf.toString();
+        }
+
+        public void addVariable(Variable v) {
+            variables.add(v);
+        }
+
+        private String variablesAsJsonObject() {
+            if (variables.size() == 0) {
+                return "";
+            }
+            final StringBuffer buf = new StringBuffer();
+            buf.append(startField());
+            buf.append("\"v\":{");
+            final Iterator iter = variables.iterator();
+            while (iter.hasNext()) {
+                final Variable element = (Variable) iter.next();
+                buf.append(element.getJsonPresentation());
+                if (iter.hasNext()) {
+                    buf.append(",");
+                }
+            }
+            buf.append("}");
+            return buf.toString();
+        }
+
+        class TagCounter {
+            int count;
+
+            public TagCounter() {
+                count = 0;
+            }
+
+            public void increment() {
+                count++;
+            }
+
+            public String postfix(String s) {
+                if (count > 0) {
+                    return s + count;
+                }
+                return s;
+            }
+        }
+    }
+
+    abstract class Variable {
+
+        String name;
+
+        public abstract String getJsonPresentation();
+    }
+
+    class BooleanVariable extends Variable {
+        boolean value;
+
+        public BooleanVariable(VariableOwner owner, String name, boolean v) {
+            value = v;
+            this.name = name;
+        }
+
+        public String getJsonPresentation() {
+            return "\"" + name + "\":" + (value == true ? "true" : "false");
+        }
+
+    }
+
+    class StringVariable extends Variable {
+        String value;
+
+        public StringVariable(VariableOwner owner, String name, String v) {
+            value = v;
+            this.name = name;
+        }
+
+        public String getJsonPresentation() {
+            return "\"" + name + "\":\"" + value + "\"";
+        }
+
+    }
+
+    class IntVariable extends Variable {
+        int value;
+
+        public IntVariable(VariableOwner owner, String name, int v) {
+            value = v;
+            this.name = name;
+        }
+
+        public String getJsonPresentation() {
+            return "\"" + name + "\":" + value;
+        }
+    }
+
+    class LongVariable extends Variable {
+        long value;
+
+        public LongVariable(VariableOwner owner, String name, long v) {
+            value = v;
+            this.name = name;
+        }
+
+        public String getJsonPresentation() {
+            return "\"" + name + "\":" + value;
+        }
+    }
+
+    class FloatVariable extends Variable {
+        float value;
+
+        public FloatVariable(VariableOwner owner, String name, float v) {
+            value = v;
+            this.name = name;
+        }
+
+        public String getJsonPresentation() {
+            return "\"" + name + "\":" + value;
+        }
+    }
+
+    class DoubleVariable extends Variable {
+        double value;
+
+        public DoubleVariable(VariableOwner owner, String name, double v) {
+            value = v;
+            this.name = name;
+        }
+
+        public String getJsonPresentation() {
+            return "\"" + name + "\":" + value;
+        }
+    }
+
+    class ArrayVariable extends Variable {
+        String[] value;
+
+        public ArrayVariable(VariableOwner owner, String name, String[] v) {
+            value = v;
+            this.name = name;
+        }
+
+        public String getJsonPresentation() {
+            String pres = "\"" + name + "\":[";
+            for (int i = 0; i < value.length;) {
+                pres += "\"" + value[i] + "\"";
+                i++;
+                if (i < value.length) {
+                    pres += ",";
+                }
+            }
+            pres += "]";
+            return pres;
+        }
+    }
+
+    public Set getPreCachedResources() {
+        return preCachedResources;
+    }
+
+    public void setPreCachedResources(Set preCachedResources) {
+        throw new UnsupportedOperationException();
+    }
+
+    /**
+     * Method to check if paintable is already painted into this target.
+     * 
+     * @param p
+     * @return true if is not yet painted into this target and is connected to
+     *         app
+     */
+    public boolean needsToBePainted(Paintable p) {
+        if (paintedComponents.contains(p)) {
+            return false;
+        } else if (((Component) p).getApplication() == null) {
+            return false;
+        } else {
+            return true;
+        }
+    }
+}
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/server/PortletApplicationContext.java b/src/com/itmill/toolkit/terminal/terminal/gwt/server/PortletApplicationContext.java
new file mode 100644 (file)
index 0000000..4ddbabd
--- /dev/null
@@ -0,0 +1,283 @@
+/**\r
+ * \r
+ */\r
+package com.itmill.toolkit.terminal.gwt.server;\r
+\r
+import java.io.IOException;\r
+import java.io.OutputStream;\r
+import java.io.PrintWriter;\r
+import java.util.HashMap;\r
+import java.util.Iterator;\r
+import java.util.LinkedHashSet;\r
+import java.util.Locale;\r
+import java.util.Map;\r
+import java.util.Set;\r
+\r
+import javax.portlet.ActionRequest;\r
+import javax.portlet.ActionResponse;\r
+import javax.portlet.Portlet;\r
+import javax.portlet.PortletSession;\r
+import javax.portlet.PortletURL;\r
+import javax.portlet.RenderRequest;\r
+import javax.portlet.RenderResponse;\r
+import javax.servlet.http.HttpSession;\r
+\r
+import com.itmill.toolkit.Application;\r
+\r
+/**\r
+ * @author marc\r
+ * \r
+ */\r
+public class PortletApplicationContext extends WebApplicationContext {\r
+\r
+    protected PortletSession portletSession;\r
+\r
+    protected Map portletListeners = new HashMap();\r
+\r
+    protected Map portletToApplication = new HashMap();\r
+\r
+    PortletApplicationContext() {\r
+\r
+    }\r
+\r
+    static public PortletApplicationContext getApplicationContext(\r
+            PortletSession session) {\r
+        WebApplicationContext cx = (WebApplicationContext) session\r
+                .getAttribute(WebApplicationContext.class.getName(),\r
+                        PortletSession.APPLICATION_SCOPE);\r
+        if (cx == null) {\r
+            cx = new PortletApplicationContext();\r
+        }\r
+        if (!(cx instanceof PortletApplicationContext)) {\r
+            // TODO Should we even try this? And should we leave original as-is?\r
+            PortletApplicationContext pcx = new PortletApplicationContext();\r
+            pcx.applications.addAll(cx.applications);\r
+            cx.applications.clear();\r
+            pcx.browser = cx.browser;\r
+            cx.browser = null;\r
+            pcx.listeners = cx.listeners;\r
+            cx.listeners = null;\r
+            pcx.session = cx.session;\r
+            cx = pcx;\r
+        }\r
+        if (((PortletApplicationContext) cx).portletSession == null) {\r
+            ((PortletApplicationContext) cx).portletSession = session;\r
+        }\r
+        session.setAttribute(WebApplicationContext.class.getName(), cx,\r
+                PortletSession.APPLICATION_SCOPE);\r
+        return (PortletApplicationContext) cx;\r
+    }\r
+\r
+    static public WebApplicationContext getApplicationContext(\r
+            HttpSession session) {\r
+        WebApplicationContext cx = (WebApplicationContext) session\r
+                .getAttribute(WebApplicationContext.class.getName());\r
+        if (cx == null) {\r
+            cx = new PortletApplicationContext();\r
+        }\r
+        if (cx.session == null) {\r
+            cx.session = session;\r
+        }\r
+        session.setAttribute(WebApplicationContext.class.getName(), cx);\r
+        return cx;\r
+    }\r
+\r
+    protected void removeApplication(Application application) {\r
+        portletListeners.remove(application);\r
+        for (Iterator it = portletToApplication.keySet().iterator(); it\r
+                .hasNext();) {\r
+            Object key = it.next();\r
+            if (key == application) {\r
+                portletToApplication.remove(key);\r
+            }\r
+        }\r
+        super.removeApplication(application);\r
+    }\r
+\r
+    public boolean equals(Object obj) {\r
+        if (portletSession == null) {\r
+            return super.equals(obj);\r
+        }\r
+        return portletSession.equals(obj);\r
+    }\r
+\r
+    public int hashCode() {\r
+        if (portletSession == null) {\r
+            return super.hashCode();\r
+        }\r
+        return portletSession.hashCode();\r
+    }\r
+\r
+    public void setPortletApplication(Portlet portlet, Application app) {\r
+        portletToApplication.put(portlet, app);\r
+    }\r
+\r
+    public Application getPortletApplication(Portlet portlet) {\r
+        return (Application) portletToApplication.get(portlet);\r
+    }\r
+\r
+    public PortletSession getPortletSession() {\r
+        return portletSession;\r
+    }\r
+\r
+    public void addPortletListener(Application app, PortletListener listener) {\r
+        Set l = (Set) portletListeners.get(app);\r
+        if (l == null) {\r
+            l = new LinkedHashSet();\r
+            portletListeners.put(app, l);\r
+        }\r
+        l.add(listener);\r
+    }\r
+\r
+    public void removePortletListener(Application app, PortletListener listener) {\r
+        Set l = (Set) portletListeners.get(app);\r
+        if (l != null) {\r
+            l.remove(listener);\r
+        }\r
+    }\r
+\r
+    public static void dispatchRequest(Portlet portlet, RenderRequest request,\r
+            RenderResponse response) {\r
+        PortletApplicationContext ctx = getApplicationContext(request\r
+                .getPortletSession());\r
+        if (ctx != null) {\r
+            ctx.firePortletRenderRequest(portlet, request, response);\r
+        }\r
+    }\r
+\r
+    public static void dispatchRequest(Portlet portlet, ActionRequest request,\r
+            ActionResponse response) {\r
+        PortletApplicationContext ctx = getApplicationContext(request\r
+                .getPortletSession());\r
+        if (ctx != null) {\r
+            ctx.firePortletActionRequest(portlet, request, response);\r
+        }\r
+    }\r
+\r
+    public void firePortletRenderRequest(Portlet portlet,\r
+            RenderRequest request, RenderResponse response) {\r
+        Application app = getPortletApplication(portlet);\r
+        Set listeners = (Set) portletListeners.get(app);\r
+        if (listeners != null) {\r
+            for (Iterator it = listeners.iterator(); it.hasNext();) {\r
+                PortletListener l = (PortletListener) it.next();\r
+                l.handleRenderRequest(request, new RestrictedRenderResponse(\r
+                        response));\r
+            }\r
+        }\r
+    }\r
+\r
+    public void firePortletActionRequest(Portlet portlet,\r
+            ActionRequest request, ActionResponse response) {\r
+        Application app = getPortletApplication(portlet);\r
+        Set listeners = (Set) portletListeners.get(app);\r
+        if (listeners != null) {\r
+            for (Iterator it = listeners.iterator(); it.hasNext();) {\r
+                PortletListener l = (PortletListener) it.next();\r
+                l.handleActionRequest(request, response);\r
+            }\r
+        }\r
+    }\r
+\r
+    public interface PortletListener {\r
+        public void handleRenderRequest(RenderRequest request,\r
+                RenderResponse response);\r
+\r
+        public void handleActionRequest(ActionRequest request,\r
+                ActionResponse response);\r
+    }\r
+\r
+    private class RestrictedRenderResponse implements RenderResponse {\r
+\r
+        private RenderResponse response;\r
+\r
+        private RestrictedRenderResponse(RenderResponse response) {\r
+            this.response = response;\r
+        }\r
+\r
+        public void addProperty(String key, String value) {\r
+            response.addProperty(key, value);\r
+        }\r
+\r
+        public PortletURL createActionURL() {\r
+            return response.createActionURL();\r
+        }\r
+\r
+        public PortletURL createRenderURL() {\r
+            return response.createRenderURL();\r
+        }\r
+\r
+        public String encodeURL(String path) {\r
+            return response.encodeURL(path);\r
+        }\r
+\r
+        public void flushBuffer() throws IOException {\r
+            // NOP\r
+            // TODO throw?\r
+        }\r
+\r
+        public int getBufferSize() {\r
+            return response.getBufferSize();\r
+        }\r
+\r
+        public String getCharacterEncoding() {\r
+            return response.getCharacterEncoding();\r
+        }\r
+\r
+        public String getContentType() {\r
+            return response.getContentType();\r
+        }\r
+\r
+        public Locale getLocale() {\r
+            return response.getLocale();\r
+        }\r
+\r
+        public String getNamespace() {\r
+            return response.getNamespace();\r
+        }\r
+\r
+        public OutputStream getPortletOutputStream() throws IOException {\r
+            // write forbidden\r
+            return null;\r
+        }\r
+\r
+        public PrintWriter getWriter() throws IOException {\r
+            // write forbidden\r
+            return null;\r
+        }\r
+\r
+        public boolean isCommitted() {\r
+            return response.isCommitted();\r
+        }\r
+\r
+        public void reset() {\r
+            // NOP\r
+            // TODO throw?\r
+        }\r
+\r
+        public void resetBuffer() {\r
+            // NOP\r
+            // TODO throw?\r
+        }\r
+\r
+        public void setBufferSize(int size) {\r
+            // NOP\r
+            // TODO throw?\r
+        }\r
+\r
+        public void setContentType(String type) {\r
+            // NOP\r
+            // TODO throw?\r
+        }\r
+\r
+        public void setProperty(String key, String value) {\r
+            response.setProperty(key, value);\r
+        }\r
+\r
+        public void setTitle(String title) {\r
+            response.setTitle(title);\r
+        }\r
+\r
+    }\r
+\r
+}\r
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/server/SessionExpired.java b/src/com/itmill/toolkit/terminal/terminal/gwt/server/SessionExpired.java
new file mode 100644 (file)
index 0000000..bd08572
--- /dev/null
@@ -0,0 +1,7 @@
+package com.itmill.toolkit.terminal.gwt.server;
+
+public class SessionExpired extends Exception {
+
+    private static final long serialVersionUID = -2211425033877155423L;
+
+}
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/server/WebApplicationContext.java b/src/com/itmill/toolkit/terminal/terminal/gwt/server/WebApplicationContext.java
new file mode 100644 (file)
index 0000000..6558789
--- /dev/null
@@ -0,0 +1,264 @@
+/* 
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.itmill.toolkit.terminal.gwt.server;
+
+import java.io.File;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpSession;
+import javax.servlet.http.HttpSessionBindingEvent;
+import javax.servlet.http.HttpSessionBindingListener;
+
+import com.itmill.toolkit.Application;
+import com.itmill.toolkit.service.ApplicationContext;
+
+/**
+ * Web application context for the IT Mill Toolkit applications.
+ * 
+ * @author IT Mill Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.1
+ */
+public class WebApplicationContext implements ApplicationContext,
+        HttpSessionBindingListener {
+
+    protected List listeners;
+
+    protected HttpSession session;
+
+    protected final HashSet applications = new HashSet();
+
+    protected WebBrowser browser = new WebBrowser();
+
+    /**
+     * Creates a new Web Application Context.
+     * 
+     */
+    WebApplicationContext() {
+
+    }
+
+    /**
+     * Gets the application context base directory.
+     * 
+     * @see com.itmill.toolkit.service.ApplicationContext#getBaseDirectory()
+     */
+    public File getBaseDirectory() {
+        final String realPath = ApplicationServlet.getResourcePath(session
+                .getServletContext(), "/");
+        if (realPath == null) {
+            return null;
+        }
+        return new File(realPath);
+    }
+
+    /**
+     * Gets the http-session application is running in.
+     * 
+     * @return HttpSession this application context resides in.
+     */
+    public HttpSession getHttpSession() {
+        return session;
+    }
+
+    /**
+     * Gets the applications in this context.
+     * 
+     * @see com.itmill.toolkit.service.ApplicationContext#getApplications()
+     */
+    public Collection getApplications() {
+        return Collections.unmodifiableCollection(applications);
+    }
+
+    /**
+     * Gets the application context for HttpSession.
+     * 
+     * @param session
+     *                the HTTP session.
+     * @return the application context for HttpSession.
+     */
+    static public WebApplicationContext getApplicationContext(
+            HttpSession session) {
+        WebApplicationContext cx = (WebApplicationContext) session
+                .getAttribute(WebApplicationContext.class.getName());
+        if (cx == null) {
+            cx = new WebApplicationContext();
+            session.setAttribute(WebApplicationContext.class.getName(), cx);
+        }
+        if (cx.session == null) {
+            cx.session = session;
+        }
+        return cx;
+    }
+
+    /**
+     * Returns <code>true</code> if and only if the argument is not
+     * <code>null</code> and is a Boolean object that represents the same
+     * boolean value as this object.
+     * 
+     * @param obj
+     *                the object to compare with.
+     * @see java.lang.Object#equals(java.lang.Object)
+     */
+    public boolean equals(Object obj) {
+        return session.equals(obj);
+    }
+
+    /**
+     * Returns the hash code value .
+     * 
+     * @see java.lang.Object#hashCode()
+     */
+    public int hashCode() {
+        return session.hashCode();
+    }
+
+    /**
+     * Adds the transaction listener to this context.
+     * 
+     * @see com.itmill.toolkit.service.ApplicationContext#addTransactionListener(com.itmill.toolkit.service.ApplicationContext.TransactionListener)
+     */
+    public void addTransactionListener(TransactionListener listener) {
+        if (listeners == null) {
+            listeners = new LinkedList();
+        }
+        listeners.add(listener);
+    }
+
+    /**
+     * Removes the transaction listener from this context.
+     * 
+     * @see com.itmill.toolkit.service.ApplicationContext#removeTransactionListener(com.itmill.toolkit.service.ApplicationContext.TransactionListener)
+     */
+    public void removeTransactionListener(TransactionListener listener) {
+        if (listeners != null) {
+            listeners.remove(listener);
+        }
+
+    }
+
+    /**
+     * Notifies the transaction start.
+     * 
+     * @param application
+     * @param request
+     *                the HTTP request.
+     */
+    protected void startTransaction(Application application,
+            HttpServletRequest request) {
+        if (listeners == null) {
+            return;
+        }
+        for (final Iterator i = listeners.iterator(); i.hasNext();) {
+            ((ApplicationContext.TransactionListener) i.next())
+                    .transactionStart(application, request);
+        }
+    }
+
+    /**
+     * Notifies the transaction end.
+     * 
+     * @param application
+     * @param request
+     *                the HTTP request.
+     */
+    protected void endTransaction(Application application,
+            HttpServletRequest request) {
+        if (listeners == null) {
+            return;
+        }
+
+        LinkedList exceptions = null;
+        for (final Iterator i = listeners.iterator(); i.hasNext();) {
+            try {
+                ((ApplicationContext.TransactionListener) i.next())
+                        .transactionEnd(application, request);
+            } catch (final RuntimeException t) {
+                if (exceptions == null) {
+                    exceptions = new LinkedList();
+                }
+                exceptions.add(t);
+            }
+        }
+
+        // If any runtime exceptions occurred, throw a combined exception
+        if (exceptions != null) {
+            final StringBuffer msg = new StringBuffer();
+            for (final Iterator i = exceptions.iterator(); i.hasNext();) {
+                final RuntimeException e = (RuntimeException) i.next();
+                if (msg.length() == 0) {
+                    msg.append("\n\n--------------------------\n\n");
+                }
+                msg.append(e.getMessage() + "\n");
+                final StringWriter trace = new StringWriter();
+                e.printStackTrace(new PrintWriter(trace, true));
+                msg.append(trace.toString());
+            }
+            throw new RuntimeException(msg.toString());
+        }
+    }
+
+    protected void removeApplication(Application application) {
+        applications.remove(application);
+    }
+
+    protected void addApplication(Application application) {
+        applications.add(application);
+    }
+
+    /**
+     * @see javax.servlet.http.HttpSessionBindingListener#valueBound(HttpSessionBindingEvent)
+     */
+    public void valueBound(HttpSessionBindingEvent arg0) {
+        // We are not interested in bindings
+    }
+
+    /**
+     * @see javax.servlet.http.HttpSessionBindingListener#valueUnbound(HttpSessionBindingEvent)
+     */
+    public void valueUnbound(HttpSessionBindingEvent event) {
+        // If we are going to be unbound from the session, the session must be
+        // closing
+        try {
+            while (!applications.isEmpty()) {
+                final Application app = (Application) applications.iterator()
+                        .next();
+                app.close();
+                ApplicationServlet.applicationToAjaxAppMgrMap.remove(app);
+                removeApplication(app);
+            }
+        } catch (Exception e) {
+            // This should never happen but is possible with rare
+            // configurations (e.g. robustness tests). If you have one
+            // thread doing HTTP socket write and another thread trying to
+            // remove same application here. Possible if you got e.g. session
+            // lifetime 1 min but socket write may take longer than 1 min.
+            System.err.println("Could not remove application, leaking memory.");
+            e.printStackTrace();
+        }
+    }
+
+    /**
+     * Get the web browser associated with this application context.
+     * 
+     * Because application context is related to the http session and server
+     * maintains one session per browser-instance, each context has exactly one
+     * web browser associated with it.
+     * 
+     * @return
+     */
+    public WebBrowser getBrowser() {
+        return browser;
+    }
+}
diff --git a/src/com/itmill/toolkit/terminal/terminal/gwt/server/WebBrowser.java b/src/com/itmill/toolkit/terminal/terminal/gwt/server/WebBrowser.java
new file mode 100644 (file)
index 0000000..027a836
--- /dev/null
@@ -0,0 +1,97 @@
+/* 
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.itmill.toolkit.terminal.gwt.server;
+
+import java.util.Locale;
+
+import javax.servlet.http.HttpServletRequest;
+
+import com.itmill.toolkit.terminal.Terminal;
+
+public class WebBrowser implements Terminal {
+
+    private int screenHeight = 0;
+    private int screenWidth = 0;
+    private String browserApplication = null;
+    private Locale locale;
+    private String address;
+    private boolean secureConnection;
+
+    /**
+     * There is no default-theme for this terminal type.
+     * 
+     * @return Allways returns null.
+     */
+    public String getDefaultTheme() {
+        return null;
+    }
+
+    /**
+     * Get the height of the users display in pixels.
+     * 
+     */
+    public int getScreenHeight() {
+        return screenHeight;
+    }
+
+    /**
+     * Get the width of the users display in pixels.
+     * 
+     */
+    public int getScreenWidth() {
+        return screenWidth;
+    }
+
+    /**
+     * Get the browser user-agent string.
+     * 
+     * @return
+     */
+    public String getBrowserApplication() {
+        return browserApplication;
+    }
+
+    void updateBrowserProperties(HttpServletRequest request) {
+        locale = request.getLocale();
+        address = request.getRemoteAddr();
+        secureConnection = request.isSecure();
+
+        final String agent = request.getHeader("user-agent");
+        if (agent != null) {
+            browserApplication = agent;
+        }
+
+        final String sw = request.getParameter("screenWidth");
+        final String sh = request.getParameter("screenHeight");
+        if (sw != null && sh != null) {
+            try {
+                screenHeight = Integer.parseInt(sh);
+                screenWidth = Integer.parseInt(sw);
+            } catch (final NumberFormatException e) {
+                screenHeight = screenWidth = 0;
+            }
+        }
+    }
+
+    /**
+     * Get the IP-address of the web browser.
+     * 
+     * @return IP-address in 1.12.123.123 -format
+     */
+    public String getAddress() {
+        return address;
+    }
+
+    /** Get the default locate of the browser. */
+    public Locale getLocale() {
+        return locale;
+    }
+
+    /** Is the connection made using HTTPS? */
+    public boolean isSecureConnection() {
+        return secureConnection;
+    }
+
+}
diff --git a/src/com/itmill/toolkit/terminal/terminal/package.html b/src/com/itmill/toolkit/terminal/terminal/package.html
new file mode 100644 (file)
index 0000000..83514a0
--- /dev/null
@@ -0,0 +1,21 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
+<html>
+<head>
+
+</head>
+
+<body bgcolor="white">
+
+<!-- Package summary here -->
+
+<p>Provides classes and interfaces that wrap the terminal-side functionalities
+for the server-side application. (FIXME: This could be a little more descriptive and wordy.)</p>
+
+<h2>Package Specification</h2>
+
+<!-- Package spec here -->
+
+<!-- Put @see and @since tags down here. -->
+
+</body>
+</html>
diff --git a/src/com/itmill/toolkit/terminal/terminal/web/ApplicationServlet.java b/src/com/itmill/toolkit/terminal/terminal/web/ApplicationServlet.java
new file mode 100644 (file)
index 0000000..9a7f93c
--- /dev/null
@@ -0,0 +1,32 @@
+/* 
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.itmill.toolkit.terminal.web;
+
+import javax.servlet.ServletConfig;
+import javax.servlet.ServletException;
+
+/**
+ * This package and servlet is only provided for backward compatibility. Since
+ * Toolkit version 5.0 you should use
+ * com.itmill.toolkit.terminal.gwt.server.ApplicationServlet instead of this
+ * class.
+ * 
+ * @author IT Mill Ltd.
+ * @version
+ * @VERSION@
+ * @since 5.0
+ */
+public class ApplicationServlet extends
+        com.itmill.toolkit.terminal.gwt.server.ApplicationServlet {
+
+    private static final long serialVersionUID = -1471357707917217303L;
+
+    public void init(ServletConfig servletConfig) throws ServletException {
+        System.err
+                .println("Compatiblity class in use. Please use com.itmill.toolkit.terminal.gwt.server.ApplicationServlet instead. You probably need to update your web.xml.");
+        super.init(servletConfig);
+    }
+
+}