]> source.dussan.org Git - vaadin-framework.git/commitdiff
svn changeset:5134/svn branch:trunk
authorMatti Tahvonen <matti.tahvonen@itmill.com>
Tue, 5 Aug 2008 07:57:55 +0000 (07:57 +0000)
committerMatti Tahvonen <matti.tahvonen@itmill.com>
Tue, 5 Aug 2008 07:57:55 +0000 (07:57 +0000)
114 files changed:
src/com/itmill/toolkit/terminal/gwt/client/client/ApplicationConfiguration.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/ApplicationConnection.java [new file with mode: 0755]
src/com/itmill/toolkit/terminal/gwt/client/client/BrowserInfo.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/CSSRule.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/Caption.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/CaptionWrapper.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/Console.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/Container.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/ContainerResizedListener.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/DateTimeService.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/DebugConsole.java [new file with mode: 0755]
src/com/itmill/toolkit/terminal/gwt/client/client/DefaultWidgetSet.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/ErrorMessage.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/Focusable.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/LocaleNotLoadedException.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/LocaleService.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/NullConsole.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/Paintable.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/StyleConstants.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/Tooltip.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/TooltipInfo.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/UIDL.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/Util.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/WidgetSet.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/ui/Action.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/ui/ActionOwner.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/ui/AlignmentInfo.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/ui/CalendarEntry.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/ui/CalendarPanel.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/ui/ContextMenu.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/ui/Field.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/ui/IAccordion.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/ui/IButton.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/ui/ICheckBox.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/ui/ICustomComponent.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/ui/ICustomLayout.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/ui/IDateField.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/ui/IDateFieldCalendar.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/ui/IEmbedded.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/ui/IExpandLayout.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/ui/IFilterSelect.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/ui/IForm.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/ui/IFormLayout.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/ui/IGridLayout.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/ui/IHorizontalExpandLayout.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/ui/ILabel.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/ui/ILink.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/ui/IListSelect.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/ui/IMenuBar.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/ui/INativeSelect.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/ui/IOptionGroup.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/ui/IOptionGroupBase.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/ui/IOrderedLayout.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/ui/IPanel.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/ui/IPasswordField.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/ui/IPopupCalendar.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/ui/IProgressIndicator.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/ui/IScrollTable.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/ui/ISlider.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/ui/ISplitPanel.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/ui/ISplitPanelHorizontal.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/ui/ISplitPanelVertical.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/ui/ITablePaging.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/ui/ITabsheet.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/ui/ITabsheetBase.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/ui/ITabsheetPanel.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/ui/ITextArea.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/ui/ITextField.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/ui/ITextualDate.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/ui/ITree.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/ui/ITwinColSelect.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/ui/IUnknownComponent.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/ui/IUpload.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/ui/IView.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/ui/IWindow.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/ui/Icon.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/ui/MarginInfo.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/ui/MenuBar.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/ui/MenuItem.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/ui/Notification.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/ui/ShortcutActionHandler.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/ui/Table.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/ui/Time.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/ui/ToolkitOverlay.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/ui/TreeAction.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/ui/TreeImages.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/ui/absolutegrid/AbsoluteGrid.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/ui/absolutegrid/ISizeableGridLayout.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/ui/richtextarea/IRichTextArea.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/ui/richtextarea/RichTextToolbar$Strings.properties [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/ui/richtextarea/RichTextToolbar.java [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/ui/richtextarea/backColors.gif [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/ui/richtextarea/bold.gif [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/ui/richtextarea/createLink.gif [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/ui/richtextarea/fontSizes.gif [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/ui/richtextarea/fonts.gif [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/ui/richtextarea/foreColors.gif [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/ui/richtextarea/gwtLogo.png [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/ui/richtextarea/hr.gif [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/ui/richtextarea/indent.gif [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/ui/richtextarea/insertImage.gif [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/ui/richtextarea/italic.gif [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/ui/richtextarea/justifyCenter.gif [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/ui/richtextarea/justifyLeft.gif [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/ui/richtextarea/justifyRight.gif [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/ui/richtextarea/ol.gif [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/ui/richtextarea/outdent.gif [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/ui/richtextarea/removeFormat.gif [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/ui/richtextarea/removeLink.gif [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/ui/richtextarea/strikeThrough.gif [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/ui/richtextarea/subscript.gif [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/ui/richtextarea/superscript.gif [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/ui/richtextarea/ul.gif [new file with mode: 0644]
src/com/itmill/toolkit/terminal/gwt/client/client/ui/richtextarea/underline.gif [new file with mode: 0644]

diff --git a/src/com/itmill/toolkit/terminal/gwt/client/client/ApplicationConfiguration.java b/src/com/itmill/toolkit/terminal/gwt/client/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/gwt/client/client/ApplicationConnection.java b/src/com/itmill/toolkit/terminal/gwt/client/client/ApplicationConnection.java
new file mode 100755 (executable)
index 0000000..10313d2
--- /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);
+        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());
+
+        // Set captions
+        if (manageCaption) {
+            final Container parent = Util.getParentLayout(component);
+            if (parent != null) {
+                parent.updateCaption((Paintable) component, uidl);
+            }
+        }
+
+        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/gwt/client/client/BrowserInfo.java b/src/com/itmill/toolkit/terminal/gwt/client/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/gwt/client/client/CSSRule.java b/src/com/itmill/toolkit/terminal/gwt/client/client/CSSRule.java
new file mode 100644 (file)
index 0000000..d7cca24
--- /dev/null
@@ -0,0 +1,84 @@
+package com.itmill.toolkit.terminal.gwt.client;
+
+import com.google.gwt.core.client.JavaScriptObject;
+
+/**
+ * Utility class for fetching CSS properties from DOM StyleSheets JS object.
+ */
+public class CSSRule {
+
+    private final String selector;
+    private JavaScriptObject rules = null;
+
+    public CSSRule(String selector) {
+        this.selector = selector;
+        fetchRule(selector);
+    }
+
+    // TODO how to find the right LINK-element? We should probably give the
+    // stylesheet a name.
+    private native void fetchRule(final String selector)
+    /*-{
+        this.@com.itmill.toolkit.terminal.gwt.client.CSSRule::rules = @com.itmill.toolkit.terminal.gwt.client.CSSRule::searchForRule(Lcom/google/gwt/core/client/JavaScriptObject;Ljava/lang/String;)($doc.styleSheets[1], selector);
+    }-*/;
+
+    /*
+     * Loops through all current style rules and collects all matching to
+     * 'rules' array. The array is reverse ordered (last one found is first).
+     */
+    private static native JavaScriptObject searchForRule(
+            JavaScriptObject sheet, final String selector)
+    /*-{
+    if(!$doc.styleSheets)
+        return null;
+        
+    selector = selector.toLowerCase();
+    
+    var allMatches = [];
+    
+    var theRules = new Array();
+    if (sheet.cssRules)
+        theRules = sheet.cssRules
+    else if (sheet.rules)
+        theRules = sheet.rules
+        
+        var j = theRules.length;
+        for(var i=0; i<j; i++) {
+            var r = theRules[i];
+            if(r.type == 3) {
+                allMatches.unshift(@com.itmill.toolkit.terminal.gwt.client.CSSRule::searchForRule(Lcom/google/gwt/core/client/JavaScriptObject;Ljava/lang/String;)(r.styleSheet, selector));
+            } else if(r.type == 1) {
+                var selectors = r.selectorText.toLowerCase().split(",");
+                var n = selectors.length;
+                for(var m=0; m<n; m++) {
+                    if(selectors[m].replace(/^\s+|\s+$/g, "") == selector) {
+                        allMatches.unshift(r);
+                        break; // No need to loop other selectors for this rule
+                    }
+                }
+            }
+        }
+        
+        return allMatches;
+    }-*/;
+
+    /**
+     * Returns a specific property value from this CSS rule.
+     * @param propertyName
+     * @return
+     */
+    public native String getPropertyValue(final String propertyName)
+    /*-{  
+        for(var i=0; i<this.@com.itmill.toolkit.terminal.gwt.client.CSSRule::rules.length; i++){
+            var value = this.@com.itmill.toolkit.terminal.gwt.client.CSSRule::rules[i].style[propertyName];
+            if(value)
+                return value;
+        }
+        return null;
+    }-*/;
+
+    public String getSelector() {
+        return selector;
+    }
+
+}
diff --git a/src/com/itmill/toolkit/terminal/gwt/client/client/Caption.java b/src/com/itmill/toolkit/terminal/gwt/client/client/Caption.java
new file mode 100644 (file)
index 0000000..a2a33a3
--- /dev/null
@@ -0,0 +1,179 @@
+/* 
+@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;
+
+    private boolean placedAfterComponent = false;
+
+    /**
+     * 
+     * @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;
+
+        placedAfterComponent = true;
+
+        if (uidl.hasAttribute("icon")) {
+            if (icon == null) {
+                icon = new Icon(client);
+                placedAfterComponent = false;
+                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;
+                placedAfterComponent = 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();
+                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;
+    }
+
+    public boolean shouldBePlacedAfterComponent() {
+        return placedAfterComponent;
+    }
+}
diff --git a/src/com/itmill/toolkit/terminal/gwt/client/client/CaptionWrapper.java b/src/com/itmill/toolkit/terminal/gwt/client/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/gwt/client/client/Console.java b/src/com/itmill/toolkit/terminal/gwt/client/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/gwt/client/client/Container.java b/src/com/itmill/toolkit/terminal/gwt/client/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/gwt/client/client/ContainerResizedListener.java b/src/com/itmill/toolkit/terminal/gwt/client/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/gwt/client/client/DateTimeService.java b/src/com/itmill/toolkit/terminal/gwt/client/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/gwt/client/client/DebugConsole.java b/src/com/itmill/toolkit/terminal/gwt/client/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/gwt/client/client/DefaultWidgetSet.java b/src/com/itmill/toolkit/terminal/gwt/client/client/DefaultWidgetSet.java
new file mode 100644 (file)
index 0000000..b98cdec
--- /dev/null
@@ -0,0 +1,323 @@
+/* 
+@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.IOrderedLayout;
+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.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.IOrderedLayout"
+                .equals(className)) {
+            return new IOrderedLayout();
+        } 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)) {
+            return "com.itmill.toolkit.terminal.gwt.client.ui.IOrderedLayout";
+        } 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/gwt/client/client/ErrorMessage.java b/src/com/itmill/toolkit/terminal/gwt/client/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/gwt/client/client/Focusable.java b/src/com/itmill/toolkit/terminal/gwt/client/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/gwt/client/client/LocaleNotLoadedException.java b/src/com/itmill/toolkit/terminal/gwt/client/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/gwt/client/client/LocaleService.java b/src/com/itmill/toolkit/terminal/gwt/client/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/gwt/client/client/NullConsole.java b/src/com/itmill/toolkit/terminal/gwt/client/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/gwt/client/client/Paintable.java b/src/com/itmill/toolkit/terminal/gwt/client/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/gwt/client/client/StyleConstants.java b/src/com/itmill/toolkit/terminal/gwt/client/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/gwt/client/client/Tooltip.java b/src/com/itmill/toolkit/terminal/gwt/client/client/Tooltip.java
new file mode 100644 (file)
index 0000000..b10a7ff
--- /dev/null
@@ -0,0 +1,186 @@
+/* 
+@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) {
+        super(false, false, true);
+        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/gwt/client/client/TooltipInfo.java b/src/com/itmill/toolkit/terminal/gwt/client/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/gwt/client/client/UIDL.java b/src/com/itmill/toolkit/terminal/gwt/client/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/gwt/client/client/Util.java b/src/com/itmill/toolkit/terminal/gwt/client/client/Util.java
new file mode 100644 (file)
index 0000000..5ecc407
--- /dev/null
@@ -0,0 +1,164 @@
+/* 
+@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);
+    }-*/;
+
+    /**
+     * Clones given element as in JavaScript.
+     * 
+     * Deprecate this if there appears similar method into GWT someday.
+     * 
+     * @param element
+     * @param deep
+     *                clone child tree also
+     * @return
+     */
+    public static native Element cloneNode(Element element, boolean deep)
+    /*-{
+        return element.cloneNode(deep);
+    }-*/;
+}
diff --git a/src/com/itmill/toolkit/terminal/gwt/client/client/WidgetSet.java b/src/com/itmill/toolkit/terminal/gwt/client/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/gwt/client/client/ui/Action.java b/src/com/itmill/toolkit/terminal/gwt/client/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/gwt/client/client/ui/ActionOwner.java b/src/com/itmill/toolkit/terminal/gwt/client/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/gwt/client/client/ui/AlignmentInfo.java b/src/com/itmill/toolkit/terminal/gwt/client/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/gwt/client/client/ui/CalendarEntry.java b/src/com/itmill/toolkit/terminal/gwt/client/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/gwt/client/client/ui/CalendarPanel.java b/src/com/itmill/toolkit/terminal/gwt/client/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/gwt/client/client/ui/ContextMenu.java b/src/com/itmill/toolkit/terminal/gwt/client/client/ui/ContextMenu.java
new file mode 100644 (file)
index 0000000..b97dafe
--- /dev/null
@@ -0,0 +1,117 @@
+/* 
+@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, false, 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();
+        }
+
+        /*public void onBrowserEvent(Event event) {
+            // Remove current selection when mouse leaves
+            if (DOM.eventGetType(event) == Event.ONMOUSEOUT) {
+                Element to = DOM.eventGetToElement(event);
+                if (!DOM.isOrHasChild(getElement(), to)) {
+                    DOM.setElementProperty(
+                            super.getSelectedItem().getElement(), "className",
+                            super.getSelectedItem().getStylePrimaryName());
+                }
+            }
+
+            super.onBrowserEvent(event);
+        }*/
+    }
+}
diff --git a/src/com/itmill/toolkit/terminal/gwt/client/client/ui/Field.java b/src/com/itmill/toolkit/terminal/gwt/client/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/gwt/client/client/ui/IAccordion.java b/src/com/itmill/toolkit/terminal/gwt/client/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/gwt/client/client/ui/IButton.java b/src/com/itmill/toolkit/terminal/gwt/client/client/ui/IButton.java
new file mode 100644 (file)
index 0000000..68d83f1
--- /dev/null
@@ -0,0 +1,117 @@
+/* 
+@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.BrowserInfo;
+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);
+
+            // Fix for IE6, IE7
+            if (BrowserInfo.get().isIE()) {
+                DOM.setInnerText(errorIndicatorElement, " ");
+            }
+
+        } 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/gwt/client/client/ui/ICheckBox.java b/src/com/itmill/toolkit/terminal/gwt/client/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/gwt/client/client/ui/ICustomComponent.java b/src/com/itmill/toolkit/terminal/gwt/client/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/gwt/client/client/ui/ICustomLayout.java b/src/com/itmill/toolkit/terminal/gwt/client/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/gwt/client/client/ui/IDateField.java b/src/com/itmill/toolkit/terminal/gwt/client/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/gwt/client/client/ui/IDateFieldCalendar.java b/src/com/itmill/toolkit/terminal/gwt/client/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/gwt/client/client/ui/IEmbedded.java b/src/com/itmill/toolkit/terminal/gwt/client/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/gwt/client/client/ui/IExpandLayout.java b/src/com/itmill/toolkit/terminal/gwt/client/client/ui/IExpandLayout.java
new file mode 100644 (file)
index 0000000..e4f86ec
--- /dev/null
@@ -0,0 +1,726 @@
+/* 
+@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.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.BrowserInfo;
+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 (BrowserInfo.get().isIE()) {
+                DOM.setStyleAttribute(marginElement, "zoom", "1");
+                DOM.setStyleAttribute(marginElement, "overflow", "hidden");
+            }
+            childContainer = DOM.createDiv();
+            if (BrowserInfo.get().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 (BrowserInfo.get().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 (BrowserInfo.get().isIE6()) {
+                DOM.setStyleAttribute(getElement(), "overflow", "hidden");
+            }
+            size = getOffsetHeight();
+            if (BrowserInfo.get().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);
+        }
+
+        // workaround for safari bug #1870
+        float wkv = BrowserInfo.get().getWebkitVersion();
+        if (wkv > 0 && wkv < 526.9) {
+            DeferredCommand.addCommand(new Command() {
+                public void execute() {
+                    iLayout();
+                }
+            });
+        }
+    }
+}
diff --git a/src/com/itmill/toolkit/terminal/gwt/client/client/ui/IFilterSelect.java b/src/com/itmill/toolkit/terminal/gwt/client/client/ui/IFilterSelect.java
new file mode 100644 (file)
index 0000000..57c11c3
--- /dev/null
@@ -0,0 +1,788 @@
+/* 
+@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, false, 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, "");
+            }
+            // We don't need to show arrows or statusbar if there is only one
+            // page
+            if (matches <= PAGELENTH) {
+                setPagingEnabled(false);
+            } else {
+                setPagingEnabled(true);
+            }
+            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", "");
+                DOM.setStyleAttribute(up, "display", "");
+                DOM.setStyleAttribute(status, "display", "");
+            } 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
+                    && !"".equals(lastFilter)
+                    && 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;
+                }
+            case KeyboardListener.KEY_ESCAPE:
+                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;
+                }
+                lastFilter = "";
+                suggestionPopup.hide();
+                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/gwt/client/client/ui/IForm.java b/src/com/itmill/toolkit/terminal/gwt/client/client/ui/IForm.java
new file mode 100644 (file)
index 0000000..6722e1e
--- /dev/null
@@ -0,0 +1,142 @@
+/* \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
+        Util.runDescendentsLayout(this);\r
+    }\r
+\r
+}\r
diff --git a/src/com/itmill/toolkit/terminal/gwt/client/client/ui/IFormLayout.java b/src/com/itmill/toolkit/terminal/gwt/client/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/gwt/client/client/ui/IGridLayout.java b/src/com/itmill/toolkit/terminal/gwt/client/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/gwt/client/client/ui/IHorizontalExpandLayout.java b/src/com/itmill/toolkit/terminal/gwt/client/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/gwt/client/client/ui/ILabel.java b/src/com/itmill/toolkit/terminal/gwt/client/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/gwt/client/client/ui/ILink.java b/src/com/itmill/toolkit/terminal/gwt/client/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/gwt/client/client/ui/IListSelect.java b/src/com/itmill/toolkit/terminal/gwt/client/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/gwt/client/client/ui/IMenuBar.java b/src/com/itmill/toolkit/terminal/gwt/client/client/ui/IMenuBar.java
new file mode 100644 (file)
index 0000000..6c8e5f9
--- /dev/null
@@ -0,0 +1,234 @@
+package com.itmill.toolkit.terminal.gwt.client.ui;
+
+import java.util.ArrayList;
+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";
+
+    /** For server connections **/
+    protected String uidlId;
+    protected ApplicationConnection client;
+
+    protected final IMenuBar hostReference = this;
+    protected static final boolean vertical = true;
+    protected String submenuIcon = null;
+    protected boolean collapseItems = true;
+
+    protected MenuItem moreItem = null;
+
+    // Construct an empty command to be used when the item has no command
+    // associated
+    protected static final Command emptyCommand = null;
+
+    /**
+     * The constructor should first call super() to initialize the component and
+     * then handle any initialization relevant to IT Mill Toolkit.
+     */
+    public IMenuBar() {
+        // Create an empty horizontal menubar
+        super();
+
+        // This method call of the Paintable interface sets the component
+        // style name in DOM tree
+        setStyleName(CLASSNAME);
+    }
+
+    public IMenuBar(boolean vertical) {
+        super(vertical);
+
+        // This method call of the Paintable interface sets the component
+        // style name in DOM tree
+        setStyleName(CLASSNAME + "_submenu");
+    }
+
+    /**
+     * 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;
+        }
+
+        // For future connections
+        this.client = client;
+        uidlId = uidl.getId();
+
+        // Empty the menu every time it receives new information
+        if (!this.getItems().isEmpty()) {
+            this.clearItems();
+        }
+
+        UIDL options = uidl.getChildUIDL(0);
+        // For GWT 1.5
+        //this.setAnimationEnabled(options.getBooleanAttribute("animationEnabled"
+        // ))
+        // ;
+
+        if (options.hasAttribute("submenuIcon")) {
+            submenuIcon = client.translateToolkitUri(uidl.getChildUIDL(0)
+                    .getStringAttribute("submenuIcon"));
+        } else {
+            submenuIcon = null;
+        }
+
+        collapseItems = options.getBooleanAttribute("collapseItems");
+
+        if (collapseItems) {
+            UIDL moreItemUIDL = options.getChildUIDL(0);
+            StringBuffer itemHTML = new StringBuffer();
+            itemHTML.append("<p>");
+            if (moreItemUIDL.hasAttribute("icon")) {
+                itemHTML.append("<img src=\""
+                        + client.translateToolkitUri(moreItemUIDL
+                                .getStringAttribute("icon"))
+                        + "\" align=\"left\" />");
+            }
+            itemHTML.append(moreItemUIDL.getStringAttribute("text"));
+            itemHTML.append("</p>");
+            moreItem = new MenuItem(itemHTML.toString(), true, emptyCommand);
+        }
+
+        UIDL items = uidl.getChildUIDL(1);
+        Iterator itr = items.getChildIterator();
+        Stack iteratorStack = new Stack();
+        Stack menuStack = new Stack();
+        MenuBar currentMenu = this;
+
+        while (itr.hasNext()) {
+            UIDL item = (UIDL) itr.next();
+            MenuItem currentItem = 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
+            StringBuffer itemHTML = new StringBuffer();
+
+            itemHTML.append("<p>");
+
+            if (item.hasAttribute("icon")) {
+                itemHTML.append("<img src=\""
+                        + client.translateToolkitUri(item
+                                .getStringAttribute("icon"))
+                        + "\" align=\"left\" />");
+            }
+
+            itemHTML.append(itemText);
+
+            if (currentMenu != this && item.getChildCount() > 0
+                    && submenuIcon != null) {
+                itemHTML.append("<img src=\"" + submenuIcon
+                        + "\" align=\"right\" />");
+            }
+
+            itemHTML.append("</p>");
+
+            Command cmd = null;
+
+            // Check if we need to create a command to this item
+            if (itemHasCommand) {
+                // Construct a command that fires onMenuClick(int) with the
+                // item's id-number
+                cmd = new Command() {
+                    public void execute() {
+                        hostReference.onMenuClick(itemId);
+                    }
+                };
+            }
+
+            currentItem = currentMenu.addItem(itemHTML.toString(), true, cmd);
+
+            if (item.getChildCount() > 0) {
+                menuStack.push(currentMenu);
+                iteratorStack.push(itr);
+                itr = item.getChildIterator();
+                currentMenu = new IMenuBar(vertical);
+                currentItem.setSubMenu(currentMenu);
+            }
+
+            while (!itr.hasNext() && !iteratorStack.empty()) {
+                itr = (Iterator) iteratorStack.pop();
+                currentMenu = (MenuBar) menuStack.pop();
+            }
+        }// while
+
+        // we might need to collapse the top-level menu
+        if (collapseItems) {
+            int topLevelWidth = 0;
+
+            int ourWidth = this.getOffsetWidth();
+
+            int i = 0;
+            for (; i < getItems().size() && topLevelWidth < ourWidth; i++) {
+                MenuItem item = (MenuItem) getItems().get(i);
+                topLevelWidth += item.getOffsetWidth();
+            }
+
+            if (topLevelWidth > this.getOffsetWidth()) {
+                ArrayList toBeCollapsed = new ArrayList();
+                MenuBar collapsed = new IMenuBar(vertical);
+                for (int j = i - 2; j < getItems().size(); j++) {
+                    toBeCollapsed.add(getItems().get(j));
+                }
+
+                for (int j = 0; j < toBeCollapsed.size(); j++) {
+                    MenuItem item = (MenuItem) toBeCollapsed.get(j);
+                    removeItem(item);
+
+                    // it's ugly, but we have to insert the submenu icon
+                    if (item.getSubMenu() != null && submenuIcon != null) {
+                        String itemHTML = item.getHTML();
+                        StringBuffer itemText = new StringBuffer(itemHTML
+                                .substring(0, itemHTML.length() - 4));
+                        itemText.append("<img src=\"" + submenuIcon
+                                + "\" align=\"right\" /></p>");
+                        item.setHTML(itemText.toString());
+                    }
+
+                    collapsed.addItem(item);
+                }
+
+                moreItem.setSubMenu(collapsed);
+                addItem(moreItem);
+            }
+        }
+    }// 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/gwt/client/client/ui/INativeSelect.java b/src/com/itmill/toolkit/terminal/gwt/client/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/gwt/client/client/ui/IOptionGroup.java b/src/com/itmill/toolkit/terminal/gwt/client/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/gwt/client/client/ui/IOptionGroupBase.java b/src/com/itmill/toolkit/terminal/gwt/client/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/gwt/client/client/ui/IOrderedLayout.java b/src/com/itmill/toolkit/terminal/gwt/client/client/ui/IOrderedLayout.java
new file mode 100644 (file)
index 0000000..f6ec503
--- /dev/null
@@ -0,0 +1,1150 @@
+/* 
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.itmill.toolkit.terminal.gwt.client.ui;
+
+import java.util.Iterator;
+import java.util.Vector;
+
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.Element;
+import com.google.gwt.user.client.ui.Panel;
+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.BrowserInfo;
+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.UIDL;
+import com.itmill.toolkit.terminal.gwt.client.Util;
+
+/**
+ * Full implementation of OrderedLayout client peer.
+ * 
+ * This class implements all features of OrderedLayout. It currently only
+ * supports use through UIDL updates. Direct client side use is not (currently)
+ * suported in all operation modes.
+ * 
+ * @author IT Mill Ltd
+ */
+public class IOrderedLayout extends Panel implements Container,
+        ContainerResizedListener {
+
+    public static final String CLASSNAME = "i-orderedlayout";
+
+    public static final int ORIENTATION_VERTICAL = 0;
+    public static final int ORIENTATION_HORIZONTAL = 1;
+
+    private int hSpacing = -1;
+    private int vSpacing = -1;
+    private int marginTop = -1;
+    private int marginBottom = -1;
+    private int marginLeft = -1;
+    private int marginRight = -1;
+
+    int orientationMode = ORIENTATION_VERTICAL;
+
+    protected ApplicationConnection client;
+
+    /**
+     * Reference to Element where wrapped childred are contained. Normally a
+     * DIV, TR or a TBODY element.
+     */
+    private Element wrappedChildContainer;
+
+    /**
+     * Elements that provides the Layout interface implementation. Root element
+     * of the component. This is the outmost div or table.
+     */
+    private Element root;
+
+    /**
+     * List of child widgets. This is not the list of wrappers, but the actual
+     * widgets
+     */
+    private final Vector childWidgets = new Vector();
+
+    /**
+     * In table mode, the root element is table instead of div.
+     */
+    private boolean tableMode = false;
+
+    /** Last set width of the component. Null if undefined (instead of being ""). */
+    private String width = null;
+
+    /**
+     * Last set height of the component. Null if undefined (instead of being
+     * "").
+     */
+    private String height = null;
+    /**
+     * List of child widget wrappers. These wrappers are in exact same indexes
+     * as the widgets in childWidgets list.
+     */
+    private final Vector childWidgetWrappers = new Vector();
+
+    /** Whether the component has spacing enabled. */
+    private boolean hasComponentSpacing;
+
+    /** Information about margin states. */
+    private MarginInfo margins = new MarginInfo(0);
+
+    /**
+     * Flag that indicates that the child layouts must be updated as soon as
+     * possible. This will be done in the end of updateFromUIDL.
+     */
+    private boolean childLayoutsHaveChanged = false;
+
+    /**
+     * Construct the DOM of the orderder layout.
+     * 
+     * <p>
+     * There are two modes - vertical and horizontal.
+     * <ul>
+     * <li>Vertical mode uses structure: div-root ( div-wrap ( child ) div-wrap (
+     * child ))).</li>
+     * <li>Horizontal mode uses structure: table ( tbody ( tr-childcontainer (
+     * td-wrap ( child ) td-wrap ( child) )) )</li>
+     * </ul>
+     * where root and childcontainer refer to the root element and the element
+     * that contain WidgetWrappers.
+     * </p>
+     * 
+     */
+    public IOrderedLayout() {
+        wrappedChildContainer = root = DOM.createDiv();
+        setElement(root);
+        setStyleName(CLASSNAME);
+    }
+
+    /**
+     * Update orientation, if it has changed.
+     * 
+     * @param newOrientationMode
+     */
+    private void rebuildRootDomStructure(int oldOrientationMode) {
+
+        // Should we have table as a root element?
+        boolean newTableMode = !(orientationMode == ORIENTATION_VERTICAL && width != null);
+
+        // Already in correct mode?
+        if (oldOrientationMode == orientationMode && newTableMode == tableMode) {
+            return;
+        }
+        boolean oldTableMode = tableMode;
+        tableMode = newTableMode;
+
+        // Constuct base DOM-structure and clean any already attached
+        // widgetwrappers from DOM.
+        if (tableMode) {
+            Element tmp = DOM.createDiv();
+            final String structure = "<table cellspacing=\"0\" cellpadding=\"0\"><tbody>"
+                    + (orientationMode == ORIENTATION_HORIZONTAL ? "<tr></tr>"
+                            : "") + "</tbody></table>";
+            DOM.setInnerHTML(tmp, structure);
+            root = DOM.getFirstChild(tmp);
+            DOM.removeChild(tmp, root);
+            // set TBODY to be the wrappedChildContainer
+            wrappedChildContainer = DOM.getFirstChild(root);
+            // In case of horizontal layouts, we must user TR instead of TBODY
+            if (orientationMode == ORIENTATION_HORIZONTAL) {
+                wrappedChildContainer = DOM
+                        .getFirstChild(wrappedChildContainer);
+            }
+        } else {
+            wrappedChildContainer = root = DOM.createDiv();
+        }
+
+        // Restore component size
+        if (width != null && !"".equals(width)) {
+            DOM.setStyleAttribute(root, "width", width);
+        }
+        if (height != null && !"".equals(height)) {
+            DOM.setStyleAttribute(root, "height", height);
+        }
+
+        // Reset widget main element
+        String styles = getStyleName();
+        setElement(root);
+        setStyleName(styles);
+
+        // Reinsert all widget wrappers to this container
+        final int currentOrientationMode = orientationMode;
+        for (int i = 0; i < childWidgetWrappers.size(); i++) {
+            WidgetWrapper wr = (WidgetWrapper) childWidgetWrappers.get(i);
+            orientationMode = oldOrientationMode;
+            tableMode = oldTableMode;
+            Element oldWrElement = wr.getElementWrappingWidgetAndCaption();
+            orientationMode = currentOrientationMode;
+            tableMode = newTableMode;
+            String classe = DOM.getElementAttribute(oldWrElement, "class");
+            wr.resetRootElement();
+            Element newWrElement = wr.getElementWrappingWidgetAndCaption();
+            if (classe != null && classe.length() > 0) {
+                DOM.setElementAttribute(newWrElement, "class", classe);
+            }
+            while (DOM.getChildCount(oldWrElement) > 0) {
+                Element c = DOM.getFirstChild(oldWrElement);
+                DOM.removeChild(oldWrElement, c);
+                DOM.appendChild(newWrElement, c);
+            }
+
+            DOM.appendChild(wrappedChildContainer, wr.getElement());
+        }
+
+        // Update child layouts
+        childLayoutsHaveChanged = true;
+    }
+
+    /** Update the contents of the layout from UIDL. */
+    public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+
+        this.client = client;
+
+        // Only non-cached UIDL:s can introduce changes
+        if (uidl.getBooleanAttribute("cached")) {
+            return;
+        }
+
+        updateMarginAndSpacingSizesFromCSS(uidl);
+
+        // Update sizes, ...
+        if (client.updateComponent(this, uidl, false)) {
+            return;
+        }
+
+        // Rebuild DOM tree root if necessary
+        int oldO = orientationMode;
+        orientationMode = "horizontal".equals(uidl
+                .getStringAttribute("orientation")) ? ORIENTATION_HORIZONTAL
+                : ORIENTATION_VERTICAL;
+        rebuildRootDomStructure(oldO);
+
+        // Handle component spacing later in handleAlignments() method
+        hasComponentSpacing = uidl.getBooleanAttribute("spacing");
+
+        // Collect the list of contained widgets after this update
+        final Vector newWidgets = new Vector();
+        for (final Iterator it = uidl.getChildIterator(); it.hasNext();) {
+            final UIDL uidlForChild = (UIDL) it.next();
+            final Paintable child = client.getPaintable(uidlForChild);
+            newWidgets.add(child);
+        }
+
+        // Iterator for old widgets
+        final Iterator oldWidgetsIterator = (new Vector(childWidgets))
+                .iterator();
+
+        // Iterator for new widgets
+        final Iterator newWidgetsIterator = newWidgets.iterator();
+
+        // Iterator for new UIDL
+        final Iterator newUIDLIterator = uidl.getChildIterator();
+
+        // List to collect all now painted widgets to in order to remove
+        // unpainted ones later
+        final Vector paintedWidgets = new Vector();
+
+        final Vector childsToPaint = new Vector();
+
+        // Add any new widgets to the ordered layout
+        Widget oldChild = null;
+        while (newWidgetsIterator.hasNext()) {
+
+            final Widget newChild = (Widget) newWidgetsIterator.next();
+            final UIDL newChildUIDL = (UIDL) newUIDLIterator.next();
+
+            // Remove any unneeded old widgets
+            if (oldChild == null && oldWidgetsIterator.hasNext()) {
+                // search for next old Paintable which still exists in layout
+                // and delete others
+                while (oldWidgetsIterator.hasNext()) {
+                    oldChild = (Widget) oldWidgetsIterator.next();
+                    // now oldChild is an instance of Paintable
+                    if (paintedWidgets.contains(oldChild)) {
+                        continue;
+                    } else if (newWidgets.contains(oldChild)) {
+                        break;
+                    } else {
+                        remove(oldChild);
+                        oldChild = null;
+                    }
+                }
+            }
+
+            if (oldChild == null) {
+                // we are adding components to the end of layout
+                add(newChild);
+            } else if (newChild == oldChild) {
+                // child already attached in correct position
+                oldChild = null;
+            } else if (hasChildComponent(newChild)) {
+
+                // current child has been moved, re-insert before current
+                // oldChild
+                add(newChild, childWidgets.indexOf(oldChild));
+
+            } else {
+                // insert new child before old one
+                add(newChild, childWidgets.indexOf(oldChild));
+            }
+
+            // Update the child component
+            childsToPaint.add(new Object[] { newChild, newChildUIDL });
+
+            // Add this newly handled component to the list of painted
+            // components
+            paintedWidgets.add(newChild);
+        }
+
+        // Remove possibly remaining old widgets which were not in painted UIDL
+        while (oldWidgetsIterator.hasNext()) {
+            oldChild = (Widget) oldWidgetsIterator.next();
+            if (!newWidgets.contains(oldChild)) {
+                remove(oldChild);
+            }
+        }
+
+        // Handle component alignments
+        handleAlignmentsSpacingAndMargins(uidl);
+
+        // Reset sizes for the children
+        updateChildSizes();
+
+        // Paint children
+        for (int i = 0; i < childsToPaint.size(); i++) {
+            Object[] t = (Object[]) childsToPaint.get(i);
+            ((Paintable) t[0]).updateFromUIDL((UIDL) t[1], client);
+        }
+
+        // Update child layouts
+        // TODO This is most probably unnecessary and should be done within
+        // update Child H/W
+        if (childLayoutsHaveChanged) {
+            Util.runDescendentsLayout(this);
+            childLayoutsHaveChanged = false;
+        }
+    }
+
+    private void updateMarginAndSpacingSizesFromCSS(UIDL uidl) {
+        // TODO Read spacing and margins from CSS as documented in #1904.
+        // Somehow refresh after updates
+
+        hSpacing = 8;
+        vSpacing = 8;
+        marginTop = 15;
+        marginBottom = 15;
+        marginLeft = 18;
+        marginRight = 18;
+    }
+
+    /**
+     * While setting width, ensure that margin div is also resized properly.
+     * Furthermore, enable/disable fixed mode
+     */
+    public void setWidth(String newWidth) {
+
+        width = newWidth == null || "".equals(newWidth) ? null : newWidth;
+
+        // When we use divs at root - for them using 100% width should be
+        // calculated with ""
+        if (!tableMode && "100%".equals(newWidth)) {
+            super.setWidth("");
+        } else {
+            super.setWidth(newWidth);
+        }
+
+        // Update child layouts
+        childLayoutsHaveChanged = true;
+    }
+
+    /**
+     * While setting height, ensure that margin div is also resized properly.
+     * Furthermore, enable/disable fixed mode
+     */
+    public void setHeight(String newHeight) {
+        super.setHeight(newHeight);
+        height = newHeight == null || "".equals(newHeight) ? null : newHeight;
+
+        // Update child layouts
+        childLayoutsHaveChanged = true;
+    }
+
+    /** Recalculate and apply the space given for each child in this layout. */
+    private void updateChildSizes() {
+
+        int numChild = childWidgets.size();
+        int childHeightTotal = -1;
+        int childHeightDivisor = 1;
+        int childWidthTotal = -1;
+        int childWidthDivisor = 1;
+
+        // Vertical layout is calculated by us
+        if (height != null) {
+
+            // Calculate the space for fixed contents minus marginals
+            if (tableMode) {
+
+                // If we know explicitly set pixel-size, use that
+                if (height != null && height.endsWith("px")) {
+                    try {
+                        childHeightTotal = Integer.parseInt(height.substring(0,
+                                height.length() - 2));
+
+                        // For negative sizes, use measurements
+                        if (childHeightTotal < 0) {
+                            childHeightTotal = rootOffsetMeasure("offsetHeight");
+                        }
+                    } catch (NumberFormatException e) {
+
+                        // In case of invalid number, try to measure the size;
+                        childHeightTotal = rootOffsetMeasure("offsetHeight");
+                    }
+                }
+                // If not, try to measure the size
+                else {
+                    childHeightTotal = rootOffsetMeasure("offsetHeight");
+                }
+
+            } else {
+                childHeightTotal = DOM.getElementPropertyInt(root,
+                        "offsetHeight");
+            }
+
+            childHeightTotal -= margins.hasTop() ? marginTop : 0;
+            childHeightTotal -= margins.hasBottom() ? marginBottom : 0;
+
+            // Reduce spacing from the size
+            if (hasComponentSpacing) {
+                childHeightTotal -= ((orientationMode == ORIENTATION_HORIZONTAL) ? hSpacing
+                        : vSpacing)
+                        * (numChild - 1);
+            }
+
+            // Total space is divided among the children
+            if (orientationMode == ORIENTATION_VERTICAL) {
+                childHeightDivisor = numChild;
+            }
+        }
+
+        // layout is calculated by us
+        if (width != null) {
+
+            // Calculate the space for fixed contents minus marginals
+            // If we know explicitly set pixel-size, use that
+            if (width != null && width.endsWith("px")) {
+                try {
+                    childWidthTotal = Integer.parseInt(width.substring(0, width
+                            .length() - 2));
+
+                    // For negative sizes, use measurements
+                    if (childWidthTotal < 0) {
+                        childWidthTotal = rootOffsetMeasure("offsetWidth");
+                    }
+
+                } catch (NumberFormatException e) {
+
+                    // In case of invalid number, try to measure the size;
+                    childWidthTotal = rootOffsetMeasure("offsetWidth");
+                }
+            }
+            // If not, try to measure the size
+            else {
+                childWidthTotal = rootOffsetMeasure("offsetWidth");
+            }
+
+            childWidthTotal -= margins.hasLeft() ? marginLeft : 0;
+            childWidthTotal -= margins.hasRight() ? marginRight : 0;
+
+            // Reduce spacing from the size
+            if (hasComponentSpacing
+                    && orientationMode == ORIENTATION_HORIZONTAL) {
+                childWidthTotal -= hSpacing * (numChild - 1);
+            }
+
+            // Total space is divided among the children
+            if (orientationMode == ORIENTATION_HORIZONTAL) {
+                childWidthDivisor = numChild;
+            }
+        }
+
+        // Set the sizes for each child
+        for (Iterator i = childWidgetWrappers.iterator(); i.hasNext();) {
+            int w, h;
+            if (childHeightDivisor > 1) {
+                h = Math.round(((float) childHeightTotal)
+                        / (childHeightDivisor--));
+                childHeightTotal -= h;
+            } else {
+                h = childHeightTotal;
+            }
+            if (childWidthDivisor > 1) {
+                w = Math.round(((float) childWidthTotal)
+                        / (childWidthDivisor--));
+                childWidthTotal -= h;
+            } else {
+                w = childWidthTotal;
+            }
+            WidgetWrapper ww = (WidgetWrapper) i.next();
+            // TODO COMBINE THESE
+            ww.forceSize(w, h);
+        }
+    }
+
+    /**
+     * Measure how much space the root element could get.
+     * 
+     * This measures the space allocated by the parent for the root element
+     * without letting root element to affect the calculation.
+     * 
+     * @param offset
+     *                offsetWidth or offsetHeight
+     */
+    private int rootOffsetMeasure(String offset) {
+        Element measure = DOM.createDiv();
+        DOM.setStyleAttribute(measure, "height", "100%");
+        Element parent = DOM.getParent(root);
+        DOM.insertBefore(parent, measure, root);
+        DOM.removeChild(parent, root);
+        int size = DOM.getElementPropertyInt(measure, offset);
+        DOM.insertBefore(parent, root, measure);
+        DOM.removeChild(parent, measure);
+        // In case the no space would be given for this element
+        // without pushing, use the current side of the root
+        return size;
+    }
+
+    /** Parse alignments from UIDL and pass whem to correct widgetwrappers */
+    private void handleAlignmentsSpacingAndMargins(UIDL uidl) {
+
+        // Only update margins when they have changed
+        // TODO this should be optimized to avoid reupdating these
+        margins = new MarginInfo(uidl.getIntAttribute("margins"));
+
+        // 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 = childWidgetWrappers.iterator();
+
+        while (it.hasNext()) {
+
+            // Calculate alignment info
+            final AlignmentInfo ai = new AlignmentInfo(
+                    alignments[alignmentIndex++]);
+
+            final WidgetWrapper wr = (WidgetWrapper) it.next();
+
+            wr.setAlignment(ai.getVerticalAlignment(), ai
+                    .getHorizontalAlignment());
+
+            // Handle spacing and margins in this loop as well
+            wr.setSpacingAndMargins(alignmentIndex == 1,
+                    alignmentIndex == alignments.length);
+        }
+    }
+
+    /**
+     * Wrapper around single child in the layout.
+     * 
+     * This helper also manages spacing, margins and alignment for individual
+     * cells handling. It also can put hard size limits for its contens by
+     * clipping the content to given pixel size.
+     * 
+     */
+    class WidgetWrapper extends UIObject {
+
+        /**
+         * When alignment table structure is used, these elements correspond to
+         * the TD elements within the structure. If alignment is not used, these
+         * are null.
+         */
+        Element alignmentTD, innermostTDinAlignmnetStructure;
+
+        /**
+         * When clipping must be done and the element wrapping clipped content
+         * would be TD instead of DIV, this element points to additional DIV
+         * that is used for clipping.
+         */
+        Element clipperDiv;
+
+        /** Caption element when used. */
+        Caption caption = null;
+
+        /**
+         * Last set pixel height for the wrapper. -1 if vertical clipping is not
+         * used.
+         */
+        int lastForcedPixelHeight = -1;
+
+        /**
+         * Last set pidel width for the wrapper. -1 if horizontal clipping is
+         * not used.
+         */
+        int lastForcedPixelWidth = -1;
+
+        /** Set the root element */
+        public WidgetWrapper() {
+            resetRootElement();
+        }
+
+        /**
+         * Set the width and height given for the wrapped widget in pixels.
+         * 
+         * -1 if unconstrained.
+         */
+        public void forceSize(int pixelWidth, int pixelHeight) {
+
+            // If we are already at the correct size, do nothing
+            if (lastForcedPixelHeight == pixelHeight
+                    && lastForcedPixelWidth == pixelWidth) {
+                return;
+            }
+
+            // Clipper DIV is needed?
+            if (tableMode && (pixelHeight >= 0 || pixelWidth >= 0)) {
+                if (clipperDiv == null) {
+                    createClipperDiv();
+                }
+            }
+
+            // ClipperDiv is not needed, remove if necessary
+            else if (clipperDiv != null) {
+                removeClipperDiv();
+            }
+
+            Element e = clipperDiv != null ? clipperDiv
+                    : getElementWrappingAlignmentStructures();
+
+            // Overflow
+            DOM.setStyleAttribute(e, "overflowX", pixelWidth < 0 ? ""
+                    : "hidden");
+            DOM.setStyleAttribute(e, "overflowY", pixelHeight < 0 ? ""
+                    : "hidden");
+
+            // Set size
+            DOM.setStyleAttribute(e, "width", pixelWidth < 0 ? "" : pixelWidth
+                    + "px");
+            DOM.setStyleAttribute(e, "height", pixelHeight < 0 ? ""
+                    : pixelHeight + "px");
+
+            // Set cached values
+            lastForcedPixelWidth = pixelWidth;
+            lastForcedPixelHeight = pixelHeight;
+        }
+
+        /** Create a DIV for clipping the child */
+        private void createClipperDiv() {
+            clipperDiv = DOM.createDiv();
+            final Element e = getElementWrappingClipperDiv();
+            String classe = DOM.getElementAttribute(e, "class");
+            while (DOM.getChildCount(e) > 0) {
+                final Element c = DOM.getFirstChild(e);
+                DOM.removeChild(e, c);
+                DOM.appendChild(clipperDiv, c);
+            }
+            if (classe != null && classe.length() > 0) {
+                DOM.removeElementAttribute(e, "class");
+                DOM.setElementAttribute(clipperDiv, "class", classe);
+            }
+            DOM.appendChild(e, clipperDiv);
+        }
+
+        /** Undo createClipperDiv() */
+        private void removeClipperDiv() {
+            final Element e = getElementWrappingClipperDiv();
+            String classe = DOM.getElementAttribute(clipperDiv, "class");
+            while (DOM.getChildCount(clipperDiv) > 0) {
+                final Element c = DOM.getFirstChild(clipperDiv);
+                DOM.removeChild(clipperDiv, c);
+                DOM.appendChild(e, c);
+            }
+            DOM.removeChild(e, clipperDiv);
+            clipperDiv = null;
+            if (classe != null && classe.length() > 0) {
+                DOM.setElementAttribute(e, "class", classe);
+            }
+        }
+
+        /**
+         * Get the element containing the caption and the wrapped widget.
+         * Returned element can one of the following:
+         * <ul>
+         * <li>(a) Root DIV of the WrapperElement when not in tableMode</li>
+         * <li>(b) TD in just below the root TR of the WrapperElement when in
+         * tableMode</li>
+         * <li>(c) clipperDiv inside the (a) or (b)</li>
+         * <li>(d) The innermost TD within alignment structures located in (a),
+         * (b) or (c)</li>
+         * </ul>
+         * 
+         * @return Element described above
+         */
+        private Element getElementWrappingWidgetAndCaption() {
+
+            // When alignment is used, we will can safely return the innermost
+            // TD
+            if (innermostTDinAlignmnetStructure != null) {
+                return innermostTDinAlignmnetStructure;
+            }
+
+            // In all other cases element wrapping the potential alignment
+            // structures is the correct one
+            return getElementWrappingAlignmentStructures();
+        }
+
+        /**
+         * Get the element where alignment structures should be placed in if
+         * they are in use.
+         * 
+         * Returned element can one of the following:
+         * <ul>
+         * <li>(a) Root DIV of the WrapperElement when not in tableMode</li>
+         * <li>(b) TD in just below the root TR of the WrapperElement when in
+         * tableMode</li>
+         * <li>(c) clipperDiv inside the (a) or (b)</li>
+         * </ul>
+         * 
+         * @return Element described above
+         */
+        private Element getElementWrappingAlignmentStructures() {
+
+            // Clipper DIV wraps the alignment structures if present
+            if (clipperDiv != null) {
+                return clipperDiv;
+            }
+
+            // When Clipper DIV is not used, we just give the element
+            // that would wrap it if it would be used
+            return getElementWrappingClipperDiv();
+        }
+
+        /**
+         * Get the element where clipperDiv should be placed in if they it is in
+         * use.
+         * 
+         * Returned element can one of the following:
+         * <ul>
+         * <li>(a) Root DIV of the WrapperElement when not in tableMode</li>
+         * <li>(b) TD in just below the root TR of the WrapperElement when in
+         * tableMode</li>
+         * </ul>
+         * 
+         * @return Element described above
+         */
+        private Element getElementWrappingClipperDiv() {
+
+            // Only vertical layouts in non-table mode use TR as root, for the
+            // rest we can safely give root element
+            if (!tableMode || orientationMode == ORIENTATION_HORIZONTAL) {
+                return getElement();
+            }
+
+            // The root is TR, we'll thus give the TD that is immediately within
+            // the root
+            return DOM.getFirstChild(getElement());
+        }
+
+        /**
+         * Create tr, td or div - depending on the orientation of the layout and
+         * set it as root.
+         * 
+         * All contents of the wrapper are cleared. Caller is responsible for
+         * preserving the contents and moving them into new root.
+         * 
+         * @return Previous root element.
+         */
+        private void resetRootElement() {
+            if (tableMode) {
+                if (orientationMode == ORIENTATION_HORIZONTAL) {
+                    setElement(DOM.createTD());
+                } else {
+                    Element tr = DOM.createTR();
+                    DOM.appendChild(tr, DOM.createTD());
+                    setElement(tr);
+                }
+            } else {
+                setElement(DOM.createDiv());
+                // Apply 'hasLayout' for IE (needed to get accurate dimension
+                // calculations)
+                if (BrowserInfo.get().isIE()) {
+                    DOM.setStyleAttribute(getElement(), "zoom", "1");
+                }
+            }
+
+            // Clear any references to intermediate elements
+            clipperDiv = alignmentTD = innermostTDinAlignmnetStructure = null;
+        }
+
+        /** Update the caption of the element contained in this wrapper. */
+        public void updateCaption(UIDL uidl, Paintable paintable) {
+
+            final Widget widget = (Widget) paintable;
+            final Element captionWrapper = getElementWrappingWidgetAndCaption();
+
+            // The widget needs caption
+            if (Caption.isNeeded(uidl)) {
+
+                // If the caption element is missing, create it
+                boolean justAdded = false;
+                if (caption == null) {
+                    justAdded = true;
+                    caption = new Caption(paintable, client);
+                }
+
+                // Update caption contents
+                caption.updateCaption(uidl);
+
+                final boolean after = caption.shouldBePlacedAfterComponent();
+                final Element captionElement = caption.getElement();
+                final Element widgetElement = widget.getElement();
+
+                if (justAdded) {
+
+                    // As the caption has just been created, insert it to DOM
+                    if (after) {
+                        DOM.appendChild(captionWrapper, captionElement);
+                        DOM.setElementAttribute(captionWrapper, "class",
+                                "i-orderedlayout-w");
+                        caption.addStyleName("i-orderedlayout-c");
+                        widget.addStyleName("i-orderedlayout-w-e");
+                    } else {
+                        DOM.insertChild(captionWrapper, captionElement, 0);
+                    }
+
+                } else
+
+                // Caption exists. Move it to correct position if needed
+                if (after == (DOM.getChildIndex(captionWrapper, widgetElement) > DOM
+                        .getChildIndex(captionWrapper, captionElement))) {
+                    Element firstElement = DOM.getChild(captionWrapper, DOM
+                            .getChildCount(captionWrapper) - 2);
+                    if (firstElement != null) {
+                        DOM.removeChild(captionWrapper, firstElement);
+                        DOM.appendChild(captionWrapper, firstElement);
+                    }
+                    DOM.setElementAttribute(captionWrapper, "class",
+                            after ? "i-orderedlayout-w" : "");
+                    if (after) {
+                        caption.addStyleName("i-orderedlayout-c");
+                        widget.addStyleName("i-orderedlayout-w-e");
+                    } else {
+                        widget.removeStyleName("i-orderedlayout-w-e");
+                        caption.removeStyleName("i-orderedlayout-w-c");
+                    }
+                }
+
+            }
+
+            // Caption is not needed
+            else {
+
+                // Remove existing caption from DOM
+                if (caption != null) {
+                    DOM.removeChild(captionWrapper, caption.getElement());
+                    caption = null;
+                    DOM.setElementAttribute(captionWrapper, "class", "");
+                    widget.removeStyleName("i-orderedlayout-w-e");
+                    caption.removeStyleName("i-orderedlayout-w-c");
+                }
+            }
+        }
+
+        /**
+         * Set alignments for this wrapper.
+         */
+        void setAlignment(String verticalAlignment, String horizontalAlignment) {
+
+            // use one-cell table to implement horizontal alignments, only
+            // for values other than top-left (which is default)
+            if (!horizontalAlignment.equals("left")
+                    || !verticalAlignment.equals("top")) {
+
+                // The previous positioning has been left (or unspecified).
+                // Thus we need to create a one-cell-table to position
+                // this element.
+                if (alignmentTD == null) {
+
+                    // Store and remove the current childs (widget and caption)
+                    Element c1 = DOM
+                            .getFirstChild(getElementWrappingWidgetAndCaption());
+                    if (c1 != null) {
+                        DOM.removeChild(getElementWrappingWidgetAndCaption(),
+                                c1);
+                    }
+                    Element c2 = DOM
+                            .getFirstChild(getElementWrappingWidgetAndCaption());
+                    if (c2 != null) {
+                        DOM.removeChild(getElementWrappingWidgetAndCaption(),
+                                c2);
+                    }
+
+                    // Construct table structure to align children
+                    final String t = "<table cellpadding='0' cellspacing='0' width='100%' height='100%'><tbody><tr><td>"
+                            + "<table cellpadding='0' cellspacing='0' ><tbody><tr><td align='left'>"
+                            + "</td></tr></tbody></table></td></tr></tbody></table>";
+                    DOM.setInnerHTML(getElementWrappingWidgetAndCaption(), t);
+                    alignmentTD = DOM
+                            .getFirstChild(DOM
+                                    .getFirstChild(DOM
+                                            .getFirstChild(DOM
+                                                    .getFirstChild(getElementWrappingWidgetAndCaption()))));
+                    innermostTDinAlignmnetStructure = DOM.getFirstChild(DOM
+                            .getFirstChild(DOM.getFirstChild(DOM
+                                    .getFirstChild(alignmentTD))));
+
+                    // Restore children inside the
+                    if (c1 != null) {
+                        DOM.appendChild(innermostTDinAlignmnetStructure, c1);
+                        if (c2 != null) {
+                            DOM
+                                    .appendChild(
+                                            innermostTDinAlignmnetStructure, c2);
+                        }
+                    }
+
+                } else {
+
+                    // Go around optimization bug in WebKit and ensure repaint
+                    if (BrowserInfo.get().isSafari()) {
+                        String prevValue = DOM.getElementAttribute(alignmentTD,
+                                "align");
+                        if (!horizontalAlignment.equals(prevValue)) {
+                            Element parent = DOM.getParent(alignmentTD);
+                            DOM.removeChild(parent, alignmentTD);
+                            DOM.appendChild(parent, alignmentTD);
+                        }
+                    }
+
+                }
+
+                // Set the alignment in td
+                DOM.setElementAttribute(alignmentTD, "align",
+                        horizontalAlignment);
+                DOM.setElementAttribute(alignmentTD, "valign",
+                        verticalAlignment);
+
+            } else {
+
+                // In this case we are requested to position this left
+                // while as it has had some other position in the past.
+                // Thus the one-cell wrapper table must be removed.
+                if (alignmentTD != null) {
+
+                    // Move content to main container
+                    final Element itd = innermostTDinAlignmnetStructure;
+                    final Element alignmentTable = DOM.getParent(DOM
+                            .getParent(DOM.getParent(alignmentTD)));
+                    final Element target = DOM.getParent(alignmentTable);
+                    while (DOM.getChildCount(itd) > 0) {
+                        Element content = DOM.getFirstChild(itd);
+                        if (content != null) {
+                            DOM.removeChild(itd, content);
+                            DOM.appendChild(target, content);
+                        }
+                    }
+
+                    // Remove unneeded table element
+                    DOM.removeChild(target, alignmentTable);
+
+                    alignmentTD = innermostTDinAlignmnetStructure = null;
+                }
+            }
+        }
+
+        /** Set class for spacing */
+        void setSpacingAndMargins(boolean first, boolean last) {
+
+            final Element e = getElementWrappingWidgetAndCaption();
+
+            if (orientationMode == ORIENTATION_HORIZONTAL) {
+                DOM.setStyleAttribute(e, "paddingLeft", first ? (margins
+                        .hasLeft() ? marginLeft + "px" : "0")
+                        : (hasComponentSpacing ? hSpacing + "px" : "0"));
+                DOM.setStyleAttribute(e, "paddingRight", last ? (margins
+                        .hasRight() ? marginRight + "px" : "0") : "");
+                DOM.setStyleAttribute(e, "paddingTop",
+                        margins.hasTop() ? marginTop + "px" : "");
+                DOM.setStyleAttribute(e, "paddingBottom",
+                        margins.hasBottom() ? marginBottom + "px" : "");
+            } else {
+                DOM.setStyleAttribute(e, "paddingLeft",
+                        margins.hasLeft() ? marginLeft + "px" : "0");
+                DOM.setStyleAttribute(e, "paddingRight",
+                        margins.hasRight() ? marginRight + "px" : "0");
+                DOM.setStyleAttribute(e, "paddingTop", first ? (margins
+                        .hasTop() ? marginTop + "px" : "")
+                        : (hasComponentSpacing ? vSpacing + "px" : "0"));
+                DOM.setStyleAttribute(e, "paddingBottom", last
+                        && margins.hasBottom() ? marginBottom + "px" : "");
+            }
+        }
+    }
+
+    /* documented at super */
+    public void add(Widget child) {
+        add(child, childWidgets.size());
+    }
+
+    /**
+     * Add widget to this layout at given position.
+     * 
+     * This methods supports reinserting exiting child into layout - it just
+     * moves the position of the child in the layout.
+     */
+    public void add(Widget child, int atIndex) {
+        /*
+         * <b>Validate:</b> Perform any sanity checks to ensure the Panel can
+         * accept a new Widget. Examples: checking for a valid index on
+         * insertion; checking that the Panel is not full if there is a max
+         * capacity.
+         */
+        if (atIndex < 0 || atIndex > childWidgets.size()) {
+            return;
+        }
+
+        /*
+         * <b>Adjust for Reinsertion:</b> Some Panels need to handle the case
+         * where the Widget is already a child of this Panel. Example: when
+         * performing a reinsert, the index might need to be adjusted to account
+         * for the Widget's removal. See
+         * {@link ComplexPanel#adjustIndex(Widget, int)}.
+         */
+        if (childWidgets.contains(child)) {
+            if (childWidgets.indexOf(child) == atIndex) {
+                return;
+            }
+
+            final int removeFromIndex = childWidgets.indexOf(child);
+            final WidgetWrapper wrapper = (WidgetWrapper) childWidgetWrappers
+                    .get(removeFromIndex);
+            Element wrapperElement = wrapper.getElement();
+            final int nonWidgetChildElements = DOM
+                    .getChildCount(wrappedChildContainer)
+                    - childWidgets.size();
+            DOM.removeChild(wrappedChildContainer, wrapperElement);
+            DOM.insertChild(wrappedChildContainer, wrapperElement, atIndex
+                    + nonWidgetChildElements);
+            childWidgets.remove(removeFromIndex);
+            childWidgetWrappers.remove(removeFromIndex);
+            childWidgets.insertElementAt(child, atIndex);
+            childWidgetWrappers.insertElementAt(wrapper, atIndex);
+            return;
+        }
+
+        /*
+         * <b>Detach Child:</b> Remove the Widget from its existing parent, if
+         * any. Most Panels will simply call {@link Widget#removeFromParent()}
+         * on the Widget.
+         */
+        child.removeFromParent();
+
+        /*
+         * <b>Logical Attach:</b> Any state variables of the Panel should be
+         * updated to reflect the addition of the new Widget. Example: the
+         * Widget is added to the Panel's {@link WidgetCollection} at the
+         * appropriate index.
+         */
+        childWidgets.insertElementAt(child, atIndex);
+
+        /*
+         * <b>Physical Attach:</b> The Widget's Element must be physically
+         * attached to the Panel's Element, either directly or indirectly.
+         */
+        final WidgetWrapper wrapper = new WidgetWrapper();
+        final int nonWidgetChildElements = DOM
+                .getChildCount(wrappedChildContainer)
+                - childWidgetWrappers.size();
+        childWidgetWrappers.insertElementAt(wrapper, atIndex);
+        DOM.insertChild(wrappedChildContainer, wrapper.getElement(), atIndex
+                + nonWidgetChildElements);
+        DOM.appendChild(wrapper.getElementWrappingWidgetAndCaption(), child
+                .getElement());
+
+        /*
+         * <b>Adopt:</b> Call {@link #adopt(Widget)} to finalize the add as the
+         * very last step.
+         */
+        adopt(child);
+    }
+
+    /* documented at super */
+    public boolean remove(Widget child) {
+
+        /*
+         * <b>Validate:</b> Make sure this Panel is actually the parent of the
+         * child Widget; return <code>false</code> if it is not.
+         */
+        if (!childWidgets.contains(child)) {
+            return false;
+        }
+
+        /*
+         * <b>Orphan:</b> Call {@link #orphan(Widget)} first while the child
+         * Widget is still attached.
+         */
+        orphan(child);
+
+        /*
+         * <b>Physical Detach:</b> Adjust the DOM to account for the removal of
+         * the child Widget. The Widget's Element must be physically removed
+         * from the DOM.
+         */
+        final int index = childWidgets.indexOf(child);
+        final WidgetWrapper wrapper = (WidgetWrapper) childWidgetWrappers
+                .get(index);
+        DOM.removeChild(wrappedChildContainer, wrapper.getElement());
+        childWidgetWrappers.remove(index);
+
+        /*
+         * <b>Logical Detach:</b> Update the Panel's state variables to reflect
+         * the removal of the child Widget. Example: the Widget is removed from
+         * the Panel's {@link WidgetCollection}.
+         */
+        childWidgets.remove(index);
+
+        return true;
+    }
+
+    /* documented at super */
+    public boolean hasChildComponent(Widget component) {
+        return childWidgets.contains(component);
+    }
+
+    /* documented at super */
+    public void replaceChildComponent(Widget oldComponent, Widget newComponent) {
+        final int index = childWidgets.indexOf(oldComponent);
+        if (index >= 0) {
+            client.unregisterPaintable((Paintable) oldComponent);
+            remove(oldComponent);
+            add(newComponent, index);
+        }
+    }
+
+    /* documented at super */
+    public void updateCaption(Paintable component, UIDL uidl) {
+        final int index = childWidgets.indexOf(component);
+        if (index >= 0) {
+            ((WidgetWrapper) childWidgetWrappers.get(index)).updateCaption(
+                    uidl, component);
+        }
+    }
+
+    /* documented at super */
+    public Iterator iterator() {
+        return childWidgets.iterator();
+    }
+
+    /* documented at super */
+    public void iLayout() {
+        updateChildSizes();
+        Util.runDescendentsLayout(this);
+        childLayoutsHaveChanged = false;
+    }
+}
diff --git a/src/com/itmill/toolkit/terminal/gwt/client/client/ui/IPanel.java b/src/com/itmill/toolkit/terminal/gwt/client/client/ui/IPanel.java
new file mode 100644 (file)
index 0000000..8a40859
--- /dev/null
@@ -0,0 +1,388 @@
+/* 
+@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;
+
+    private int scrollTop;
+
+    private int scrollLeft;
+
+    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);
+        DOM.sinkEvents(contentNode, Event.ONSCROLL);
+    }
+
+    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);
+                }
+            }
+        }
+
+        if (uidl.hasVariable("scrollTop")
+                && uidl.getIntVariable("scrollTop") != scrollTop) {
+            scrollTop = uidl.getIntVariable("scrollTop");
+            DOM.setElementPropertyInt(contentNode, "scrollTop", scrollTop);
+        }
+
+        if (uidl.hasVariable("scrollLeft")
+                && uidl.getIntVariable("scrollLeft") != scrollLeft) {
+            scrollLeft = uidl.getIntVariable("scrollLeft");
+            DOM.setElementPropertyInt(contentNode, "scrollLeft", scrollLeft);
+        }
+
+    }
+
+    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 (type == Event.ONSCROLL) {
+            int newscrollTop = DOM.getElementPropertyInt(contentNode,
+                    "scrollTop");
+            int newscrollLeft = DOM.getElementPropertyInt(contentNode,
+                    "scrollLeft");
+            if (client != null
+                    && (newscrollLeft != scrollLeft || newscrollTop != scrollTop)) {
+                ApplicationConnection.getConsole().log("scrollded panel");
+                scrollLeft = newscrollLeft;
+                scrollTop = newscrollTop;
+                client.updateVariable(id, "scrollTop", scrollTop, false);
+                client.updateVariable(id, "scrollLeft", scrollLeft, false);
+            }
+        } else 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("");
+        } else {
+            super.setWidth(width);
+        }
+    }
+
+}
diff --git a/src/com/itmill/toolkit/terminal/gwt/client/client/ui/IPasswordField.java b/src/com/itmill/toolkit/terminal/gwt/client/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/gwt/client/client/ui/IPopupCalendar.java b/src/com/itmill/toolkit/terminal/gwt/client/client/ui/IPopupCalendar.java
new file mode 100644 (file)
index 0000000..63af2f7
--- /dev/null
@@ -0,0 +1,126 @@
+/* \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, 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
+                    \r
+                    // fix size\r
+                    popup.setWidth(w + "px");\r
+                    popup.setHeight(h + "px");\r
+                    \r
+                    popup.setPopupPosition(l, t\r
+                            + calendarToggle.getOffsetHeight() + 2);\r
+\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/gwt/client/client/ui/IProgressIndicator.java b/src/com/itmill/toolkit/terminal/gwt/client/client/ui/IProgressIndicator.java
new file mode 100644 (file)
index 0000000..67100b8
--- /dev/null
@@ -0,0 +1,96 @@
+/* 
+@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;
+    private boolean pollerSuspendedDueDetach;
+
+    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"));
+        }
+    }
+
+    protected void onAttach() {
+        super.onAttach();
+        if (pollerSuspendedDueDetach) {
+            poller.run();
+        }
+    }
+
+    protected void onDetach() {
+        super.onDetach();
+        poller.cancel();
+        pollerSuspendedDueDetach = true;
+    }
+
+    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/gwt/client/client/ui/IScrollTable.java b/src/com/itmill/toolkit/terminal/gwt/client/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/gwt/client/client/ui/ISlider.java b/src/com/itmill/toolkit/terminal/gwt/client/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/gwt/client/client/ui/ISplitPanel.java b/src/com/itmill/toolkit/terminal/gwt/client/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/gwt/client/client/ui/ISplitPanelHorizontal.java b/src/com/itmill/toolkit/terminal/gwt/client/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/gwt/client/client/ui/ISplitPanelVertical.java b/src/com/itmill/toolkit/terminal/gwt/client/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/gwt/client/client/ui/ITablePaging.java b/src/com/itmill/toolkit/terminal/gwt/client/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/gwt/client/client/ui/ITabsheet.java b/src/com/itmill/toolkit/terminal/gwt/client/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/gwt/client/client/ui/ITabsheetBase.java b/src/com/itmill/toolkit/terminal/gwt/client/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/gwt/client/client/ui/ITabsheetPanel.java b/src/com/itmill/toolkit/terminal/gwt/client/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/gwt/client/client/ui/ITextArea.java b/src/com/itmill/toolkit/terminal/gwt/client/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/gwt/client/client/ui/ITextField.java b/src/com/itmill/toolkit/terminal/gwt/client/client/ui/ITextField.java
new file mode 100644 (file)
index 0000000..5438cc9
--- /dev/null
@@ -0,0 +1,204 @@
+/* 
+@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.ContainerResizedListener;
+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;
+
+/**
+ * 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, ContainerResizedListener {
+
+    /**
+     * 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;
+    private float proportionalHeight = -1;
+    private float proportionalWidth = -1;
+    private int extraHorizontalPixels = -1;
+    private int extraVerticalPixels = -1;
+
+    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) {}
+    }-*/;
+
+    public void setHeight(String height) {
+        if (height != null && height.indexOf("%") > 0) {
+            // special handling for proportional height
+            proportionalHeight = Float.parseFloat(height.substring(0, height
+                    .indexOf("%"))) / 100;
+            iLayout();
+        } else {
+            super.setHeight(height);
+            proportionalHeight = -1;
+        }
+    }
+
+    public void setWidth(String width) {
+        if (width != null && width.indexOf("%") > 0) {
+            // special handling for proportional w
+            proportionalWidth = Float.parseFloat(width.substring(0, width
+                    .indexOf("%"))) / 100;
+            iLayout();
+        } else {
+            super.setHeight(width);
+            proportionalWidth = -1;
+        }
+    }
+
+    public void iLayout() {
+        if (proportionalWidth >= 0) {
+            int availPixels = (int) (DOM.getElementPropertyInt(DOM
+                    .getParent(getElement()), "clientWidth") * proportionalWidth);
+            availPixels -= getExtraHorizontalPixels();
+            super.setWidth(availPixels + "px");
+        }
+        if (proportionalHeight >= 0) {
+            int availPixels = (int) (DOM.getElementPropertyInt(DOM
+                    .getParent(getElement()), "clientHeight") * proportionalHeight);
+            availPixels -= getExtraVerticalPixels();
+            super.setHeight(availPixels + "px");
+        }
+    }
+
+    /**
+     * @return space used by components paddings and borders
+     */
+    private int getExtraHorizontalPixels() {
+        if (extraHorizontalPixels < 0) {
+            detectExtraSizes();
+        }
+        return extraHorizontalPixels;
+    }
+
+    /**
+     * @return space used by components paddings and borders
+     */
+    private int getExtraVerticalPixels() {
+        if (extraVerticalPixels < 0) {
+            detectExtraSizes();
+        }
+        return extraVerticalPixels;
+    }
+
+    /**
+     * Detects space used by components paddings and borders. Used when
+     * relational size are used.
+     */
+    private void detectExtraSizes() {
+        Element clone = Util.cloneNode(getElement(), false);
+        DOM.setElementAttribute(clone, "id", "");
+        DOM.setStyleAttribute(clone, "visibility", "hidden");
+        DOM.setStyleAttribute(clone, "position", "absolute");
+        // due FF3 bug set size to 10px and later subtract it from extra pixels
+        DOM.setStyleAttribute(clone, "width", "10px");
+        DOM.setStyleAttribute(clone, "height", "10px");
+        DOM.appendChild(DOM.getParent(getElement()), clone);
+        extraHorizontalPixels = DOM.getElementPropertyInt(clone, "offsetWidth") - 10;
+        extraVerticalPixels = DOM.getElementPropertyInt(clone, "offsetHeight") - 10;
+        DOM.removeChild(DOM.getParent(getElement()), clone);
+    }
+
+}
diff --git a/src/com/itmill/toolkit/terminal/gwt/client/client/ui/ITextualDate.java b/src/com/itmill/toolkit/terminal/gwt/client/client/ui/ITextualDate.java
new file mode 100644 (file)
index 0000000..a08dcba
--- /dev/null
@@ -0,0 +1,279 @@
+/* \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.DOM;\r
+import com.google.gwt.user.client.ui.ChangeListener;\r
+import com.google.gwt.user.client.ui.TextBox;\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.BrowserInfo;\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
+\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 TextBox 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 TextBox();\r
+        // use normal textfield styles as a basis\r
+        text.setStyleName(ITextField.CLASSNAME);\r
+        // add datefield spesific style name also\r
+        text.addStyleName(CLASSNAME + "-textfield");\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 (BrowserInfo.get().isIE6()) {\r
+                // in IE6 cols ~ min-width\r
+                DOM.setElementProperty(text.getElement(), "size", "1");\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
+                if (BrowserInfo.get().isIE6()) {\r
+                    // revert IE6 hack\r
+                    DOM.setElementProperty(text.getElement(), "size", "");\r
+                }\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/gwt/client/client/ui/ITree.java b/src/com/itmill/toolkit/terminal/gwt/client/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/gwt/client/client/ui/ITwinColSelect.java b/src/com/itmill/toolkit/terminal/gwt/client/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/gwt/client/client/ui/IUnknownComponent.java b/src/com/itmill/toolkit/terminal/gwt/client/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/gwt/client/client/ui/IUpload.java b/src/com/itmill/toolkit/terminal/gwt/client/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/gwt/client/client/ui/IView.java b/src/com/itmill/toolkit/terminal/gwt/client/client/ui/IView.java
new file mode 100644 (file)
index 0000000..13a41fc
--- /dev/null
@@ -0,0 +1,295 @@
+/* 
+@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 optimization */
+    private int width;
+
+    /** stored height for IE resize optimization */
+    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;
+    }
+
+    /**
+     * Used to reload host page on theme changes.
+     */
+    private static native void reloadHostPage()
+    /*-{
+         $wnd.location.reload(); 
+     }-*/;
+
+    public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+
+        id = uidl.getId();
+
+        String newTheme = uidl.getStringAttribute("theme");
+        if (theme != null && !newTheme.equals(theme)) {
+            // Complete page refresh is needed due css can affect layout
+            // calculations etc
+            reloadHostPage();
+        } else {
+            theme = newTheme;
+        }
+        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/gwt/client/client/ui/IWindow.java b/src/com/itmill/toolkit/terminal/gwt/client/client/ui/IWindow.java
new file mode 100644 (file)
index 0000000..76c7a9e
--- /dev/null
@@ -0,0 +1,678 @@
+/* 
+@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 (BrowserInfo.get().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 = Util.escapeHTML(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);
+
+        // Handle window caption tooltips
+        if (client != null && DOM.isOrHasChild(header, target)) {
+            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/gwt/client/client/ui/Icon.java b/src/com/itmill/toolkit/terminal/gwt/client/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/gwt/client/client/ui/MarginInfo.java b/src/com/itmill/toolkit/terminal/gwt/client/client/ui/MarginInfo.java
new file mode 100644 (file)
index 0000000..68d2e2e
--- /dev/null
@@ -0,0 +1,68 @@
+/* 
+@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;
+        }
+    }
+
+    public boolean equals(Object obj) {
+        if (!(obj instanceof MarginInfo)) {
+            return false;
+        }
+
+        return ((MarginInfo) obj).bitMask == bitMask;
+    }
+
+}
diff --git a/src/com/itmill/toolkit/terminal/gwt/client/client/ui/MenuBar.java b/src/com/itmill/toolkit/terminal/gwt/client/client/ui/MenuBar.java
new file mode 100644 (file)
index 0000000..b446e7d
--- /dev/null
@@ -0,0 +1,513 @@
+/* 
+@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>
+ * 
+ * @deprecated
+ */
+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/gwt/client/client/ui/MenuItem.java b/src/com/itmill/toolkit/terminal/gwt/client/client/ui/MenuItem.java
new file mode 100644 (file)
index 0000000..a9651e8
--- /dev/null
@@ -0,0 +1,188 @@
+/* 
+@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.
+ * 
+ * @deprecated
+ */
+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/gwt/client/client/ui/Notification.java b/src/com/itmill/toolkit/terminal/gwt/client/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/gwt/client/client/ui/ShortcutActionHandler.java b/src/com/itmill/toolkit/terminal/gwt/client/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/gwt/client/client/ui/Table.java b/src/com/itmill/toolkit/terminal/gwt/client/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/gwt/client/client/ui/Time.java b/src/com/itmill/toolkit/terminal/gwt/client/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/gwt/client/client/ui/ToolkitOverlay.java b/src/com/itmill/toolkit/terminal/gwt/client/client/ui/ToolkitOverlay.java
new file mode 100644 (file)
index 0000000..306c864
--- /dev/null
@@ -0,0 +1,147 @@
+/* 
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.itmill.toolkit.terminal.gwt.client.ui;
+
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.ui.HTML;
+import com.google.gwt.user.client.ui.PopupListener;
+import com.google.gwt.user.client.ui.PopupPanel;
+import com.google.gwt.user.client.ui.RootPanel;
+import com.google.gwt.user.client.ui.Widget;
+import com.itmill.toolkit.terminal.gwt.client.BrowserInfo;
+
+/**
+ * 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;
+
+    private Shadow shadow;
+
+    public ToolkitOverlay() {
+        super();
+        adjustZIndex();
+    }
+
+    public ToolkitOverlay(boolean autoHide) {
+        super(autoHide);
+        adjustZIndex();
+    }
+
+    public ToolkitOverlay(boolean autoHide, boolean modal) {
+        super(autoHide, modal);
+        adjustZIndex();
+    }
+
+    public ToolkitOverlay(boolean autoHide, boolean modal, boolean showShadow) {
+        super(autoHide, modal);
+        if (showShadow) {
+            shadow = new Shadow(this);
+        }
+        adjustZIndex();
+    }
+
+    private void adjustZIndex() {
+        DOM.setStyleAttribute(getElement(), "zIndex", "" + Z_INDEX);
+    }
+
+    public void setPopupPosition(int left, int top) {
+        super.setPopupPosition(left, top);
+        if (shadow != null) {
+            shadow.updateSizeAndPosition();
+        }
+    }
+
+    public void show() {
+        super.show();
+        if (shadow != null) {
+            DOM.appendChild(RootPanel.get().getElement(), shadow.getElement());
+            shadow.updateSizeAndPosition();
+        }
+    }
+    
+    public void setShadowOffset(int top, int right, int bottom, int left) {
+        if(shadow != null) {
+            shadow.setOffset(top, right, bottom, left);
+        }
+    }
+
+    private class Shadow extends HTML {
+
+        private static final String CLASSNAME = "i-shadow";
+
+        private static final String HTML = "<div class=\"top-left\"></div><div class=\"top\"></div><div class=\"top-right\"></div><div class=\"left\"></div><div class=\"center\"></div><div class=\"right\"></div><div class=\"bottom-left\"></div><div class=\"bottom\"></div><div class=\"bottom-right\"></div>";
+
+        private Widget overlay;
+
+        // Amount of shadow on each side.
+        private int top = 2;
+        private int right = 5;
+        private int bottom = 6;
+        private int left = 5;
+
+        public Shadow(ToolkitOverlay overlay) {
+            super(HTML);
+            setStyleName(CLASSNAME);
+            DOM.setStyleAttribute(getElement(), "position", "absolute");
+
+            this.overlay = overlay;
+            overlay.addPopupListener(new PopupListener() {
+                public void onPopupClosed(PopupPanel sender, boolean autoClosed) {
+                    DOM.removeChild(RootPanel.get().getElement(), shadow.getElement());
+                }
+            });
+        }
+
+        public void updateSizeAndPosition() {
+            // Calculate proper z-index
+            String zIndex = DOM.getStyleAttribute(overlay.getElement(),
+                    "zIndex");
+            if (zIndex == null) {
+                zIndex = "" + Z_INDEX;
+            }
+
+            // Calculate position and size
+            if(BrowserInfo.get().isIE()) {
+                // Shake IE
+                overlay.getOffsetHeight();
+                overlay.getOffsetWidth();
+            }
+            int x = overlay.getAbsoluteLeft() - left;
+            int y = overlay.getAbsoluteTop() - top;
+            int width = overlay.getOffsetWidth() + left + right;
+            int height = overlay.getOffsetHeight() + top + bottom;
+            if (width < 0) {
+                width = 0;
+            }
+            if (height < 0) {
+                height = 0;
+            }
+
+            // Update correct values
+            DOM.setStyleAttribute(shadow.getElement(), "zIndex", ""
+                    + (Integer.parseInt(zIndex) - 1));
+            DOM.setStyleAttribute(getElement(), "width", width + "px");
+            DOM.setStyleAttribute(getElement(), "height", height + "px");
+            DOM.setStyleAttribute(getElement(), "top", y + "px");
+            DOM.setStyleAttribute(getElement(), "left", x + "px");
+        }
+        
+        public void setOffset(int top, int right, int bottom, int left) {
+            this.top = top;
+            this.right = right;
+            this.bottom = bottom;
+            this.left = left;
+            if(overlay.isAttached()) {
+                updateSizeAndPosition();
+            }
+        }
+
+    }
+
+}
diff --git a/src/com/itmill/toolkit/terminal/gwt/client/client/ui/TreeAction.java b/src/com/itmill/toolkit/terminal/gwt/client/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/gwt/client/client/ui/TreeImages.java b/src/com/itmill/toolkit/terminal/gwt/client/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/gwt/client/client/ui/absolutegrid/AbsoluteGrid.java b/src/com/itmill/toolkit/terminal/gwt/client/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/gwt/client/client/ui/absolutegrid/ISizeableGridLayout.java b/src/com/itmill/toolkit/terminal/gwt/client/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/gwt/client/client/ui/richtextarea/IRichTextArea.java b/src/com/itmill/toolkit/terminal/gwt/client/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/gwt/client/client/ui/richtextarea/RichTextToolbar$Strings.properties b/src/com/itmill/toolkit/terminal/gwt/client/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/gwt/client/client/ui/richtextarea/RichTextToolbar.java b/src/com/itmill/toolkit/terminal/gwt/client/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/gwt/client/client/ui/richtextarea/backColors.gif b/src/com/itmill/toolkit/terminal/gwt/client/client/ui/richtextarea/backColors.gif
new file mode 100644 (file)
index 0000000..ddfc1ce
Binary files /dev/null and b/src/com/itmill/toolkit/terminal/gwt/client/client/ui/richtextarea/backColors.gif differ
diff --git a/src/com/itmill/toolkit/terminal/gwt/client/client/ui/richtextarea/bold.gif b/src/com/itmill/toolkit/terminal/gwt/client/client/ui/richtextarea/bold.gif
new file mode 100644 (file)
index 0000000..249e5af
Binary files /dev/null and b/src/com/itmill/toolkit/terminal/gwt/client/client/ui/richtextarea/bold.gif differ
diff --git a/src/com/itmill/toolkit/terminal/gwt/client/client/ui/richtextarea/createLink.gif b/src/com/itmill/toolkit/terminal/gwt/client/client/ui/richtextarea/createLink.gif
new file mode 100644 (file)
index 0000000..3ab9e59
Binary files /dev/null and b/src/com/itmill/toolkit/terminal/gwt/client/client/ui/richtextarea/createLink.gif differ
diff --git a/src/com/itmill/toolkit/terminal/gwt/client/client/ui/richtextarea/fontSizes.gif b/src/com/itmill/toolkit/terminal/gwt/client/client/ui/richtextarea/fontSizes.gif
new file mode 100644 (file)
index 0000000..c2f4c8c
Binary files /dev/null and b/src/com/itmill/toolkit/terminal/gwt/client/client/ui/richtextarea/fontSizes.gif differ
diff --git a/src/com/itmill/toolkit/terminal/gwt/client/client/ui/richtextarea/fonts.gif b/src/com/itmill/toolkit/terminal/gwt/client/client/ui/richtextarea/fonts.gif
new file mode 100644 (file)
index 0000000..1629cab
Binary files /dev/null and b/src/com/itmill/toolkit/terminal/gwt/client/client/ui/richtextarea/fonts.gif differ
diff --git a/src/com/itmill/toolkit/terminal/gwt/client/client/ui/richtextarea/foreColors.gif b/src/com/itmill/toolkit/terminal/gwt/client/client/ui/richtextarea/foreColors.gif
new file mode 100644 (file)
index 0000000..2bb89ef
Binary files /dev/null and b/src/com/itmill/toolkit/terminal/gwt/client/client/ui/richtextarea/foreColors.gif differ
diff --git a/src/com/itmill/toolkit/terminal/gwt/client/client/ui/richtextarea/gwtLogo.png b/src/com/itmill/toolkit/terminal/gwt/client/client/ui/richtextarea/gwtLogo.png
new file mode 100644 (file)
index 0000000..8072818
Binary files /dev/null and b/src/com/itmill/toolkit/terminal/gwt/client/client/ui/richtextarea/gwtLogo.png differ
diff --git a/src/com/itmill/toolkit/terminal/gwt/client/client/ui/richtextarea/hr.gif b/src/com/itmill/toolkit/terminal/gwt/client/client/ui/richtextarea/hr.gif
new file mode 100644 (file)
index 0000000..3fb1607
Binary files /dev/null and b/src/com/itmill/toolkit/terminal/gwt/client/client/ui/richtextarea/hr.gif differ
diff --git a/src/com/itmill/toolkit/terminal/gwt/client/client/ui/richtextarea/indent.gif b/src/com/itmill/toolkit/terminal/gwt/client/client/ui/richtextarea/indent.gif
new file mode 100644 (file)
index 0000000..8b837f0
Binary files /dev/null and b/src/com/itmill/toolkit/terminal/gwt/client/client/ui/richtextarea/indent.gif differ
diff --git a/src/com/itmill/toolkit/terminal/gwt/client/client/ui/richtextarea/insertImage.gif b/src/com/itmill/toolkit/terminal/gwt/client/client/ui/richtextarea/insertImage.gif
new file mode 100644 (file)
index 0000000..db61c9a
Binary files /dev/null and b/src/com/itmill/toolkit/terminal/gwt/client/client/ui/richtextarea/insertImage.gif differ
diff --git a/src/com/itmill/toolkit/terminal/gwt/client/client/ui/richtextarea/italic.gif b/src/com/itmill/toolkit/terminal/gwt/client/client/ui/richtextarea/italic.gif
new file mode 100644 (file)
index 0000000..2b0a5a0
Binary files /dev/null and b/src/com/itmill/toolkit/terminal/gwt/client/client/ui/richtextarea/italic.gif differ
diff --git a/src/com/itmill/toolkit/terminal/gwt/client/client/ui/richtextarea/justifyCenter.gif b/src/com/itmill/toolkit/terminal/gwt/client/client/ui/richtextarea/justifyCenter.gif
new file mode 100644 (file)
index 0000000..7d22640
Binary files /dev/null and b/src/com/itmill/toolkit/terminal/gwt/client/client/ui/richtextarea/justifyCenter.gif differ
diff --git a/src/com/itmill/toolkit/terminal/gwt/client/client/ui/richtextarea/justifyLeft.gif b/src/com/itmill/toolkit/terminal/gwt/client/client/ui/richtextarea/justifyLeft.gif
new file mode 100644 (file)
index 0000000..3c0f350
Binary files /dev/null and b/src/com/itmill/toolkit/terminal/gwt/client/client/ui/richtextarea/justifyLeft.gif differ
diff --git a/src/com/itmill/toolkit/terminal/gwt/client/client/ui/richtextarea/justifyRight.gif b/src/com/itmill/toolkit/terminal/gwt/client/client/ui/richtextarea/justifyRight.gif
new file mode 100644 (file)
index 0000000..99ee258
Binary files /dev/null and b/src/com/itmill/toolkit/terminal/gwt/client/client/ui/richtextarea/justifyRight.gif differ
diff --git a/src/com/itmill/toolkit/terminal/gwt/client/client/ui/richtextarea/ol.gif b/src/com/itmill/toolkit/terminal/gwt/client/client/ui/richtextarea/ol.gif
new file mode 100644 (file)
index 0000000..833bb40
Binary files /dev/null and b/src/com/itmill/toolkit/terminal/gwt/client/client/ui/richtextarea/ol.gif differ
diff --git a/src/com/itmill/toolkit/terminal/gwt/client/client/ui/richtextarea/outdent.gif b/src/com/itmill/toolkit/terminal/gwt/client/client/ui/richtextarea/outdent.gif
new file mode 100644 (file)
index 0000000..be86624
Binary files /dev/null and b/src/com/itmill/toolkit/terminal/gwt/client/client/ui/richtextarea/outdent.gif differ
diff --git a/src/com/itmill/toolkit/terminal/gwt/client/client/ui/richtextarea/removeFormat.gif b/src/com/itmill/toolkit/terminal/gwt/client/client/ui/richtextarea/removeFormat.gif
new file mode 100644 (file)
index 0000000..a4339c0
Binary files /dev/null and b/src/com/itmill/toolkit/terminal/gwt/client/client/ui/richtextarea/removeFormat.gif differ
diff --git a/src/com/itmill/toolkit/terminal/gwt/client/client/ui/richtextarea/removeLink.gif b/src/com/itmill/toolkit/terminal/gwt/client/client/ui/richtextarea/removeLink.gif
new file mode 100644 (file)
index 0000000..522ab4b
Binary files /dev/null and b/src/com/itmill/toolkit/terminal/gwt/client/client/ui/richtextarea/removeLink.gif differ
diff --git a/src/com/itmill/toolkit/terminal/gwt/client/client/ui/richtextarea/strikeThrough.gif b/src/com/itmill/toolkit/terminal/gwt/client/client/ui/richtextarea/strikeThrough.gif
new file mode 100644 (file)
index 0000000..6b174c8
Binary files /dev/null and b/src/com/itmill/toolkit/terminal/gwt/client/client/ui/richtextarea/strikeThrough.gif differ
diff --git a/src/com/itmill/toolkit/terminal/gwt/client/client/ui/richtextarea/subscript.gif b/src/com/itmill/toolkit/terminal/gwt/client/client/ui/richtextarea/subscript.gif
new file mode 100644 (file)
index 0000000..04bba05
Binary files /dev/null and b/src/com/itmill/toolkit/terminal/gwt/client/client/ui/richtextarea/subscript.gif differ
diff --git a/src/com/itmill/toolkit/terminal/gwt/client/client/ui/richtextarea/superscript.gif b/src/com/itmill/toolkit/terminal/gwt/client/client/ui/richtextarea/superscript.gif
new file mode 100644 (file)
index 0000000..ac478ee
Binary files /dev/null and b/src/com/itmill/toolkit/terminal/gwt/client/client/ui/richtextarea/superscript.gif differ
diff --git a/src/com/itmill/toolkit/terminal/gwt/client/client/ui/richtextarea/ul.gif b/src/com/itmill/toolkit/terminal/gwt/client/client/ui/richtextarea/ul.gif
new file mode 100644 (file)
index 0000000..01380db
Binary files /dev/null and b/src/com/itmill/toolkit/terminal/gwt/client/client/ui/richtextarea/ul.gif differ
diff --git a/src/com/itmill/toolkit/terminal/gwt/client/client/ui/richtextarea/underline.gif b/src/com/itmill/toolkit/terminal/gwt/client/client/ui/richtextarea/underline.gif
new file mode 100644 (file)
index 0000000..82bae11
Binary files /dev/null and b/src/com/itmill/toolkit/terminal/gwt/client/client/ui/richtextarea/underline.gif differ