summaryrefslogtreecommitdiffstats
path: root/src/com/vaadin/terminal/gwt
diff options
context:
space:
mode:
authorHenri Sara <henri.sara@itmill.com>2009-05-11 09:19:03 +0000
committerHenri Sara <henri.sara@itmill.com>2009-05-11 09:19:03 +0000
commitadc8c0ad3573272c236040c3a76005b9e73a5737 (patch)
treea3860704dbd5b82dc6af38684b80f8ef79a32722 /src/com/vaadin/terminal/gwt
parent5abc870dda584d0c2fc47fd5eec4ae3de3fa240e (diff)
downloadvaadin-framework-adc8c0ad3573272c236040c3a76005b9e73a5737.tar.gz
vaadin-framework-adc8c0ad3573272c236040c3a76005b9e73a5737.zip
#2904: initial bulk rename "com.itmill.toolkit" -> "com.vaadin"
- com.itmill.toolkit.external not yet fully renamed svn changeset:7715/svn branch:6.0
Diffstat (limited to 'src/com/vaadin/terminal/gwt')
-rw-r--r--src/com/vaadin/terminal/gwt/DefaultWidgetSet.gwt.xml29
-rw-r--r--src/com/vaadin/terminal/gwt/DefaultWidgetSetNoEntry.gwt.xml6
-rw-r--r--src/com/vaadin/terminal/gwt/client/ApplicationConfiguration.java201
-rwxr-xr-xsrc/com/vaadin/terminal/gwt/client/ApplicationConnection.java1633
-rw-r--r--src/com/vaadin/terminal/gwt/client/BrowserInfo.java225
-rw-r--r--src/com/vaadin/terminal/gwt/client/CSSRule.java85
-rw-r--r--src/com/vaadin/terminal/gwt/client/ClientExceptionHandler.java27
-rw-r--r--src/com/vaadin/terminal/gwt/client/ComponentDetail.java88
-rw-r--r--src/com/vaadin/terminal/gwt/client/ComponentDetailMap.java72
-rw-r--r--src/com/vaadin/terminal/gwt/client/ComponentLocator.java343
-rw-r--r--src/com/vaadin/terminal/gwt/client/Console.java26
-rw-r--r--src/com/vaadin/terminal/gwt/client/Container.java71
-rw-r--r--src/com/vaadin/terminal/gwt/client/ContainerResizedListener.java21
-rw-r--r--src/com/vaadin/terminal/gwt/client/DateTimeService.java237
-rw-r--r--src/com/vaadin/terminal/gwt/client/DefaultWidgetSet.java280
-rw-r--r--src/com/vaadin/terminal/gwt/client/Focusable.java16
-rw-r--r--src/com/vaadin/terminal/gwt/client/ICaption.java443
-rw-r--r--src/com/vaadin/terminal/gwt/client/ICaptionWrapper.java32
-rwxr-xr-xsrc/com/vaadin/terminal/gwt/client/IDebugConsole.java466
-rw-r--r--src/com/vaadin/terminal/gwt/client/IErrorMessage.java73
-rw-r--r--src/com/vaadin/terminal/gwt/client/ITooltip.java225
-rw-r--r--src/com/vaadin/terminal/gwt/client/LocaleNotLoadedException.java13
-rw-r--r--src/com/vaadin/terminal/gwt/client/LocaleService.java197
-rw-r--r--src/com/vaadin/terminal/gwt/client/MouseEventDetails.java93
-rw-r--r--src/com/vaadin/terminal/gwt/client/NullConsole.java36
-rw-r--r--src/com/vaadin/terminal/gwt/client/Paintable.java10
-rw-r--r--src/com/vaadin/terminal/gwt/client/RenderInformation.java125
-rw-r--r--src/com/vaadin/terminal/gwt/client/RenderSpace.java53
-rw-r--r--src/com/vaadin/terminal/gwt/client/StyleConstants.java17
-rw-r--r--src/com/vaadin/terminal/gwt/client/TooltipInfo.java28
-rw-r--r--src/com/vaadin/terminal/gwt/client/UIDL.java472
-rw-r--r--src/com/vaadin/terminal/gwt/client/Util.java703
-rw-r--r--src/com/vaadin/terminal/gwt/client/WidgetSet.java34
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/Action.java55
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/ActionOwner.java16
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/AlignmentInfo.java89
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/CalendarEntry.java126
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/Field.java13
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/IAbsoluteLayout.java378
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/IAccordion.java647
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/IButton.java190
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/ICalendarPanel.java520
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/ICheckBox.java140
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/IContextMenu.java158
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/ICustomComponent.java153
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/ICustomLayout.java644
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/IDateField.java233
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/IDateFieldCalendar.java26
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/IEmbedded.java225
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/IFilterSelect.java1059
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/IForm.java288
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/IFormLayout.java464
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/IGridLayout.java1018
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/IHorizontalLayout.java11
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/ILabel.java123
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/ILink.java182
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/IListSelect.java141
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/IMarginInfo.java76
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/IMenuBar.java644
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/INativeSelect.java111
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/INotification.java323
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/IOptionGroup.java102
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/IOptionGroupBase.java229
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/IOrderedLayout.java876
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/IPanel.java516
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/IPasswordField.java21
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/IPopupCalendar.java130
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/IPopupView.java417
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/IProgressIndicator.java100
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/IScrollTable.java2841
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/ISlider.java436
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/ISplitPanel.java584
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/ISplitPanelHorizontal.java12
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/ISplitPanelVertical.java12
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/ITablePaging.java439
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/ITabsheet.java841
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/ITabsheetBase.java144
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/ITabsheetPanel.java183
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/ITextArea.java72
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/ITextField.java270
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/ITextualDate.java301
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/ITime.java317
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/IToolkitOverlay.java313
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/ITree.java469
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/ITwinColSelect.java244
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/IUnknownComponent.java40
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/IUpload.java152
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/IUriFragmentUtility.java67
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/IVerticalLayout.java11
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/IView.java575
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/IWindow.java1001
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/Icon.java44
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/MenuBar.java514
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/MenuItem.java189
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/ShortcutActionHandler.java178
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/SubPartAware.java11
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/Table.java15
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/TreeAction.java56
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/TreeImages.java31
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_alignment.pngbin0 -> 14779 bytes
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_component_handles_the_caption.pngbin0 -> 5676 bytes
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_h150.pngbin0 -> 9802 bytes
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_horizontal.pngbin0 -> 5769 bytes
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_horizontal_spacing.pngbin0 -> 5862 bytes
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_margin.pngbin0 -> 6828 bytes
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_no_caption.pngbin0 -> 2113 bytes
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_normal_caption.pngbin0 -> 3571 bytes
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_special-margin.pngbin0 -> 3763 bytes
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_vertical.pngbin0 -> 6140 bytes
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_vertical_spacing.pngbin0 -> 6652 bytes
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_w300.pngbin0 -> 9543 bytes
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_w300_h150.pngbin0 -> 10067 bytes
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/layout/CellBasedLayout.java335
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/layout/ChildComponentContainer.java736
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/layout/Margins.java83
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/richtextarea/IRichTextArea.java251
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/richtextarea/IRichTextToolbar$Strings.properties35
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/richtextarea/IRichTextToolbar.java474
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/richtextarea/backColors.gifbin0 -> 104 bytes
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/richtextarea/bold.gifbin0 -> 864 bytes
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/richtextarea/createLink.gifbin0 -> 118 bytes
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/richtextarea/fontSizes.gifbin0 -> 96 bytes
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/richtextarea/fonts.gifbin0 -> 147 bytes
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/richtextarea/foreColors.gifbin0 -> 173 bytes
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/richtextarea/gwtLogo.pngbin0 -> 11454 bytes
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/richtextarea/hr.gifbin0 -> 67 bytes
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/richtextarea/indent.gifbin0 -> 82 bytes
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/richtextarea/insertImage.gifbin0 -> 290 bytes
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/richtextarea/italic.gifbin0 -> 79 bytes
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/richtextarea/justifyCenter.gifbin0 -> 70 bytes
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/richtextarea/justifyLeft.gifbin0 -> 71 bytes
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/richtextarea/justifyRight.gifbin0 -> 855 bytes
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/richtextarea/ol.gifbin0 -> 76 bytes
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/richtextarea/outdent.gifbin0 -> 82 bytes
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/richtextarea/removeFormat.gifbin0 -> 360 bytes
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/richtextarea/removeLink.gifbin0 -> 895 bytes
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/richtextarea/strikeThrough.gifbin0 -> 80 bytes
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/richtextarea/subscript.gifbin0 -> 80 bytes
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/richtextarea/superscript.gifbin0 -> 80 bytes
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/richtextarea/ul.gifbin0 -> 863 bytes
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/richtextarea/underline.gifbin0 -> 88 bytes
-rw-r--r--src/com/vaadin/terminal/gwt/server/AbstractApplicationServlet.java1797
-rw-r--r--src/com/vaadin/terminal/gwt/server/ApplicationPortlet.java93
-rw-r--r--src/com/vaadin/terminal/gwt/server/ApplicationRunnerServlet.java173
-rw-r--r--src/com/vaadin/terminal/gwt/server/ApplicationServlet.java84
-rw-r--r--src/com/vaadin/terminal/gwt/server/ChangeVariablesErrorEvent.java35
-rw-r--r--src/com/vaadin/terminal/gwt/server/CommunicationManager.java1497
-rw-r--r--src/com/vaadin/terminal/gwt/server/ComponentSizeValidator.java678
-rw-r--r--src/com/vaadin/terminal/gwt/server/HttpUploadStream.java91
-rw-r--r--src/com/vaadin/terminal/gwt/server/JsonPaintTarget.java1108
-rw-r--r--src/com/vaadin/terminal/gwt/server/PortletApplicationContext.java290
-rw-r--r--src/com/vaadin/terminal/gwt/server/SessionExpired.java6
-rw-r--r--src/com/vaadin/terminal/gwt/server/SystemMessageException.java54
-rw-r--r--src/com/vaadin/terminal/gwt/server/WebApplicationContext.java286
-rw-r--r--src/com/vaadin/terminal/gwt/server/WebBrowser.java98
155 files changed, 35390 insertions, 0 deletions
diff --git a/src/com/vaadin/terminal/gwt/DefaultWidgetSet.gwt.xml b/src/com/vaadin/terminal/gwt/DefaultWidgetSet.gwt.xml
new file mode 100644
index 0000000000..1606dea3c3
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/DefaultWidgetSet.gwt.xml
@@ -0,0 +1,29 @@
+<module>
+ <!--
+ This GWT module defines the IT Mill Toolkit DefaultWidgetSet. This is
+ the module you want to extend when creating an extended widget set, or
+ when creating a specialized widget set with a subset of the
+ components.
+ -->
+ <!--
+ NOTE that your WidgetSet entry-point (.java) should have the same
+ "logical" name (a.k.a SimpleName) as the specification (.gwt.xml).
+ -->
+ <!--
+ E.g: com/example/gwt/MyWidgetSet.gwt.xml should point to the
+ entry-point
+ com.example.gwt.client[.some.package].MyWidgetSet.java
+ -->
+
+ <inherits name="com.google.gwt.user.User" />
+
+ <inherits name="com.google.gwt.http.HTTP" />
+
+ <inherits name="com.google.gwt.xml.XML" />
+
+ <inherits name="com.google.gwt.json.JSON" />
+
+ <source path="client" />
+
+ <entry-point class="com.vaadin.terminal.gwt.client.DefaultWidgetSet" />
+</module>
diff --git a/src/com/vaadin/terminal/gwt/DefaultWidgetSetNoEntry.gwt.xml b/src/com/vaadin/terminal/gwt/DefaultWidgetSetNoEntry.gwt.xml
new file mode 100644
index 0000000000..d8d8607213
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/DefaultWidgetSetNoEntry.gwt.xml
@@ -0,0 +1,6 @@
+<module>
+ <!--
+ "NoEntry" -concept deprecated, inherit DefaultWidgetSet instead.
+ -->
+ <inherits name="com.vaadin.terminal.gwt.DefaultWidgetSet" />
+</module> \ No newline at end of file
diff --git a/src/com/vaadin/terminal/gwt/client/ApplicationConfiguration.java b/src/com/vaadin/terminal/gwt/client/ApplicationConfiguration.java
new file mode 100644
index 0000000000..564a87445e
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ApplicationConfiguration.java
@@ -0,0 +1,201 @@
+package com.vaadin.terminal.gwt.client;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.core.client.JavaScriptObject;
+
+public class ApplicationConfiguration {
+
+ // can only be inited once, to avoid multiple-entrypoint-problem
+ private static WidgetSet initedWidgetSet;
+
+ private String id;
+ private String themeUri;
+ private String pathInfo;
+ private String appUri;
+ private JavaScriptObject versionInfo;
+ private String windowName;
+ private String communicationErrorCaption;
+ private String communicationErrorMessage;
+ private String communicationErrorUrl;
+ private boolean useDebugIdInDom = true;
+
+ private static ArrayList<ApplicationConnection> unstartedApplications = new ArrayList<ApplicationConnection>();
+ private static ArrayList<ApplicationConnection> runningApplications = new ArrayList<ApplicationConnection>();
+
+ 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 void setInitialWindowName(String name) {
+ windowName = name;
+ }
+
+ public String getInitialWindowName() {
+ return windowName;
+ }
+
+ public JavaScriptObject getVersionInfoJSObject() {
+ return versionInfo;
+ }
+
+ public String getCommunicationErrorCaption() {
+ return communicationErrorCaption;
+ }
+
+ public String getCommunicationErrorMessage() {
+ return communicationErrorMessage;
+ }
+
+ public String getCommunicationErrorUrl() {
+ return communicationErrorUrl;
+ }
+
+ private native void loadFromDOM()
+ /*-{
+
+ var id = this.@com.vaadin.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.vaadin.terminal.gwt.client.ApplicationConfiguration::appUri = uri;
+ this.@com.vaadin.terminal.gwt.client.ApplicationConfiguration::pathInfo = jsobj.pathInfo;
+ this.@com.vaadin.terminal.gwt.client.ApplicationConfiguration::themeUri = jsobj.themeUri;
+ if(jsobj.windowName) {
+ this.@com.vaadin.terminal.gwt.client.ApplicationConfiguration::windowName = jsobj.windowName;
+ }
+ if('useDebugIdInDom' in jsobj && typeof(jsobj.useDebugIdInDom) == "boolean") {
+ this.@com.vaadin.terminal.gwt.client.ApplicationConfiguration::useDebugIdInDom = jsobj.useDebugIdInDom;
+ }
+ if(jsobj.versionInfo) {
+ this.@com.vaadin.terminal.gwt.client.ApplicationConfiguration::versionInfo = jsobj.versionInfo;
+ }
+ if(jsobj.comErrMsg) {
+ this.@com.vaadin.terminal.gwt.client.ApplicationConfiguration::communicationErrorCaption = jsobj.comErrMsg.caption;
+ this.@com.vaadin.terminal.gwt.client.ApplicationConfiguration::communicationErrorMessage = jsobj.comErrMsg.message;
+ this.@com.vaadin.terminal.gwt.client.ApplicationConfiguration::communicationErrorUrl = jsobj.comErrMsg.url;
+ }
+
+ } else {
+ $wnd.alert("Toolkit app failed to initialize: " + this.id);
+ }
+
+ }-*/;
+
+ /**
+ * Inits the ApplicationConfiguration by reading the DOM and instantiating
+ * ApplicationConenctions accordingly. Call {@link #startNextApplication()}
+ * to actually start the applications.
+ *
+ * @param widgetset
+ * the widgetset that is running the apps
+ */
+ public static void initConfigurations(WidgetSet widgetset) {
+ String wsname = widgetset.getClass().getName();
+ String module = GWT.getModuleName();
+ int lastdot = module.lastIndexOf(".");
+ String base = module.substring(0, lastdot);
+ String simpleName = module.substring(lastdot + 1);
+ if (!wsname.startsWith(base) || !wsname.endsWith(simpleName)) {
+ // WidgetSet module name does not match implementation name;
+ // probably inherited WidgetSet with entry-point. Skip.
+ GWT.log("Ignored init for " + wsname + " when starting " + module,
+ null);
+ return;
+ }
+
+ if (initedWidgetSet != null) {
+ // Something went wrong: multiple widgetsets inited
+ String msg = "Tried to init " + widgetset.getClass().getName()
+ + ", but " + initedWidgetSet.getClass().getName()
+ + " is already inited.";
+ System.err.println(msg);
+ throw new IllegalStateException(msg);
+ }
+ initedWidgetSet = widgetset;
+ ArrayList<String> appIds = new ArrayList<String>();
+ loadAppIdListFromDOM(appIds);
+
+ for (Iterator<String> it = appIds.iterator(); it.hasNext();) {
+ String appId = it.next();
+ ApplicationConfiguration appConf = getConfigFromDOM(appId);
+ ApplicationConnection a = new ApplicationConnection(widgetset,
+ appConf);
+ unstartedApplications.add(a);
+ }
+ }
+
+ /**
+ * Starts the next unstarted application. The WidgetSet should call this
+ * once to start the first application; after that, each application should
+ * call this once it has started. This ensures that the applications are
+ * started synchronously, which is neccessary to avoid session-id problems.
+ *
+ * @return true if an unstarted application was found
+ */
+ public static boolean startNextApplication() {
+ if (unstartedApplications.size() > 0) {
+ ApplicationConnection a = unstartedApplications.remove(0);
+ a.start();
+ runningApplications.add(a);
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ public static List<ApplicationConnection> getRunningApplications() {
+ return runningApplications;
+ }
+
+ private native static void loadAppIdListFromDOM(ArrayList<String> 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;
+ }
+
+ public native String getServletVersion()
+ /*-{
+ return this.@com.vaadin.terminal.gwt.client.ApplicationConfiguration::versionInfo.toolkitVersion;
+ }-*/;
+
+ public native String getApplicationVersion()
+ /*-{
+ return this.@com.vaadin.terminal.gwt.client.ApplicationConfiguration::versionInfo.applicationVersion;
+ }-*/;
+
+ public boolean useDebugIdInDOM() {
+ return useDebugIdInDom;
+ }
+}
diff --git a/src/com/vaadin/terminal/gwt/client/ApplicationConnection.java b/src/com/vaadin/terminal/gwt/client/ApplicationConnection.java
new file mode 100755
index 0000000000..db13b1f205
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ApplicationConnection.java
@@ -0,0 +1,1633 @@
+/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Set;
+import java.util.Vector;
+
+import com.google.gwt.core.client.GWT;
+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.impl.HTTPRequestImpl;
+import com.google.gwt.user.client.ui.FocusWidget;
+import com.google.gwt.user.client.ui.HasWidgets;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.terminal.gwt.client.RenderInformation.FloatSize;
+import com.vaadin.terminal.gwt.client.RenderInformation.Size;
+import com.vaadin.terminal.gwt.client.ui.Field;
+import com.vaadin.terminal.gwt.client.ui.IContextMenu;
+import com.vaadin.terminal.gwt.client.ui.INotification;
+import com.vaadin.terminal.gwt.client.ui.IView;
+import com.vaadin.terminal.gwt.client.ui.INotification.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";
+
+ public static final String VAR_BURST_SEPARATOR = "\u001d";
+
+ public static final String VAR_ARRAYITEM_SEPARATOR = "\u001c";
+
+ public static final String UIDL_SECURITY_HEADER = "com.itmill.seckey";
+
+ public static final String PARAM_UNLOADBURST = "onunloadburst";
+
+ private static String uidl_security_key = "init";
+
+ private final HashMap<String, String> resourcesMap = new HashMap<String, String>();
+
+ private static Console console;
+
+ private final Vector<String> pendingVariables = new Vector<String>();
+
+ private final ComponentDetailMap idToPaintableDetail = ComponentDetailMap
+ .create();
+
+ private final WidgetSet widgetSet;
+
+ private IContextMenu contextMenu = null;
+
+ private Timer loadTimer;
+ private Timer loadTimer2;
+ private Timer loadTimer3;
+ private Element loadElement;
+
+ private final IView view;
+
+ private boolean applicationRunning = false;
+
+ private int activeRequests = 0;
+
+ /** Parameters for this application connection loaded from the web-page */
+ private final ApplicationConfiguration configuration;
+
+ /** List of pending variable change bursts that must be submitted in order */
+ private final Vector<Vector<String>> pendingVariableBursts = new Vector<Vector<String>>();
+
+ /** Timer for automatic refirect to SessionExpiredURL */
+ private Timer redirectTimer;
+
+ /** redirectTimer scheduling interval in seconds */
+ private int sessionExpirationInterval;
+
+ private ArrayList<Paintable> relativeSizeChanges = new ArrayList<Paintable>();;
+ private ArrayList<Paintable> componentCaptionSizeChanges = new ArrayList<Paintable>();;
+
+ private Date requestStartTime;
+
+ private boolean validatingLayouts = false;
+
+ private Set<Paintable> zeroWidthComponents = null;
+
+ private Set<Paintable> zeroHeightComponents = null;
+
+ public ApplicationConnection(WidgetSet widgetSet,
+ ApplicationConfiguration cnf) {
+ this.widgetSet = widgetSet;
+ configuration = cnf;
+ windowName = configuration.getInitialWindowName();
+ if (isDebugMode()) {
+ console = new IDebugConsole(this, cnf, !isQuietDebugMode());
+ } else {
+ console = new NullConsole();
+ }
+
+ ComponentLocator componentLocator = new ComponentLocator(this);
+
+ String appRootPanelName = cnf.getRootPanelId();
+ // remove the end (window name) of autogenarated rootpanel id
+ appRootPanelName = appRootPanelName.replaceFirst("-\\d+$", "");
+
+ initializeTestingToolsHooks(componentLocator, appRootPanelName);
+
+ initializeClientHooks();
+
+ view = new IView(cnf.getRootPanelId());
+ showLoadingIndicator();
+
+ }
+
+ /**
+ * Starts this application. Don't call this method directly - it's called by
+ * {@link ApplicationConfiguration#startNextApplication()}, which should be
+ * called once this application has started (first response received) or
+ * failed to start. This ensures that the applications are started in order,
+ * to avoid session-id problems.
+ */
+ void start() {
+ makeUidlRequest("", true, false, false);
+ }
+
+ private native void initializeTestingToolsHooks(
+ ComponentLocator componentLocator, String TTAppId)
+ /*-{
+ var ap = this;
+ var client = {};
+ client.isActive = function() {
+ return ap.@com.vaadin.terminal.gwt.client.ApplicationConnection::hasActiveRequest()();
+ }
+ var vi = ap.@com.vaadin.terminal.gwt.client.ApplicationConnection::getVersionInfo()();
+ if (vi) {
+ client.getVersionInfo = function() {
+ return vi;
+ }
+ }
+
+ client.getElementByPath = function(id) {
+ return componentLocator.@com.vaadin.terminal.gwt.client.ComponentLocator::getElementByPath(Ljava/lang/String;)(id);
+ }
+ client.getPathForElement = function(element) {
+ return componentLocator.@com.vaadin.terminal.gwt.client.ComponentLocator::getPathForElement(Lcom/google/gwt/user/client/Element;)(element);
+ }
+
+ if(!$wnd.itmill.clients) {
+ $wnd.itmill.clients = {};
+ }
+
+ $wnd.itmill.clients[TTAppId] = client;
+ }-*/;
+
+ /**
+ * Helper for tt initialization
+ */
+ @SuppressWarnings("unused")
+ private JavaScriptObject getVersionInfo() {
+ return configuration.getVersionInfoJSObject();
+ }
+
+ /**
+ * 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.vaadin.terminal.gwt.client.ApplicationConnection::sendPendingVariableChanges()();
+ }
+ var oldForceLayout;
+ if($wnd.itmill.forceLayout) {
+ oldForceLayout = $wnd.itmill.forceLayout;
+ }
+ $wnd.itmill.forceLayout = function() {
+ if(oldForceLayout) {
+ oldForceLayout();
+ }
+ app.@com.vaadin.terminal.gwt.client.ApplicationConnection::forceLayout()();
+ }
+ }-*/;
+
+ public static Console getConsole() {
+ return console;
+ }
+
+ /**
+ * Checks if client side is in debug mode. Practically this is invoked by
+ * adding ?debug parameter to URI.
+ *
+ * @return true if client side is currently been debugged
+ */
+ public native static boolean isDebugMode()
+ /*-{
+ if($wnd.itmill.debug) {
+ var parameters = $wnd.location.search;
+ var re = /debug[^\/]*$/;
+ return re.test(parameters);
+ } else {
+ return false;
+ }
+ }-*/;
+
+ private native static boolean isQuietDebugMode()
+ /*-{
+ var uri = $wnd.location;
+ var re = /debug=q[^\/]*$/;
+ return re.test(uri);
+ }-*/;
+
+ public String getAppUri() {
+ return configuration.getApplicationUri();
+ };
+
+ public boolean hasActiveRequest() {
+ return (activeRequests > 0);
+ }
+
+ private void makeUidlRequest(String requestData, boolean repaintAll,
+ boolean forceSync, boolean analyzeLayouts) {
+ startRequest();
+
+ // Security: double cookie submission pattern
+ requestData = uidl_security_key + VAR_BURST_SEPARATOR + requestData;
+
+ console.log("Making UIDL Request with params: " + requestData);
+ String uri = getAppUri() + "UIDL" + configuration.getPathInfo();
+ if (repaintAll) {
+ uri += "?repaintAll=1";
+ if (analyzeLayouts) {
+ uri += "&analyzeLayouts=1";
+ }
+ }
+ if (windowName != null && windowName.length() > 0) {
+ uri += (repaintAll ? "&" : "?") + "windowName=" + windowName;
+ }
+
+ if (!forceSync) {
+ final RequestBuilder rb = new RequestBuilder(RequestBuilder.POST,
+ uri);
+ // TODO enable timeout
+ // rb.setTimeoutMillis(timeoutMillis);
+ rb.setHeader("Content-Type", "text/plain;charset=utf-8");
+ try {
+ rb.sendRequest(requestData, new RequestCallback() {
+ public void onError(Request request, Throwable exception) {
+ showCommunicationError(exception.getMessage());
+ endRequest();
+ if (!applicationRunning) {
+ // start failed, let's try to start the next app
+ ApplicationConfiguration.startNextApplication();
+ }
+ }
+
+ public void onResponseReceived(Request request,
+ Response response) {
+ console.log("Server visit took "
+ + String.valueOf((new Date()).getTime()
+ - requestStartTime.getTime()) + "ms");
+
+ switch (response.getStatusCode()) {
+ case 0:
+ showCommunicationError("Invalid status code 0 (server down?)");
+ return;
+ // TODO could add more cases
+ }
+ if ("init".equals(uidl_security_key)) {
+ // Read security key
+ String key = response
+ .getHeader(UIDL_SECURITY_HEADER);
+ if (null != key) {
+ uidl_security_key = key;
+ }
+ }
+ if (applicationRunning) {
+ handleReceivedJSONMessage(response);
+ } else {
+ applicationRunning = true;
+ handleWhenCSSLoaded(response);
+ ApplicationConfiguration.startNextApplication();
+ }
+ }
+
+ int cssWaits = 0;
+ static final int MAX_CSS_WAITS = 20;
+
+ private void handleWhenCSSLoaded(final Response response) {
+ int heightOfLoadElement = DOM.getElementPropertyInt(
+ loadElement, "offsetHeight");
+ if (heightOfLoadElement == 0
+ && cssWaits < MAX_CSS_WAITS) {
+ (new Timer() {
+ @Override
+ public void run() {
+ handleWhenCSSLoaded(response);
+ }
+ }).schedule(50);
+ console
+ .log("Assuming CSS loading is not complete, "
+ + "postponing render phase. "
+ + "(.i-loading-indicator height == 0)");
+ cssWaits++;
+ } else {
+ handleReceivedJSONMessage(response);
+ if (cssWaits >= MAX_CSS_WAITS) {
+ console
+ .error("CSS files may have not loaded properly.");
+ }
+ }
+ }
+
+ });
+
+ } catch (final RequestException e) {
+ ClientExceptionHandler.displayError(e);
+ endRequest();
+ }
+ } else {
+ // Synchronized call, discarded response
+
+ syncSendForce(((HTTPRequestImpl) GWT.create(HTTPRequestImpl.class))
+ .createXmlHTTPRequest(), uri + "&" + PARAM_UNLOADBURST
+ + "=1", requestData);
+ }
+ }
+
+ /**
+ * Shows the communication error notification. The 'details' only go to the
+ * console for now.
+ *
+ * @param details
+ * Optional details for debugging.
+ */
+ private void showCommunicationError(String details) {
+ console.error("Communication error: " + details);
+ String html = "";
+ if (configuration.getCommunicationErrorCaption() != null) {
+ html += "<h1>" + configuration.getCommunicationErrorCaption()
+ + "</h1>";
+ }
+ if (configuration.getCommunicationErrorMessage() != null) {
+ html += "<p>" + configuration.getCommunicationErrorMessage()
+ + "</p>";
+ }
+ if (html.length() > 0) {
+ INotification n = new INotification(1000 * 60 * 45);
+ n.addEventListener(new NotificationRedirect(configuration
+ .getCommunicationErrorUrl()));
+ n
+ .show(html, INotification.CENTERED_TOP,
+ INotification.STYLE_SYSTEM);
+ } else {
+ redirect(configuration.getCommunicationErrorUrl());
+ }
+ }
+
+ private native void syncSendForce(JavaScriptObject xmlHttpRequest,
+ String uri, String requestData)
+ /*-{
+ try {
+ xmlHttpRequest.open("POST", uri, false);
+ xmlHttpRequest.setRequestHeader("Content-Type", "text/plain;charset=utf-8");
+ xmlHttpRequest.send(requestData);
+ } catch (e) {
+ // No errors are managed as this is synchronous forceful send that can just fail
+ }
+
+ }-*/;
+
+ private void startRequest() {
+ activeRequests++;
+ requestStartTime = new Date();
+ // show initial throbber
+ if (loadTimer == null) {
+ loadTimer = new Timer() {
+ @Override
+ public void run() {
+ /*
+ * IE7 does not properly cancel the event with
+ * loadTimer.cancel() so we have to check that we really
+ * should make it visible
+ */
+ if (loadTimer != null) {
+ showLoadingIndicator();
+ }
+
+ }
+ };
+ // First one kicks in at 300ms
+ }
+ loadTimer.schedule(300);
+ }
+
+ private void endRequest() {
+ if (applicationRunning) {
+ 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<Vector<String>> iterator = pendingVariableBursts
+ .iterator(); iterator.hasNext();) {
+ cleanVariableBurst(iterator.next());
+ }
+ Vector<String> nextBurst = pendingVariableBursts.firstElement();
+ pendingVariableBursts.remove(0);
+ buildAndSendVariableBurst(nextBurst, false);
+ }
+ }
+
+ /**
+ * Cleans given queue of variable changes of such changes that came from
+ * components that do not exist anymore.
+ *
+ * @param variableBurst
+ */
+ private void cleanVariableBurst(Vector<String> variableBurst) {
+ for (int i = 1; i < variableBurst.size(); i += 2) {
+ String id = variableBurst.get(i);
+ id = id.substring(0, id.indexOf(VAR_FIELD_SEPARATOR));
+ if (!idToPaintableDetail.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 (loadElement == null) {
+ loadElement = DOM.createDiv();
+ DOM.setStyleAttribute(loadElement, "position", "absolute");
+ DOM.appendChild(view.getElement(), loadElement);
+ ApplicationConnection.getConsole().log("inserting load indicator");
+ }
+ DOM.setElementProperty(loadElement, "className", "i-loading-indicator");
+ DOM.setStyleAttribute(loadElement, "display", "block");
+ // Initialize other timers
+ loadTimer2 = new Timer() {
+ @Override
+ public void run() {
+ DOM.setElementProperty(loadElement, "className",
+ "i-loading-indicator-delay");
+ }
+ };
+ // Second one kicks in at 1500ms from request start
+ loadTimer2.schedule(1200);
+
+ loadTimer3 = new Timer() {
+ @Override
+ public void run() {
+ DOM.setElementProperty(loadElement, "className",
+ "i-loading-indicator-wait");
+ }
+ };
+ // Third one kicks in at 5000ms from request start
+ loadTimer3.schedule(4700);
+ }
+
+ private void hideLoadingIndicator() {
+ if (loadTimer != null) {
+ loadTimer.cancel();
+ if (loadTimer2 != null) {
+ loadTimer2.cancel();
+ loadTimer3.cancel();
+ }
+ loadTimer = null;
+ }
+ if (loadElement != null) {
+ DOM.setStyleAttribute(loadElement, "display", "none");
+ }
+ }
+
+ 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();
+ showCommunicationError(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<String> i = resources.keySet().iterator(); i
+ .hasNext();) {
+ final String key = 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();
+ idToPaintableDetail.clear();
+ if (meta.containsKey("invalidLayouts")) {
+ validatingLayouts = true;
+ zeroWidthComponents = new HashSet<Paintable>();
+ zeroHeightComponents = new HashSet<Paintable>();
+ }
+ }
+ if (meta.containsKey("timedRedirect")) {
+ final JSONObject timedRedirect = meta.get("timedRedirect")
+ .isObject();
+ redirectTimer = new Timer() {
+ @Override
+ public void run() {
+ redirect(timedRedirect.get("url").isString()
+ .stringValue());
+ }
+ };
+ sessionExpirationInterval = Integer.parseInt(timedRedirect.get(
+ "interval").toString());
+ }
+ }
+ if (redirectTimer != null) {
+ redirectTimer.schedule(1000 * sessionExpirationInterval);
+ }
+ // Process changes
+ final JSONArray changes = (JSONArray) ((JSONObject) json)
+ .get("changes");
+
+ Vector<Paintable> updatedWidgets = new Vector<Paintable>();
+ relativeSizeChanges.clear();
+ componentCaptionSizeChanges.clear();
+
+ 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) {
+ ClientExceptionHandler.displayError(e);
+ // 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);
+ // TODO optimize
+ final Paintable paintable = getPaintable(uidl.getId());
+ if (paintable != null) {
+ paintable.updateFromUIDL(uidl, this);
+ // paintable may have changed during render to another
+ // implementation, use the new one for updated widgets map
+ updatedWidgets.add(idToPaintableDetail.get(uidl.getId())
+ .getComponent());
+ } else {
+ if (!uidl.getTag().equals("window")) {
+ ClientExceptionHandler
+ .displayError("Received update for "
+ + uidl.getTag()
+ + ", but there is no such paintable ("
+ + uidl.getId() + ") rendered.");
+ } else {
+ view.updateFromUIDL(uidl, this);
+ }
+ }
+ } catch (final Throwable e) {
+ ClientExceptionHandler.displayError(e);
+ }
+ }
+
+ // Check which widgets' size has been updated
+ Set<Paintable> sizeUpdatedWidgets = new HashSet<Paintable>();
+
+ updatedWidgets.addAll(relativeSizeChanges);
+ sizeUpdatedWidgets.addAll(componentCaptionSizeChanges);
+
+ for (Paintable paintable : updatedWidgets) {
+ ComponentDetail detail = idToPaintableDetail.get(getPid(paintable));
+ Widget widget = (Widget) paintable;
+ Size oldSize = detail.getOffsetSize();
+ Size newSize = new Size(widget.getOffsetWidth(), widget
+ .getOffsetHeight());
+
+ if (oldSize == null || !oldSize.equals(newSize)) {
+ sizeUpdatedWidgets.add(paintable);
+ detail.setOffsetSize(newSize);
+ }
+
+ }
+
+ Util.componentSizeUpdated(sizeUpdatedWidgets);
+
+ if (meta != null) {
+ 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) {
+ /* 45 min */
+ INotification n = new INotification(1000 * 60 * 45);
+ n.addEventListener(new NotificationRedirect(url));
+ n.show(html, INotification.CENTERED_TOP,
+ INotification.STYLE_SYSTEM);
+ } else {
+ redirect(url);
+ }
+ applicationRunning = false;
+ }
+ if (validatingLayouts) {
+ getConsole().printLayoutProblems(
+ meta.get("invalidLayouts").isArray(), this,
+ zeroHeightComponents, zeroWidthComponents);
+ zeroHeightComponents = null;
+ zeroWidthComponents = null;
+ validatingLayouts = 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: " + idToPaintableDetail.size());
+
+ endRequest();
+ }
+
+ /**
+ * This method assures that all pending variable changes are sent to server.
+ * Method uses synchronized xmlhttprequest and does not return before the
+ * changes are sent. No UIDL updates are processed and thut UI is left in
+ * inconsistent state. This method should be called only when closing
+ * windows - normally sendPendingVariableChanges() should be used.
+ */
+ public void sendPendingVariableChangesSync() {
+ if (applicationRunning) {
+ pendingVariableBursts.add(pendingVariables);
+ Vector<String> nextBurst = pendingVariableBursts.firstElement();
+ pendingVariableBursts.remove(0);
+ buildAndSendVariableBurst(nextBurst, true);
+ }
+ }
+
+ // Redirect browser, null reloads current page
+ private static native void redirect(String url)
+ /*-{
+ if (url) {
+ $wnd.location = url;
+ } else {
+ $wnd.location.reload(false);
+ }
+ }-*/;
+
+ public void registerPaintable(String id, Paintable paintable) {
+ ComponentDetail componentDetail = new ComponentDetail();
+ componentDetail.setComponent(paintable);
+ idToPaintableDetail.put(id, componentDetail);
+ setPid(((Widget) paintable).getElement(), id);
+ }
+
+ private native void setPid(Element el, String pid)
+ /*-{
+ el.tkPid = pid;
+ }-*/;
+
+ public String getPid(Paintable paintable) {
+ return getPid(((Widget) paintable).getElement());
+ }
+
+ public native String getPid(Element el)
+ /*-{
+ return el.tkPid;
+ }-*/;
+
+ public Element getElementByPid(String pid) {
+ return ((Widget) getPaintable(pid)).getElement();
+ }
+
+ public void unregisterPaintable(Paintable p) {
+ if (p == null) {
+ ApplicationConnection.getConsole().error(
+ "WARN: Trying to unregister null paintable");
+ return;
+ }
+ String id = getPid(p);
+ idToPaintableDetail.remove(id);
+ if (p instanceof HasWidgets) {
+ unregisterChildPaintables((HasWidgets) p);
+ }
+ }
+
+ public void unregisterChildPaintables(HasWidgets container) {
+ final Iterator<Widget> it = container.iterator();
+ while (it.hasNext()) {
+ final Widget w = 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) {
+ ComponentDetail componentDetail = idToPaintableDetail.get(id);
+ if (componentDetail == null) {
+ return null;
+ } else {
+ return componentDetail.getComponent();
+ }
+ }
+
+ 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.
+ *
+ */
+ @SuppressWarnings("unchecked")
+ 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<String> burst = (Vector<String>) pendingVariables
+ .clone();
+ pendingVariableBursts.add(burst);
+ pendingVariables.clear();
+ }
+ } else {
+ buildAndSendVariableBurst(pendingVariables, false);
+ }
+ }
+ }
+
+ /**
+ * Build the variable burst and send it to server.
+ *
+ * When sync is forced, we also force sending of all pending variable-bursts
+ * at the same time. This is ok as we can assume that DOM will newer be
+ * updated after this.
+ *
+ * @param pendingVariables
+ * Vector of variablechanges to send
+ * @param forceSync
+ * Should we use synchronous request?
+ */
+ private void buildAndSendVariableBurst(Vector<String> pendingVariables,
+ boolean forceSync) {
+ final StringBuffer req = new StringBuffer();
+
+ while (!pendingVariables.isEmpty()) {
+ 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();
+ // Append all the busts to this synchronous request
+ if (forceSync && !pendingVariableBursts.isEmpty()) {
+ pendingVariables = pendingVariableBursts.firstElement();
+ pendingVariableBursts.remove(0);
+ req.append(VAR_BURST_SEPARATOR);
+ }
+ }
+ makeUidlRequest(req.toString(), false, forceSync, false);
+ }
+
+ public void updateVariable(String paintableId, String variableName,
+ Paintable newValue, boolean immediate) {
+ String pid = (newValue != null) ? getPid(newValue) : null;
+ addVariableToQueue(paintableId, variableName, pid, immediate, 'p');
+ }
+
+ 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(VAR_ARRAYITEM_SEPARATOR);
+ }
+ 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) {
+ String pid = getPid(component.getElement());
+ if (pid == null) {
+ getConsole().error(
+ "Trying to update an unregistered component: "
+ + Util.getSimpleName(component));
+ return true;
+ }
+
+ ComponentDetail componentDetail = idToPaintableDetail.get(pid);
+
+ if (componentDetail == null) {
+ getConsole().error(
+ "ComponentDetail not found for "
+ + Util.getSimpleName(component) + " with PID "
+ + pid + ". This should not happen.");
+ return true;
+ }
+
+ // 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");
+ boolean wasVisible = component.isVisible();
+ component.setVisible(visible);
+ if (wasVisible != visible) {
+ // Changed invisibile <-> visible
+ if (wasVisible && manageCaption) {
+ // Must hide caption when component is hidden
+ final Container parent = Util.getLayout(component);
+ if (parent != null) {
+ parent.updateCaption((Paintable) component, uidl);
+ }
+
+ }
+ }
+
+ if (!visible) {
+ // component is invisible, delete old size to notify parent, if
+ // later make visible
+ componentDetail.setOffsetSize(null);
+ return true;
+ }
+
+ // Switch to correct implementation if needed
+ if (!widgetSet.isCorrectImplementation(component, uidl)) {
+ final Container parent = Util.getLayout(component);
+ if (parent != null) {
+ final Widget w = (Widget) widgetSet.createWidget(uidl);
+ parent.replaceChildComponent(component, w);
+ unregisterPaintable((Paintable) component);
+ registerPaintable(uidl.getId(), (Paintable) w);
+ ((Paintable) w).updateFromUIDL(uidl, this);
+ return true;
+ }
+ }
+
+ boolean enabled = !uidl.getBooleanAttribute("disabled");
+ if (component instanceof FocusWidget) {
+ FocusWidget fw = (FocusWidget) component;
+ fw.setEnabled(enabled);
+ if (uidl.hasAttribute("tabindex")) {
+ fw.setTabIndex(uidl.getIntAttribute("tabindex"));
+ }
+ }
+
+ 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]);
+ 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 = componentDetail.getTooltipInfo();
+ 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.getLayout(component);
+ if (parent != null) {
+ parent.updateCaption((Paintable) component, uidl);
+ }
+ }
+
+ if (configuration.useDebugIdInDOM() && uidl.getId().startsWith("PID_S")) {
+ DOM.setElementProperty(component.getElement(), "id", uidl.getId()
+ .substring(5));
+ }
+
+ /*
+ * updateComponentSize need to be after caption update so caption can be
+ * taken into account
+ */
+
+ updateComponentSize(componentDetail, uidl);
+
+ return false;
+ }
+
+ private void updateComponentSize(ComponentDetail cd, UIDL uidl) {
+ String w = uidl.hasAttribute("width") ? uidl
+ .getStringAttribute("width") : "";
+
+ String h = uidl.hasAttribute("height") ? uidl
+ .getStringAttribute("height") : "";
+
+ float relativeWidth = Util.parseRelativeSize(w);
+ float relativeHeight = Util.parseRelativeSize(h);
+
+ // First update maps so they are correct in the setHeight/setWidth calls
+ if (relativeHeight >= 0.0 || relativeWidth >= 0.0) {
+ // One or both is relative
+ FloatSize relativeSize = new FloatSize(relativeWidth,
+ relativeHeight);
+ if (cd.getRelativeSize() == null && cd.getOffsetSize() != null) {
+ // The component has changed from absolute size to relative size
+ relativeSizeChanges.add(cd.getComponent());
+ }
+ cd.setRelativeSize(relativeSize);
+ } else if (relativeHeight < 0.0 && relativeWidth < 0.0) {
+ if (cd.getRelativeSize() != null) {
+ // The component has changed from relative size to absolute size
+ relativeSizeChanges.add(cd.getComponent());
+ }
+ cd.setRelativeSize(null);
+ }
+
+ Widget component = (Widget) cd.getComponent();
+ // Set absolute sizes
+ if (relativeHeight < 0.0) {
+ component.setHeight(h);
+ }
+ if (relativeWidth < 0.0) {
+ component.setWidth(w);
+ }
+
+ // Set relative sizes
+ if (relativeHeight >= 0.0 || relativeWidth >= 0.0) {
+ // One or both is relative
+ handleComponentRelativeSize(cd);
+ }
+
+ }
+
+ /**
+ * Traverses recursively child widgets until ContainerResizedListener child
+ * widget is found. They will delegate it further if needed.
+ *
+ * @param container
+ */
+ private boolean runningLayout = false;
+
+ public void runDescendentsLayout(HasWidgets container) {
+ if (runningLayout) {
+ // getConsole().log(
+ // "Already running descendents layout. Not running again for "
+ // + Util.getSimpleName(container));
+ return;
+ }
+ runningLayout = true;
+ internalRunDescendentsLayout(container);
+ runningLayout = false;
+ }
+
+ /**
+ * This will cause re-layouting of all components. Mainly used for
+ * development. Published to JavaScript.
+ */
+ public void forceLayout() {
+ Set<Paintable> set = new HashSet<Paintable>();
+ for (ComponentDetail cd : idToPaintableDetail.values()) {
+ set.add(cd.getComponent());
+ }
+ Util.componentSizeUpdated(set);
+ }
+
+ private void internalRunDescendentsLayout(HasWidgets container) {
+ // getConsole().log(
+ // "runDescendentsLayout(" + Util.getSimpleName(container) + ")");
+ final Iterator<Widget> childWidgets = container.iterator();
+ while (childWidgets.hasNext()) {
+ final Widget child = childWidgets.next();
+
+ if (child instanceof Paintable) {
+
+ if (handleComponentRelativeSize(child)) {
+ /*
+ * Only need to propagate event if "child" has a relative
+ * size
+ */
+
+ if (child instanceof ContainerResizedListener) {
+ ((ContainerResizedListener) child).iLayout();
+ }
+
+ if (child instanceof HasWidgets) {
+ final HasWidgets childContainer = (HasWidgets) child;
+ internalRunDescendentsLayout(childContainer);
+ }
+ }
+ } else if (child instanceof HasWidgets) {
+ // propagate over non Paintable HasWidgets
+ internalRunDescendentsLayout((HasWidgets) child);
+ }
+
+ }
+ }
+
+ /**
+ * Converts relative sizes into pixel sizes.
+ *
+ * @param child
+ * @return true if the child has a relative size
+ */
+ private boolean handleComponentRelativeSize(ComponentDetail cd) {
+ if (cd == null) {
+ return false;
+ }
+ boolean debugSizes = false;
+
+ FloatSize relativeSize = cd.getRelativeSize();
+ if (relativeSize == null) {
+ return false;
+ }
+ Widget widget = (Widget) cd.getComponent();
+
+ boolean horizontalScrollBar = false;
+ boolean verticalScrollBar = false;
+
+ Container parent = Util.getLayout(widget);
+ RenderSpace renderSpace;
+
+ // Parent-less components (like sub-windows) are relative to browser
+ // window.
+ if (parent == null) {
+ renderSpace = new RenderSpace(Window.getClientWidth(), Window
+ .getClientHeight());
+ } else {
+ renderSpace = parent.getAllocatedSpace(widget);
+ }
+
+ if (relativeSize.getHeight() >= 0) {
+ if (renderSpace != null) {
+
+ if (renderSpace.getScrollbarSize() > 0) {
+ if (relativeSize.getWidth() > 100) {
+ horizontalScrollBar = true;
+ } else if (relativeSize.getWidth() < 0
+ && renderSpace.getWidth() > 0) {
+ int offsetWidth = widget.getOffsetWidth();
+ int width = renderSpace.getWidth();
+ if (offsetWidth > width) {
+ horizontalScrollBar = true;
+ }
+ }
+ }
+
+ int height = renderSpace.getHeight();
+ if (horizontalScrollBar) {
+ height -= renderSpace.getScrollbarSize();
+ }
+ if (validatingLayouts && height <= 0) {
+ zeroHeightComponents.add(cd.getComponent());
+ }
+
+ height = (int) (height * relativeSize.getHeight() / 100.0);
+
+ if (height < 0) {
+ height = 0;
+ }
+
+ if (debugSizes) {
+ getConsole()
+ .log(
+ "Widget "
+ + Util.getSimpleName(widget)
+ + "/"
+ + getPid(widget.getElement())
+ + " relative height "
+ + relativeSize.getHeight()
+ + "% of "
+ + renderSpace.getHeight()
+ + "px (reported by "
+
+ + Util.getSimpleName(parent)
+ + "/"
+ + (parent == null ? "?" : parent
+ .hashCode()) + ") : "
+ + height + "px");
+ }
+ widget.setHeight(height + "px");
+ } else {
+ widget.setHeight(relativeSize.getHeight() + "%");
+ ApplicationConnection.getConsole().error(
+ Util.getLayout(widget).getClass().getName()
+ + " did not produce allocatedSpace for "
+ + widget.getClass().getName());
+ }
+ }
+
+ if (relativeSize.getWidth() >= 0) {
+
+ if (renderSpace != null) {
+
+ int width = renderSpace.getWidth();
+
+ if (renderSpace.getScrollbarSize() > 0) {
+ if (relativeSize.getHeight() > 100) {
+ verticalScrollBar = true;
+ } else if (relativeSize.getHeight() < 0
+ && renderSpace.getHeight() > 0
+ && widget.getOffsetHeight() > renderSpace
+ .getHeight()) {
+ verticalScrollBar = true;
+ }
+ }
+
+ if (verticalScrollBar) {
+ width -= renderSpace.getScrollbarSize();
+ }
+ if (validatingLayouts && width <= 0) {
+ zeroWidthComponents.add(cd.getComponent());
+ }
+
+ width = (int) (width * relativeSize.getWidth() / 100.0);
+
+ if (width < 0) {
+ width = 0;
+ }
+
+ if (debugSizes) {
+ getConsole().log(
+ "Widget " + Util.getSimpleName(widget) + "/"
+ + getPid(widget.getElement())
+ + " relative width "
+ + relativeSize.getWidth() + "% of "
+ + renderSpace.getWidth()
+ + "px (reported by "
+ + Util.getSimpleName(parent) + "/"
+ + (parent == null ? "?" : getPid(parent))
+ + ") : " + width + "px");
+ }
+ widget.setWidth(width + "px");
+ } else {
+ widget.setWidth(relativeSize.getWidth() + "%");
+ ApplicationConnection.getConsole().error(
+ Util.getLayout(widget).getClass().getName()
+ + " did not produce allocatedSpace for "
+ + widget.getClass().getName());
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Converts relative sizes into pixel sizes.
+ *
+ * @param child
+ * @return true if the child has a relative size
+ */
+ public boolean handleComponentRelativeSize(Widget child) {
+ return handleComponentRelativeSize(idToPaintableDetail.get(getPid(child
+ .getElement())));
+
+ }
+
+ public FloatSize getRelativeSize(Widget widget) {
+ return idToPaintableDetail.get(getPid(widget.getElement()))
+ .getRelativeSize();
+ }
+
+ /**
+ * 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;
+ } else {
+ w = widgetSet.createWidget(uidl);
+ registerPaintable(id, w);
+ return w;
+
+ }
+ }
+
+ /**
+ * Returns a Paintable element by its root element
+ *
+ * @param element
+ * Root element of the paintable
+ */
+ public Paintable getPaintable(Element element) {
+ return getPaintable(getPid(element));
+ }
+
+ public String getResource(String name) {
+ return resourcesMap.get(name);
+ }
+
+ /**
+ * Singleton method to get instance of app's context menu.
+ *
+ * @return IContextMenu object
+ */
+ public IContextMenu getContextMenu() {
+ if (contextMenu == null) {
+ contextMenu = new IContextMenu();
+ 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 INotification.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) {
+ if (null == titleOwner) {
+ return null;
+ }
+ ComponentDetail pd = idToPaintableDetail.get(getPid(titleOwner));
+ if (null != pd) {
+ return pd.getTooltipInfo();
+ } else {
+ return null;
+ }
+ }
+
+ private final ITooltip tooltip = new ITooltip(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");
+ }
+ }
+
+ /*
+ * Helper to run layout functions triggered by child components with a
+ * decent interval.
+ */
+ private final Timer layoutTimer = new Timer() {
+
+ private boolean isPending = false;
+
+ @Override
+ public void schedule(int delayMillis) {
+ if (!isPending) {
+ super.schedule(delayMillis);
+ isPending = true;
+ }
+ }
+
+ @Override
+ public void run() {
+ getConsole().log(
+ "Running re-layout of " + view.getClass().getName());
+ runDescendentsLayout(view);
+ isPending = false;
+ }
+ };
+
+ /**
+ * Components can call this function to run all layout functions. This is
+ * usually done, when component knows that its size has changed.
+ */
+ public void requestLayoutPhase() {
+ layoutTimer.schedule(500);
+ }
+
+ private String windowName = null;
+
+ /**
+ * Reset the name of the current browser-window. This should reflect the
+ * window-name used in the server, but might be different from the
+ * window-object target-name on client.
+ *
+ * @param stringAttribute
+ * New name for the window.
+ */
+ public void setWindowName(String newName) {
+ windowName = newName;
+ }
+
+ public void captionSizeUpdated(Paintable component) {
+ componentCaptionSizeChanges.add(component);
+ }
+
+ public void analyzeLayouts() {
+ makeUidlRequest("", true, false, true);
+ }
+
+ public IView getView() {
+ return view;
+ }
+
+}
diff --git a/src/com/vaadin/terminal/gwt/client/BrowserInfo.java b/src/com/vaadin/terminal/gwt/client/BrowserInfo.java
new file mode 100644
index 0000000000..8e41137e56
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/BrowserInfo.java
@@ -0,0 +1,225 @@
+/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client;
+
+import com.google.gwt.user.client.ui.RootPanel;
+
+/**
+ * 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;
+
+ private static String cssClass = null;
+
+ static {
+ // Add browser dependent i-* classnames to body to help css hacks
+ String browserClassnames = get().getCSSClass();
+ RootPanel.get().addStyleName(browserClassnames);
+ }
+
+ /**
+ * 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 version = -1;
+
+ private BrowserInfo() {
+ try {
+ String ua = getBrowserString().toLowerCase();
+ // browser engine name
+ isGecko = ua.indexOf("gecko") != -1 && ua.indexOf("webkit") == -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");
+ version = Float.parseFloat(tmp);
+ }
+ if (isAppleWebKit) {
+ String tmp = ua.substring(ua.indexOf("webkit/") + 7);
+ tmp = tmp.replaceFirst("([0-9]+)[^0-9].+", "$1");
+ version = Float.parseFloat(tmp);
+
+ }
+
+ if (isIE) {
+ String ieVersionString = ua.substring(ua.indexOf("msie ") + 5);
+ ieVersionString = ieVersionString.substring(0, ieVersionString
+ .indexOf(";"));
+ version = Float.parseFloat(ieVersionString);
+
+ if (version == 8 && isIE8InIE7CompatibilityMode()) {
+ version = 7;
+ }
+
+ }
+ } catch (Exception e) {
+ ClientExceptionHandler.displayError(e);
+ }
+ }
+
+ /**
+ * Returns a string representing the browser in use, for use in CSS
+ * classnames. The classnames will be space separated abbrevitaions,
+ * optionally with a version appended.
+ *
+ * Abbreviaions: Firefox: ff Internet Explorer: ie Safari: sa Opera: op
+ *
+ * Browsers that CSS-wise behave like each other will get the same
+ * abbreviation (this usually depends on the rendering engine).
+ *
+ * This is quite simple at the moment, more heuristics will be added when
+ * needed.
+ *
+ * Examples: Internet Explorer 6: ".i-ie .i-ie6", Firefox 3.0.4:
+ * ".i-ff .i-ff3", Opera 9.60: ".i-op .i-op96"
+ *
+ * @return
+ */
+ public String getCSSClass() {
+ String prefix = "i-";
+
+ if (cssClass == null) {
+ String bs = getBrowserString().toLowerCase();
+ String b = "";
+ String v = "";
+ if (bs.indexOf(" firefox/") != -1) {
+ b = "ff";
+ int i = bs.indexOf(" firefox/") + 9;
+ v = b + bs.substring(i, i + 1);
+ } else if (bs.indexOf(" chrome/") != -1) {
+ // TODO update when Chrome is more stable
+ b = "sa";
+ v = "ch";
+ } else if (bs.indexOf(" safari") != -1) {
+ b = "sa";
+ int i = bs.indexOf(" version/") + 9;
+ v = b + bs.substring(i, i + 1);
+ } else if (bs.indexOf(" msie ") != -1) {
+ b = "ie";
+ int i = bs.indexOf(" msie ") + 6;
+ String ieVersion = bs.substring(i, i + 1);
+
+ if (ieVersion != null && ieVersion.equals("8")
+ && isIE8InIE7CompatibilityMode()) {
+ ieVersion = "7";
+ }
+
+ v = b + ieVersion;
+ } else if (bs.indexOf("opera/") != -1) {
+ b = "op";
+ int i = bs.indexOf("opera/") + 6;
+ v = b + bs.substring(i, i + 3).replace(".", "");
+ }
+ cssClass = prefix + b + " " + prefix + v;
+ }
+
+ return cssClass;
+ }
+
+ private native boolean isIE8InIE7CompatibilityMode()
+ /*-{
+ var mode = $wnd.document.documentMode;
+ if (!mode)
+ return false;
+ return (mode == 7);
+ }-*/;
+
+ public boolean isIE() {
+ return isIE;
+ }
+
+ public boolean isSafari() {
+ return isSafari;
+ }
+
+ public boolean isIE6() {
+ return isIE && version == 6;
+ }
+
+ public boolean isIE7() {
+ return isIE && version == 7;
+ }
+
+ public boolean isIE8() {
+ return isIE && version == 8;
+ }
+
+ public boolean isGecko() {
+ return isGecko;
+ }
+
+ public boolean isFF2() {
+ return isGecko && version == 1.8;
+ }
+
+ public boolean isFF3() {
+ return isGecko && version == 1.9;
+ }
+
+ public float getGeckoVersion() {
+ return (isGecko ? version : -1);
+ }
+
+ public float getWebkitVersion() {
+ return (isAppleWebKit ? version : -1);
+ }
+
+ public float getIEVersion() {
+ return (isIE ? version : -1);
+ }
+
+ 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("isIE8() " + get().isIE8());
+ 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/vaadin/terminal/gwt/client/CSSRule.java b/src/com/vaadin/terminal/gwt/client/CSSRule.java
new file mode 100644
index 0000000000..eda6008c2b
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/CSSRule.java
@@ -0,0 +1,85 @@
+package com.vaadin.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.vaadin.terminal.gwt.client.CSSRule::rules = @com.vaadin.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.vaadin.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.vaadin.terminal.gwt.client.CSSRule::rules.length; i++){
+ var value = this.@com.vaadin.terminal.gwt.client.CSSRule::rules[i].style[propertyName];
+ if(value)
+ return value;
+ }
+ return null;
+ }-*/;
+
+ public String getSelector() {
+ return selector;
+ }
+
+}
diff --git a/src/com/vaadin/terminal/gwt/client/ClientExceptionHandler.java b/src/com/vaadin/terminal/gwt/client/ClientExceptionHandler.java
new file mode 100644
index 0000000000..724bd24a69
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ClientExceptionHandler.java
@@ -0,0 +1,27 @@
+package com.vaadin.terminal.gwt.client;
+
+public class ClientExceptionHandler {
+
+ public static void displayError(Throwable e) {
+ displayError(e.getMessage());
+ e.printStackTrace();
+ }
+
+ public static void displayError(String msg) {
+
+ Console console = ApplicationConnection.getConsole();
+
+ if (console != null) {
+ console.error(msg);
+ // } else {
+ // System.err.println(msg);
+ }
+ }
+
+ public static void displayError(String msg, Throwable e) {
+ displayError(msg);
+ displayError(e);
+
+ }
+
+}
diff --git a/src/com/vaadin/terminal/gwt/client/ComponentDetail.java b/src/com/vaadin/terminal/gwt/client/ComponentDetail.java
new file mode 100644
index 0000000000..8ee91ad9e4
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ComponentDetail.java
@@ -0,0 +1,88 @@
+package com.vaadin.terminal.gwt.client;
+
+import com.vaadin.terminal.gwt.client.RenderInformation.FloatSize;
+import com.vaadin.terminal.gwt.client.RenderInformation.Size;
+
+class ComponentDetail {
+ private String pid;
+ private Paintable component;
+ private TooltipInfo tooltipInfo = new TooltipInfo();
+ private FloatSize relativeSize;
+ private Size offsetSize;
+
+ /**
+ * @return the pid
+ */
+ String getPid() {
+ return pid;
+ }
+
+ /**
+ * @param pid
+ * the pid to set
+ */
+ void setPid(String pid) {
+ this.pid = pid;
+ }
+
+ /**
+ * @return the component
+ */
+ Paintable getComponent() {
+ return component;
+ }
+
+ /**
+ * @param component
+ * the component to set
+ */
+ void setComponent(Paintable component) {
+ this.component = component;
+ }
+
+ /**
+ * @return the tooltipInfo
+ */
+ TooltipInfo getTooltipInfo() {
+ return tooltipInfo;
+ }
+
+ /**
+ * @param tooltipInfo
+ * the tooltipInfo to set
+ */
+ void setTooltipInfo(TooltipInfo tooltipInfo) {
+ this.tooltipInfo = tooltipInfo;
+ }
+
+ /**
+ * @return the relativeSize
+ */
+ FloatSize getRelativeSize() {
+ return relativeSize;
+ }
+
+ /**
+ * @param relativeSize
+ * the relativeSize to set
+ */
+ void setRelativeSize(FloatSize relativeSize) {
+ this.relativeSize = relativeSize;
+ }
+
+ /**
+ * @return the offsetSize
+ */
+ Size getOffsetSize() {
+ return offsetSize;
+ }
+
+ /**
+ * @param offsetSize
+ * the offsetSize to set
+ */
+ void setOffsetSize(Size offsetSize) {
+ this.offsetSize = offsetSize;
+ }
+
+}
diff --git a/src/com/vaadin/terminal/gwt/client/ComponentDetailMap.java b/src/com/vaadin/terminal/gwt/client/ComponentDetailMap.java
new file mode 100644
index 0000000000..cab3160922
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ComponentDetailMap.java
@@ -0,0 +1,72 @@
+package com.vaadin.terminal.gwt.client;
+
+import java.util.ArrayList;
+import java.util.Collection;
+
+import com.google.gwt.core.client.JavaScriptObject;
+
+final class ComponentDetailMap extends JavaScriptObject {
+
+ protected ComponentDetailMap() {
+ }
+
+ static ComponentDetailMap create() {
+ return (ComponentDetailMap) JavaScriptObject.createObject();
+ }
+
+ boolean isEmpty() {
+ return size() == 0;
+ }
+
+ final native boolean containsKey(String key)
+ /*-{
+ return this.hasOwnProperty(key);
+ }-*/;
+
+ final native ComponentDetail get(String key)
+ /*-{
+ return this[key];
+ }-*/;
+
+ final native void put(String id, ComponentDetail value)
+ /*-{
+ this[id] = value;
+ }-*/;
+
+ final native void remove(String id)
+ /*-{
+ delete this[id];
+ }-*/;
+
+ final native int size()
+ /*-{
+ var count = 0;
+ for(var key in this) {
+ count++;
+ }
+ return count;
+ }-*/;
+
+ final native void clear()
+ /*-{
+ for(var key in this) {
+ if(this.hasOwnProperty(key)) {
+ delete this[key];
+ }
+ }
+ }-*/;
+
+ private final native void fillWithValues(Collection<ComponentDetail> list)
+ /*-{
+ for(var key in this) {
+ list.@java.util.Collection::add(Ljava/lang/Object;)(this[key]);
+ }
+ }-*/;
+
+ final Collection<ComponentDetail> values() {
+ ArrayList<ComponentDetail> list = new ArrayList<ComponentDetail>();
+ fillWithValues(list);
+ return list;
+ }
+
+}
diff --git a/src/com/vaadin/terminal/gwt/client/ComponentLocator.java b/src/com/vaadin/terminal/gwt/client/ComponentLocator.java
new file mode 100644
index 0000000000..ccd0022876
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ComponentLocator.java
@@ -0,0 +1,343 @@
+package com.vaadin.terminal.gwt.client;
+
+import java.util.ArrayList;
+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;
+import com.vaadin.terminal.gwt.client.ui.IView;
+import com.vaadin.terminal.gwt.client.ui.IWindow;
+import com.vaadin.terminal.gwt.client.ui.SubPartAware;
+
+/**
+ * ComponentLocator provides methods for uniquely identifying DOM elements using
+ * string expressions. This class is EXPERIMENTAL and subject to change.
+ */
+public class ComponentLocator {
+
+ /**
+ * Separator used in the string expression between a parent and a child
+ * widget.
+ */
+ private static final String PARENTCHILD_SEPARATOR = "/";
+
+ /**
+ * Separator used in the string expression between a widget and the widget's
+ * sub part. NOT CURRENTLY IN USE.
+ */
+ private static final String SUBPART_SEPARATOR = "#";
+
+ private ApplicationConnection client;
+
+ public ComponentLocator(ApplicationConnection client) {
+ this.client = client;
+ }
+
+ /**
+ * EXPERIMENTAL.
+ *
+ * Generates a string expression (path) which uniquely identifies the target
+ * element . The getElementByPath method can be used for the inverse
+ * operation, i.e. locating an element based on the string expression.
+ *
+ * @since 5.4
+ * @param targetElement
+ * The element to generate a path for.
+ * @return A string expression uniquely identifying the target element or
+ * null if a string expression could not be created.
+ */
+ public String getPathForElement(Element targetElement) {
+ String pid = null;
+
+ Element e = targetElement;
+
+ while (true) {
+ pid = client.getPid(e);
+ if (pid != null) {
+ break;
+ }
+
+ e = DOM.getParent(e);
+ if (e == null) {
+ break;
+ }
+ }
+
+ if (e == null || pid == null) {
+
+ // Still test for context menu option
+ String subPartName = client.getContextMenu().getSubPartName(
+ targetElement);
+ if (subPartName != null) {
+ // IContextMenu, singleton attached directly to rootpanel
+ return "/IContextMenu[0]" + SUBPART_SEPARATOR + subPartName;
+
+ }
+ return null;
+ }
+
+ Widget w = (Widget) client.getPaintable(pid);
+ if (w == null) {
+ return null;
+ }
+ // ApplicationConnection.getConsole().log(
+ // "First parent widget: " + Util.getSimpleName(w));
+
+ String path = getPathForWidget(w);
+ // ApplicationConnection.getConsole().log(
+ // "getPathFromWidget returned " + path);
+ if (w.getElement() == targetElement) {
+ // ApplicationConnection.getConsole().log(
+ // "Path for " + Util.getSimpleName(w) + ": " + path);
+
+ return path;
+ } else if (w instanceof SubPartAware) {
+ return path + SUBPART_SEPARATOR
+ + ((SubPartAware) w).getSubPartName(targetElement);
+ } else {
+ path = path + getDOMPathForElement(targetElement, w.getElement());
+ // ApplicationConnection.getConsole().log(
+ // "Path with dom addition for " + Util.getSimpleName(w)
+ // + ": " + path);
+
+ return path;
+ }
+ }
+
+ private Element getElementByDOMPath(Element baseElement, String path) {
+ String parts[] = path.split(PARENTCHILD_SEPARATOR);
+ Element element = baseElement;
+
+ for (String part : parts) {
+ if (part.startsWith("domChild[")) {
+ String childIndexString = part.substring("domChild[".length(),
+ part.length() - 1);
+ try {
+ int childIndex = Integer.parseInt(childIndexString);
+ element = DOM.getChild(element, childIndex);
+ } catch (Exception e) {
+ // ApplicationConnection.getConsole().error(
+ // "Failed to parse integer in " + childIndexString);
+ return null;
+ }
+ }
+ }
+
+ return element;
+ }
+
+ private String getDOMPathForElement(Element element, Element baseElement) {
+ Element e = element;
+ String path = "";
+ while (true) {
+ Element parent = DOM.getParent(e);
+ if (parent == null) {
+ return "ERROR, baseElement is not a parent to element";
+ }
+
+ int childIndex = -1;
+
+ int childCount = DOM.getChildCount(parent);
+ for (int i = 0; i < childCount; i++) {
+ if (e == DOM.getChild(parent, i)) {
+ childIndex = i;
+ break;
+ }
+ }
+ if (childIndex == -1) {
+ return "ERROR, baseElement is not a parent to element.";
+ }
+
+ path = PARENTCHILD_SEPARATOR + "domChild[" + childIndex + "]"
+ + path;
+
+ if (parent == baseElement) {
+ break;
+ }
+
+ e = parent;
+ }
+
+ return path;
+ }
+
+ /**
+ * EXPERIMENTAL.
+ *
+ * Locates an element by using a string expression (path) which uniquely
+ * identifies the element. The getPathForElement method can be used for the
+ * inverse operation, i.e. generating a string expression for a target
+ * element.
+ *
+ * @since 5.4
+ * @param path
+ * The string expression which uniquely identifies the target
+ * element.
+ * @return The DOM element identified by the path or null if the element
+ * could not be located.
+ */
+ public Element getElementByPath(String path) {
+ // ApplicationConnection.getConsole()
+ // .log("getElementByPath(" + path + ")");
+
+ // Path is of type "PID/componentPart"
+ String parts[] = path.split(SUBPART_SEPARATOR, 2);
+ String widgetPath = parts[0];
+ Widget w = getWidgetFromPath(widgetPath);
+ if (w == null) {
+ return null;
+ }
+
+ if (parts.length == 1) {
+ int pos = widgetPath.indexOf("domChild");
+ if (pos == -1) {
+ return w.getElement();
+ }
+
+ // Contains dom reference to a sub element of the widget
+ String subPath = widgetPath.substring(pos);
+ return getElementByDOMPath(w.getElement(), subPath);
+ } else if (parts.length == 2) {
+ if (w instanceof SubPartAware) {
+ // ApplicationConnection.getConsole().log(
+ // "subPartAware: " + parts[1]);
+ return ((SubPartAware) w).getSubPartElement(parts[1]);
+ } else {
+ // ApplicationConnection.getConsole().error(
+ // "getElementByPath failed because "
+ // + Util.getSimpleName(w)
+ // + " is not SubPartAware");
+ return null;
+ }
+ }
+
+ return null;
+ }
+
+ private String getPathForWidget(Widget w) {
+ if (w == null) {
+ return "";
+ }
+
+ String pid = client.getPid(w.getElement());
+ if (isStaticPid(pid)) {
+ return pid;
+ }
+
+ if (w instanceof IView) {
+ return "";
+ } else if (w instanceof IWindow) {
+ IWindow win = (IWindow) w;
+ ArrayList<IWindow> subWindowList = client.getView()
+ .getSubWindowList();
+ int indexOfSubWindow = subWindowList.indexOf(win);
+ return PARENTCHILD_SEPARATOR + "IWindow[" + indexOfSubWindow + "]";
+ }
+
+ Widget parent = w.getParent();
+
+ String basePath = getPathForWidget(parent);
+
+ String simpleName = Util.getSimpleName(w);
+
+ Iterator<Widget> i = ((HasWidgets) parent).iterator();
+ int pos = 0;
+ while (i.hasNext()) {
+ Object child = i.next();
+ if (child == w) {
+ return basePath + PARENTCHILD_SEPARATOR + simpleName + "["
+ + pos + "]";
+ }
+ String simpleName2 = Util.getSimpleName(child);
+ if (simpleName.equals(simpleName2)) {
+ pos++;
+ }
+ }
+
+ return "NOTFOUND";
+ }
+
+ private Widget getWidgetFromPath(String path) {
+ Widget w = null;
+ String parts[] = path.split(PARENTCHILD_SEPARATOR);
+
+ // ApplicationConnection.getConsole().log(
+ // "getWidgetFromPath(" + path + ")");
+
+ for (String part : parts) {
+ // ApplicationConnection.getConsole().log("Part: " + part);
+ // ApplicationConnection.getConsole().log(
+ // "Widget: " + Util.getSimpleName(w));
+ if (part.equals("")) {
+ w = client.getView();
+ } else if (w == null) {
+ w = (Widget) client.getPaintable(part);
+ } else if (part.startsWith("domChild[")) {
+ break;
+ } else if (w instanceof HasWidgets) {
+ HasWidgets parent = (HasWidgets) w;
+
+ String[] split = part.split("\\[");
+
+ Iterator<? extends Widget> i;
+ String widgetClassName = split[0];
+ if (widgetClassName.equals("IWindow")) {
+ i = client.getView().getSubWindowList().iterator();
+ } else if (widgetClassName.equals("IContextMenu")) {
+ return client.getContextMenu();
+ } else {
+ i = parent.iterator();
+ }
+
+ boolean ok = false;
+ int pos = Integer.parseInt(split[1].substring(0, split[1]
+ .length() - 1));
+ // ApplicationConnection.getConsole().log(
+ // "Looking for child " + pos);
+ while (i.hasNext()) {
+ // ApplicationConnection.getConsole().log("- child found");
+
+ Widget child = i.next();
+ String simpleName2 = Util.getSimpleName(child);
+
+ if (widgetClassName.equals(simpleName2)) {
+ if (pos == 0) {
+ w = child;
+ ok = true;
+ break;
+ }
+ pos--;
+ }
+ }
+
+ if (!ok) {
+ // Did not find the child
+ // ApplicationConnection.getConsole().error(
+ // "getWidgetFromPath(" + path + ") - did not find '"
+ // + part + "' for "
+ // + Util.getSimpleName(parent));
+
+ return null;
+ }
+ } else {
+ // ApplicationConnection.getConsole().error(
+ // "getWidgetFromPath(" + path + ") - failed for '" + part
+ // + "'");
+ return null;
+ }
+ }
+
+ return w;
+ }
+
+ private boolean isStaticPid(String pid) {
+ if (pid == null) {
+ return false;
+ }
+
+ return pid.startsWith("PID_S");
+ }
+
+}
diff --git a/src/com/vaadin/terminal/gwt/client/Console.java b/src/com/vaadin/terminal/gwt/client/Console.java
new file mode 100644
index 0000000000..2d433f8997
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/Console.java
@@ -0,0 +1,26 @@
+/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client;
+
+import java.util.Set;
+
+import com.google.gwt.json.client.JSONArray;
+
+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);
+
+ public abstract void printLayoutProblems(JSONArray array,
+ ApplicationConnection applicationConnection,
+ Set<Paintable> zeroHeightComponents,
+ Set<Paintable> zeroWidthComponents);
+
+} \ No newline at end of file
diff --git a/src/com/vaadin/terminal/gwt/client/Container.java b/src/com/vaadin/terminal/gwt/client/Container.java
new file mode 100644
index 0000000000..28c31fa101
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/Container.java
@@ -0,0 +1,71 @@
+/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client;
+
+import java.util.Set;
+
+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.
+ *
+ * @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);
+
+ /**
+ * Called when a child components size has been updated in the rendering
+ * phase.
+ *
+ * @param children
+ * Set of child widgets whose size have changed
+ * @return true if the size of the Container remains the same, false if the
+ * event need to be propagated to the Containers parent
+ */
+ boolean requestLayout(Set<Paintable> children);
+
+ /**
+ * Returns the size currently allocated for the child component.
+ *
+ * @param child
+ * @return
+ */
+ RenderSpace getAllocatedSpace(Widget child);
+
+}
diff --git a/src/com/vaadin/terminal/gwt/client/ContainerResizedListener.java b/src/com/vaadin/terminal/gwt/client/ContainerResizedListener.java
new file mode 100644
index 0000000000..bfefb5dd05
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ContainerResizedListener.java
@@ -0,0 +1,21 @@
+/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.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/vaadin/terminal/gwt/client/DateTimeService.java b/src/com/vaadin/terminal/gwt/client/DateTimeService.java
new file mode 100644
index 0000000000..e16d9b078d
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/DateTimeService.java
@@ -0,0 +1,237 @@
+/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client;
+
+import java.util.Date;
+
+/**
+ * This class provides date/time parsing services to all components on the
+ * client side.
+ *
+ * @author IT Mill Ltd.
+ *
+ */
+@SuppressWarnings("deprecation")
+public class DateTimeService {
+ public static int RESOLUTION_YEAR = 0;
+ public static int RESOLUTION_MONTH = 1;
+ public static int RESOLUTION_DAY = 2;
+ public static int RESOLUTION_HOUR = 3;
+ public static int RESOLUTION_MIN = 4;
+ public static int RESOLUTION_SEC = 5;
+ public static int RESOLUTION_MSEC = 6;
+
+ private String currentLocale;
+
+ private static int[] maxDaysInMonth = { 31, 28, 31, 30, 31, 30, 31, 31, 30,
+ 31, 30, 31 };
+
+ /**
+ * Creates a new date time service with the application default locale.
+ */
+ public DateTimeService() {
+ currentLocale = LocaleService.getDefaultLocale();
+ }
+
+ /**
+ * Creates a new date time service with a given locale.
+ *
+ * @param locale
+ * e.g. fi, en etc.
+ * @throws LocaleNotLoadedException
+ */
+ public DateTimeService(String locale) throws LocaleNotLoadedException {
+ setLocale(locale);
+ }
+
+ public void setLocale(String locale) throws LocaleNotLoadedException {
+ if (LocaleService.getAvailableLocales().contains(locale)) {
+ currentLocale = locale;
+ } else {
+ throw new LocaleNotLoadedException(locale);
+ }
+ }
+
+ public String getLocale() {
+ return currentLocale;
+ }
+
+ public String getMonth(int month) {
+ try {
+ return LocaleService.getMonthNames(currentLocale)[month];
+ } catch (final LocaleNotLoadedException e) {
+ ClientExceptionHandler.displayError(e);
+ }
+ return null;
+ }
+
+ public String getShortMonth(int month) {
+ try {
+ return LocaleService.getShortMonthNames(currentLocale)[month];
+ } catch (final LocaleNotLoadedException e) {
+ ClientExceptionHandler.displayError(e);
+ }
+ return null;
+ }
+
+ public String getDay(int day) {
+ try {
+ return LocaleService.getDayNames(currentLocale)[day];
+ } catch (final LocaleNotLoadedException e) {
+ ClientExceptionHandler.displayError(e);
+ }
+ return null;
+ }
+
+ public String getShortDay(int day) {
+ try {
+ return LocaleService.getShortDayNames(currentLocale)[day];
+ } catch (final LocaleNotLoadedException e) {
+ ClientExceptionHandler.displayError(e);
+ }
+ return null;
+ }
+
+ public int getFirstDayOfWeek() {
+ try {
+ return LocaleService.getFirstDayOfWeek(currentLocale);
+ } catch (final LocaleNotLoadedException e) {
+ ClientExceptionHandler.displayError(e);
+ }
+ return 0;
+ }
+
+ public boolean isTwelveHourClock() {
+ try {
+ return LocaleService.isTwelveHourClock(currentLocale);
+ } catch (final LocaleNotLoadedException e) {
+ ClientExceptionHandler.displayError(e);
+ }
+ return false;
+ }
+
+ public String getClockDelimeter() {
+ try {
+ return LocaleService.getClockDelimiter(currentLocale);
+ } catch (final LocaleNotLoadedException e) {
+ ClientExceptionHandler.displayError(e);
+ }
+ return ":";
+ }
+
+ public String[] getAmPmStrings() {
+ try {
+ return LocaleService.getAmPmStrings(currentLocale);
+ } catch (final LocaleNotLoadedException e) {
+ ClientExceptionHandler.displayError(e);
+ }
+ final String[] temp = new String[2];
+ temp[0] = "AM";
+ temp[1] = "PM";
+ return temp;
+ }
+
+ public int getStartWeekDay(Date date) {
+ final Date dateForFirstOfThisMonth = new Date(date.getYear(), date
+ .getMonth(), 1);
+ int firstDay;
+ try {
+ firstDay = LocaleService.getFirstDayOfWeek(currentLocale);
+ } catch (final LocaleNotLoadedException e) {
+ firstDay = 0;
+ ClientExceptionHandler.displayError(e);
+ }
+ int start = dateForFirstOfThisMonth.getDay() - firstDay;
+ if (start < 0) {
+ start = 6;
+ }
+ return start;
+ }
+
+ public static int getNumberOfDaysInMonth(Date date) {
+ final int month = date.getMonth();
+ if (month == 1 && true == isLeapYear(date)) {
+ return 29;
+ }
+ return maxDaysInMonth[month];
+ }
+
+ public static boolean isLeapYear(Date date) {
+ // Instantiate the date for 1st March of that year
+ final Date firstMarch = new Date(date.getYear(), 2, 1);
+
+ // Go back 1 day
+ final long firstMarchTime = firstMarch.getTime();
+ final long lastDayTimeFeb = firstMarchTime - (24 * 60 * 60 * 1000); // NUM_MILLISECS_A_DAY
+
+ // Instantiate new Date with this time
+ final Date febLastDay = new Date(lastDayTimeFeb);
+
+ // Check for date in this new instance
+ return (29 == febLastDay.getDate()) ? true : false;
+ }
+
+ public static boolean isSameDay(Date d1, Date d2) {
+ return (getDayInt(d1) == getDayInt(d2));
+ }
+
+ public static boolean isInRange(Date date, Date rangeStart, Date rangeEnd,
+ int resolution) {
+ Date s;
+ Date e;
+ if (rangeStart.after(rangeEnd)) {
+ s = rangeEnd;
+ e = rangeStart;
+ } else {
+ e = rangeEnd;
+ s = rangeStart;
+ }
+ long start = s.getYear() * 10000000000l;
+ long end = e.getYear() * 10000000000l;
+ long target = date.getYear() * 10000000000l;
+
+ if (resolution == RESOLUTION_YEAR) {
+ return (start <= target && end >= target);
+ }
+ start += s.getMonth() * 100000000;
+ end += e.getMonth() * 100000000;
+ target += date.getMonth() * 100000000;
+ if (resolution == RESOLUTION_MONTH) {
+ return (start <= target && end >= target);
+ }
+ start += s.getDate() * 1000000;
+ end += e.getDate() * 1000000;
+ target += date.getDate() * 1000000;
+ if (resolution == RESOLUTION_DAY) {
+ return (start <= target && end >= target);
+ }
+ start += s.getHours() * 10000;
+ end += e.getHours() * 10000;
+ target += date.getHours() * 10000;
+ if (resolution == RESOLUTION_HOUR) {
+ return (start <= target && end >= target);
+ }
+ start += s.getMinutes() * 100;
+ end += e.getMinutes() * 100;
+ target += date.getMinutes() * 100;
+ if (resolution == RESOLUTION_MIN) {
+ return (start <= target && end >= target);
+ }
+ start += s.getSeconds();
+ end += e.getSeconds();
+ target += date.getSeconds();
+ return (start <= target && end >= target);
+
+ }
+
+ private static int getDayInt(Date date) {
+ final int y = date.getYear();
+ final int m = date.getMonth();
+ final int d = date.getDate();
+
+ return ((y + 1900) * 10000 + m * 100 + d) * 1000000000;
+ }
+
+}
diff --git a/src/com/vaadin/terminal/gwt/client/DefaultWidgetSet.java b/src/com/vaadin/terminal/gwt/client/DefaultWidgetSet.java
new file mode 100644
index 0000000000..104ca1d232
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/DefaultWidgetSet.java
@@ -0,0 +1,280 @@
+/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client;
+
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.terminal.gwt.client.ui.IAbsoluteLayout;
+import com.vaadin.terminal.gwt.client.ui.IAccordion;
+import com.vaadin.terminal.gwt.client.ui.IButton;
+import com.vaadin.terminal.gwt.client.ui.ICheckBox;
+import com.vaadin.terminal.gwt.client.ui.ICustomComponent;
+import com.vaadin.terminal.gwt.client.ui.ICustomLayout;
+import com.vaadin.terminal.gwt.client.ui.IDateFieldCalendar;
+import com.vaadin.terminal.gwt.client.ui.IEmbedded;
+import com.vaadin.terminal.gwt.client.ui.IFilterSelect;
+import com.vaadin.terminal.gwt.client.ui.IForm;
+import com.vaadin.terminal.gwt.client.ui.IFormLayout;
+import com.vaadin.terminal.gwt.client.ui.IGridLayout;
+import com.vaadin.terminal.gwt.client.ui.IHorizontalLayout;
+import com.vaadin.terminal.gwt.client.ui.ILabel;
+import com.vaadin.terminal.gwt.client.ui.ILink;
+import com.vaadin.terminal.gwt.client.ui.IListSelect;
+import com.vaadin.terminal.gwt.client.ui.IMenuBar;
+import com.vaadin.terminal.gwt.client.ui.INativeSelect;
+import com.vaadin.terminal.gwt.client.ui.IOptionGroup;
+import com.vaadin.terminal.gwt.client.ui.IOrderedLayout;
+import com.vaadin.terminal.gwt.client.ui.IPanel;
+import com.vaadin.terminal.gwt.client.ui.IPasswordField;
+import com.vaadin.terminal.gwt.client.ui.IPopupCalendar;
+import com.vaadin.terminal.gwt.client.ui.IPopupView;
+import com.vaadin.terminal.gwt.client.ui.IProgressIndicator;
+import com.vaadin.terminal.gwt.client.ui.IScrollTable;
+import com.vaadin.terminal.gwt.client.ui.ISlider;
+import com.vaadin.terminal.gwt.client.ui.ISplitPanelHorizontal;
+import com.vaadin.terminal.gwt.client.ui.ISplitPanelVertical;
+import com.vaadin.terminal.gwt.client.ui.ITablePaging;
+import com.vaadin.terminal.gwt.client.ui.ITabsheet;
+import com.vaadin.terminal.gwt.client.ui.ITextArea;
+import com.vaadin.terminal.gwt.client.ui.ITextField;
+import com.vaadin.terminal.gwt.client.ui.ITextualDate;
+import com.vaadin.terminal.gwt.client.ui.ITree;
+import com.vaadin.terminal.gwt.client.ui.ITwinColSelect;
+import com.vaadin.terminal.gwt.client.ui.IUnknownComponent;
+import com.vaadin.terminal.gwt.client.ui.IUpload;
+import com.vaadin.terminal.gwt.client.ui.IUriFragmentUtility;
+import com.vaadin.terminal.gwt.client.ui.IVerticalLayout;
+import com.vaadin.terminal.gwt.client.ui.IWindow;
+import com.vaadin.terminal.gwt.client.ui.richtextarea.IRichTextArea;
+
+public class DefaultWidgetSet implements WidgetSet {
+
+ /**
+ * This is the entry point method. It will start the first
+ */
+ public void onModuleLoad() {
+ ApplicationConfiguration.initConfigurations(this);
+ ApplicationConfiguration.startNextApplication(); // start first app
+ }
+
+ public Paintable createWidget(UIDL uidl) {
+ final Class classType = resolveWidgetType(uidl);
+ if (ICheckBox.class == classType) {
+ return new ICheckBox();
+ } else if (IButton.class == classType) {
+ return new IButton();
+ } else if (IWindow.class == classType) {
+ return new IWindow();
+ } else if (IOrderedLayout.class == classType) {
+ return new IOrderedLayout();
+ } else if (IVerticalLayout.class == classType) {
+ return new IVerticalLayout();
+ } else if (IHorizontalLayout.class == classType) {
+ return new IHorizontalLayout();
+ } else if (ILabel.class == classType) {
+ return new ILabel();
+ } else if (ILink.class == classType) {
+ return new ILink();
+ } else if (IGridLayout.class == classType) {
+ return new IGridLayout();
+ } else if (ITree.class == classType) {
+ return new ITree();
+ } else if (IOptionGroup.class == classType) {
+ return new IOptionGroup();
+ } else if (ITwinColSelect.class == classType) {
+ return new ITwinColSelect();
+ } else if (INativeSelect.class == classType) {
+ return new INativeSelect();
+ } else if (IListSelect.class == classType) {
+ return new IListSelect();
+ } else if (IPanel.class == classType) {
+ return new IPanel();
+ } else if (ITabsheet.class == classType) {
+ return new ITabsheet();
+ } else if (IEmbedded.class == classType) {
+ return new IEmbedded();
+ } else if (ICustomLayout.class == classType) {
+ return new ICustomLayout();
+ } else if (ICustomComponent.class == classType) {
+ return new ICustomComponent();
+ } else if (ITextArea.class == classType) {
+ return new ITextArea();
+ } else if (IPasswordField.class == classType) {
+ return new IPasswordField();
+ } else if (ITextField.class == classType) {
+ return new ITextField();
+ } else if (ITablePaging.class == classType) {
+ return new ITablePaging();
+ } else if (IScrollTable.class == classType) {
+ return new IScrollTable();
+ } else if (IDateFieldCalendar.class == classType) {
+ return new IDateFieldCalendar();
+ } else if (ITextualDate.class == classType) {
+ return new ITextualDate();
+ } else if (IPopupCalendar.class == classType) {
+ return new IPopupCalendar();
+ } else if (ISlider.class == classType) {
+ return new ISlider();
+ } else if (IForm.class == classType) {
+ return new IForm();
+ } else if (IFormLayout.class == classType) {
+ return new IFormLayout();
+ } else if (IUpload.class == classType) {
+ return new IUpload();
+ } else if (ISplitPanelHorizontal.class == classType) {
+ return new ISplitPanelHorizontal();
+ } else if (ISplitPanelVertical.class == classType) {
+ return new ISplitPanelVertical();
+ } else if (IFilterSelect.class == classType) {
+ return new IFilterSelect();
+ } else if (IProgressIndicator.class == classType) {
+ return new IProgressIndicator();
+ } else if (IRichTextArea.class == classType) {
+ return new IRichTextArea();
+ } else if (IAccordion.class == classType) {
+ return new IAccordion();
+ } else if (IMenuBar.class == classType) {
+ return new IMenuBar();
+ } else if (IPopupView.class == classType) {
+ return new IPopupView();
+ } else if (IUriFragmentUtility.class == classType) {
+ return new IUriFragmentUtility();
+ } else if (IAbsoluteLayout.class == classType) {
+ return new IAbsoluteLayout();
+ }
+
+ return new IUnknownComponent();
+
+ }
+
+ protected Class resolveWidgetType(UIDL uidl) {
+ final String tag = uidl.getTag();
+ if ("button".equals(tag)) {
+ if ("switch".equals(uidl.getStringAttribute("type"))) {
+ return ICheckBox.class;
+ } else {
+ return IButton.class;
+ }
+ } else if ("window".equals(tag)) {
+ return IWindow.class;
+ } else if ("orderedlayout".equals(tag)) {
+ return IOrderedLayout.class;
+ } else if ("verticallayout".equals(tag)) {
+ return IVerticalLayout.class;
+ } else if ("horizontallayout".equals(tag)) {
+ return IHorizontalLayout.class;
+ } else if ("label".equals(tag)) {
+ return ILabel.class;
+ } else if ("link".equals(tag)) {
+ return ILink.class;
+ } else if ("gridlayout".equals(tag)) {
+ return IGridLayout.class;
+ } else if ("tree".equals(tag)) {
+ return ITree.class;
+ } else if ("select".equals(tag)) {
+ if (uidl.hasAttribute("type")) {
+ final String type = uidl.getStringAttribute("type");
+ if (type.equals("twincol")) {
+ return ITwinColSelect.class;
+ }
+ if (type.equals("optiongroup")) {
+ return IOptionGroup.class;
+ }
+ if (type.equals("native")) {
+ return INativeSelect.class;
+ }
+ if (type.equals("list")) {
+ return IListSelect.class;
+ }
+ } else {
+ if (uidl.hasAttribute("selectmode")
+ && uidl.getStringAttribute("selectmode")
+ .equals("multi")) {
+ return IListSelect.class;
+ } else {
+ return IFilterSelect.class;
+ }
+ }
+ } else if ("panel".equals(tag)) {
+ return IPanel.class;
+ } else if ("tabsheet".equals(tag)) {
+ return ITabsheet.class;
+ } else if ("accordion".equals(tag)) {
+ return IAccordion.class;
+ } else if ("embedded".equals(tag)) {
+ return IEmbedded.class;
+ } else if ("customlayout".equals(tag)) {
+ return ICustomLayout.class;
+ } else if ("customcomponent".equals(tag)) {
+ return ICustomComponent.class;
+ } else if ("textfield".equals(tag)) {
+ if (uidl.getBooleanAttribute("richtext")) {
+ return IRichTextArea.class;
+ } else if (uidl.hasAttribute("multiline")) {
+ return ITextArea.class;
+ } else if (uidl.getBooleanAttribute("secret")) {
+ return IPasswordField.class;
+ } else {
+ return ITextField.class;
+ }
+ } else if ("table".equals(tag)) {
+ return IScrollTable.class;
+ } else if ("pagingtable".equals(tag)) {
+ return ITablePaging.class;
+ } else if ("datefield".equals(tag)) {
+ if (uidl.hasAttribute("type")) {
+ if ("inline".equals(uidl.getStringAttribute("type"))) {
+ return IDateFieldCalendar.class;
+ } else if ("popup".equals(uidl.getStringAttribute("type"))) {
+ return IPopupCalendar.class;
+ }
+ }
+ // popup calendar is the default
+ return IPopupCalendar.class;
+ } else if ("slider".equals(tag)) {
+ return ISlider.class;
+ } else if ("form".equals(tag)) {
+ return IForm.class;
+ } else if ("formlayout".equals(tag)) {
+ return IFormLayout.class;
+ } else if ("upload".equals(tag)) {
+ return IUpload.class;
+ } else if ("hsplitpanel".equals(tag)) {
+ return ISplitPanelHorizontal.class;
+ } else if ("vsplitpanel".equals(tag)) {
+ return ISplitPanelVertical.class;
+ } else if ("progressindicator".equals(tag)) {
+ return IProgressIndicator.class;
+ } else if ("menubar".equals(tag)) {
+ return IMenuBar.class;
+ } else if ("popupview".equals(tag)) {
+ return IPopupView.class;
+ } else if ("urifragment".equals(tag)) {
+ return IUriFragmentUtility.class;
+ } else if (IAbsoluteLayout.TAGNAME.equals(tag)) {
+ return IAbsoluteLayout.class;
+ }
+
+ return IUnknownComponent.class;
+ }
+
+ /**
+ * Kept here to support 5.2 era widget sets
+ *
+ * @deprecated use resolveWidgetType instead
+ */
+ @Deprecated
+ protected String resolveWidgetTypeName(UIDL uidl) {
+ Class type = resolveWidgetType(uidl);
+ return type.getName();
+ }
+
+ public boolean isCorrectImplementation(Widget currentWidget, UIDL uidl) {
+ // TODO remove backwardscompatibility check
+ return currentWidget.getClass() == resolveWidgetType(uidl)
+ || currentWidget.getClass().getName().equals(
+ resolveWidgetTypeName(uidl));
+ }
+
+}
diff --git a/src/com/vaadin/terminal/gwt/client/Focusable.java b/src/com/vaadin/terminal/gwt/client/Focusable.java
new file mode 100644
index 0000000000..bf225ad61d
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/Focusable.java
@@ -0,0 +1,16 @@
+package com.vaadin.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/vaadin/terminal/gwt/client/ICaption.java b/src/com/vaadin/terminal/gwt/client/ICaption.java
new file mode 100644
index 0000000000..972a844bf9
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ICaption.java
@@ -0,0 +1,443 @@
+/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.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.vaadin.terminal.gwt.client.ui.Icon;
+
+public class ICaption 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 Element clearElement;
+
+ private final ApplicationConnection client;
+
+ private boolean placedAfterComponent = false;
+ private boolean iconOnloadHandled = false;
+
+ private int maxWidth = -1;
+
+ private static String ATTRIBUTE_ICON = "icon";
+ private static String ATTRIBUTE_CAPTION = "caption";
+ private static String ATTRIBUTE_DESCRIPTION = "description";
+ private static String ATTRIBUTE_REQUIRED = "required";
+ private static String ATTRIBUTE_ERROR = "error";
+ private static String ATTRIBUTE_HIDEERRORS = "hideErrors";
+
+ /**
+ *
+ * @param component
+ * optional owner of caption. If not set, getOwner will return
+ * null
+ * @param client
+ */
+ public ICaption(Paintable component, ApplicationConnection client) {
+ super();
+ this.client = client;
+ owner = component;
+ setStyleName(CLASSNAME);
+ sinkEvents(ITooltip.TOOLTIP_EVENTS);
+
+ }
+
+ /**
+ * Updates the caption from UIDL.
+ *
+ * @param uidl
+ * @return true if the position where the caption should be placed has
+ * changed
+ */
+ public boolean updateCaption(UIDL uidl) {
+ setVisible(!uidl.getBooleanAttribute("invisible"));
+
+ boolean wasPlacedAfterComponent = placedAfterComponent;
+
+ placedAfterComponent = true;
+
+ String style = CLASSNAME;
+ if (uidl.hasAttribute("style")) {
+ final String[] styles = uidl.getStringAttribute("style").split(" ");
+ for (int i = 0; i < styles.length; i++) {
+ style += " " + CLASSNAME + "-" + styles[i];
+ }
+ }
+
+ if (uidl.hasAttribute("disabled")) {
+ style += " " + "i-disabled";
+ }
+
+ setStyleName(style);
+
+ if (uidl.hasAttribute(ATTRIBUTE_ICON)) {
+ if (icon == null) {
+ icon = new Icon(client);
+ icon.setWidth("0px");
+ icon.setHeight("0px");
+
+ DOM.insertChild(getElement(), icon.getElement(),
+ getInsertPosition(ATTRIBUTE_ICON));
+ }
+ placedAfterComponent = false;
+
+ iconOnloadHandled = false;
+ icon.setUri(uidl.getStringAttribute(ATTRIBUTE_ICON));
+
+ } else if (icon != null) {
+ // Remove existing
+ DOM.removeChild(getElement(), icon.getElement());
+ icon = null;
+ }
+
+ if (uidl.hasAttribute(ATTRIBUTE_CAPTION)) {
+ if (captionText == null) {
+ captionText = DOM.createDiv();
+ captionText.setClassName("i-captiontext");
+
+ DOM.insertChild(getElement(), captionText,
+ getInsertPosition(ATTRIBUTE_CAPTION));
+ }
+
+ // Update caption text
+ String c = uidl.getStringAttribute(ATTRIBUTE_CAPTION);
+ if (c == null) {
+ c = "";
+ } else {
+ placedAfterComponent = false;
+ }
+ DOM.setInnerText(captionText, c);
+ } else if (captionText != null) {
+ // Remove existing
+ DOM.removeChild(getElement(), captionText);
+ captionText = null;
+ }
+
+ if (uidl.hasAttribute(ATTRIBUTE_DESCRIPTION)) {
+ if (captionText != null) {
+ addStyleDependentName("hasdescription");
+ } else {
+ removeStyleDependentName("hasdescription");
+ }
+ }
+
+ if (uidl.getBooleanAttribute(ATTRIBUTE_REQUIRED)) {
+ if (requiredFieldIndicator == null) {
+ requiredFieldIndicator = DOM.createDiv();
+ requiredFieldIndicator
+ .setClassName("i-required-field-indicator");
+ DOM.setInnerText(requiredFieldIndicator, "*");
+
+ DOM.insertChild(getElement(), requiredFieldIndicator,
+ getInsertPosition(ATTRIBUTE_REQUIRED));
+ }
+ } else if (requiredFieldIndicator != null) {
+ // Remove existing
+ DOM.removeChild(getElement(), requiredFieldIndicator);
+ requiredFieldIndicator = null;
+ }
+
+ if (uidl.hasAttribute(ATTRIBUTE_ERROR)
+ && !uidl.getBooleanAttribute(ATTRIBUTE_HIDEERRORS)) {
+ if (errorIndicatorElement == null) {
+ errorIndicatorElement = DOM.createDiv();
+ DOM.setInnerHTML(errorIndicatorElement, "&nbsp;");
+ DOM.setElementProperty(errorIndicatorElement, "className",
+ "i-errorindicator");
+
+ DOM.insertChild(getElement(), errorIndicatorElement,
+ getInsertPosition(ATTRIBUTE_ERROR));
+ }
+ } else if (errorIndicatorElement != null) {
+ // Remove existing
+ DOM.removeChild(getElement(), errorIndicatorElement);
+ errorIndicatorElement = null;
+ }
+
+ if (clearElement == null) {
+ clearElement = DOM.createDiv();
+ DOM.setStyleAttribute(clearElement, "clear", "both");
+ DOM.setStyleAttribute(clearElement, "width", "0px");
+ DOM.setStyleAttribute(clearElement, "height", "0px");
+ DOM.setStyleAttribute(clearElement, "overflow", "hidden");
+ DOM.appendChild(getElement(), clearElement);
+ }
+
+ return (wasPlacedAfterComponent != placedAfterComponent);
+ }
+
+ private int getInsertPosition(String element) {
+ int pos = 0;
+ if (element.equals(ATTRIBUTE_ICON)) {
+ return pos;
+ }
+ if (icon != null) {
+ pos++;
+ }
+
+ if (element.equals(ATTRIBUTE_CAPTION)) {
+ return pos;
+ }
+
+ if (captionText != null) {
+ pos++;
+ }
+
+ if (element.equals(ATTRIBUTE_REQUIRED)) {
+ return pos;
+ }
+ if (requiredFieldIndicator != null) {
+ pos++;
+ }
+
+ // if (element.equals(ATTRIBUTE_ERROR)) {
+ // }
+ return pos;
+
+ }
+
+ @Override
+ public void onBrowserEvent(Event event) {
+ super.onBrowserEvent(event);
+ final Element target = DOM.eventGetTarget(event);
+ if (client != null && owner != null && target != getElement()) {
+ client.handleTooltipEvent(event, owner);
+ }
+
+ if (DOM.eventGetType(event) == Event.ONLOAD
+ && icon.getElement() == target && !iconOnloadHandled) {
+ icon.setWidth("");
+ icon.setHeight("");
+
+ /*
+ * IE6 pngFix causes two onload events to be fired and we want to
+ * react only to the first one
+ */
+ iconOnloadHandled = true;
+
+ // if max width defined, recalculate
+ if (maxWidth != -1) {
+ setMaxWidth(maxWidth);
+ } else {
+ String width = getElement().getStyle().getProperty("width");
+ if (width != null && !width.equals("")) {
+ setWidth(getRequiredWidth() + "px");
+ }
+ }
+
+ /*
+ * The size of the icon might affect the size of the component so we
+ * must report the size change to the parent TODO consider moving
+ * the responsibility of reacting to ONLOAD from ICaption to layouts
+ */
+ if (owner != null) {
+ Util.notifyParentOfSizeChange(owner, true);
+ } else {
+ ApplicationConnection
+ .getConsole()
+ .log(
+ "Warning: Icon load event was not propagated because ICaption owner is unknown.");
+ }
+ }
+ }
+
+ public static boolean isNeeded(UIDL uidl) {
+ if (uidl.getStringAttribute(ATTRIBUTE_CAPTION) != null) {
+ return true;
+ }
+ if (uidl.hasAttribute(ATTRIBUTE_ERROR)) {
+ return true;
+ }
+ if (uidl.hasAttribute(ATTRIBUTE_ICON)) {
+ return true;
+ }
+ if (uidl.hasAttribute(ATTRIBUTE_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;
+ }
+
+ public int getRenderedWidth() {
+ int width = 0;
+
+ if (icon != null) {
+ width += Util.getRequiredWidth(icon.getElement());
+ }
+
+ if (captionText != null) {
+ width += Util.getRequiredWidth(captionText);
+ }
+ if (requiredFieldIndicator != null) {
+ width += Util.getRequiredWidth(requiredFieldIndicator);
+ }
+ if (errorIndicatorElement != null) {
+ width += Util.getRequiredWidth(errorIndicatorElement);
+ }
+
+ return width;
+
+ }
+
+ public int getRequiredWidth() {
+ int width = 0;
+
+ if (icon != null) {
+ width += Util.getRequiredWidth(icon.getElement());
+ }
+ if (captionText != null) {
+ int textWidth = captionText.getScrollWidth();
+ if (BrowserInfo.get().isFF3()) {
+ /*
+ * In Firefox3 the caption might require more space than the
+ * scrollWidth returns as scrollWidth is rounded down.
+ */
+ int requiredWidth = Util.getRequiredWidth(captionText);
+ if (requiredWidth > textWidth) {
+ textWidth = requiredWidth;
+ }
+
+ }
+ width += textWidth;
+ }
+ if (requiredFieldIndicator != null) {
+ width += Util.getRequiredWidth(requiredFieldIndicator);
+ }
+ if (errorIndicatorElement != null) {
+ width += Util.getRequiredWidth(errorIndicatorElement);
+ }
+
+ return width;
+
+ }
+
+ public int getHeight() {
+ int height = 0;
+ int h;
+
+ if (icon != null) {
+ h = icon.getOffsetHeight();
+ if (h > height) {
+ height = h;
+ }
+ }
+
+ if (captionText != null) {
+ h = captionText.getOffsetHeight();
+ if (h > height) {
+ height = h;
+ }
+ }
+ if (requiredFieldIndicator != null) {
+ h = requiredFieldIndicator.getOffsetHeight();
+ if (h > height) {
+ height = h;
+ }
+ }
+ if (errorIndicatorElement != null) {
+ h = errorIndicatorElement.getOffsetHeight();
+ if (h > height) {
+ height = h;
+ }
+ }
+
+ return height;
+ }
+
+ public void setAlignment(String alignment) {
+ DOM.setStyleAttribute(getElement(), "textAlign", alignment);
+ }
+
+ public void setMaxWidth(int maxWidth) {
+ this.maxWidth = maxWidth;
+ DOM.setStyleAttribute(getElement(), "width", maxWidth + "px");
+
+ if (icon != null) {
+ DOM.setStyleAttribute(icon.getElement(), "width", "");
+ }
+
+ if (captionText != null) {
+ DOM.setStyleAttribute(captionText, "width", "");
+ }
+
+ int requiredWidth = getRequiredWidth();
+ /*
+ * ApplicationConnection.getConsole().log( "Caption maxWidth: " +
+ * maxWidth + ", requiredWidth: " + requiredWidth);
+ */
+ if (requiredWidth > maxWidth) {
+ // Needs to truncate and clip
+ int availableWidth = maxWidth;
+
+ // DOM.setStyleAttribute(getElement(), "width", maxWidth + "px");
+ if (requiredFieldIndicator != null) {
+ availableWidth -= Util.getRequiredWidth(requiredFieldIndicator);
+ }
+
+ if (errorIndicatorElement != null) {
+ availableWidth -= Util.getRequiredWidth(errorIndicatorElement);
+ }
+
+ if (availableWidth < 0) {
+ availableWidth = 0;
+ }
+
+ if (icon != null) {
+ int iconRequiredWidth = Util
+ .getRequiredWidth(icon.getElement());
+ if (availableWidth > iconRequiredWidth) {
+ availableWidth -= iconRequiredWidth;
+ } else {
+ DOM.setStyleAttribute(icon.getElement(), "width",
+ availableWidth + "px");
+ availableWidth = 0;
+ }
+ }
+ if (captionText != null) {
+ int captionWidth = Util.getRequiredWidth(captionText);
+ if (availableWidth > captionWidth) {
+ availableWidth -= captionWidth;
+
+ } else {
+ DOM.setStyleAttribute(captionText, "width", availableWidth
+ + "px");
+ availableWidth = 0;
+ }
+
+ }
+
+ }
+ }
+
+ protected Element getTextElement() {
+ return captionText;
+ }
+
+}
diff --git a/src/com/vaadin/terminal/gwt/client/ICaptionWrapper.java b/src/com/vaadin/terminal/gwt/client/ICaptionWrapper.java
new file mode 100644
index 0000000000..15f1c5e820
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ICaptionWrapper.java
@@ -0,0 +1,32 @@
+/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client;
+
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.Widget;
+
+public class ICaptionWrapper extends FlowPanel {
+
+ public static final String CLASSNAME = "i-captionwrapper";
+ ICaption caption;
+ Paintable widget;
+
+ public ICaptionWrapper(Paintable toBeWrapped, ApplicationConnection client) {
+ caption = new ICaption(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/vaadin/terminal/gwt/client/IDebugConsole.java b/src/com/vaadin/terminal/gwt/client/IDebugConsole.java
new file mode 100755
index 0000000000..6986aea2ed
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/IDebugConsole.java
@@ -0,0 +1,466 @@
+/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client;
+
+import java.util.List;
+import java.util.Set;
+
+import com.google.gwt.json.client.JSONArray;
+import com.google.gwt.json.client.JSONObject;
+import com.google.gwt.json.client.JSONValue;
+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.EventPreview;
+import com.google.gwt.user.client.Window;
+import com.google.gwt.user.client.Window.Location;
+import com.google.gwt.user.client.ui.Button;
+import com.google.gwt.user.client.ui.CheckBox;
+import com.google.gwt.user.client.ui.ClickListener;
+import com.google.gwt.user.client.ui.FlowPanel;
+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.Panel;
+import com.google.gwt.user.client.ui.Tree;
+import com.google.gwt.user.client.ui.TreeItem;
+import com.google.gwt.user.client.ui.VerticalPanel;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.terminal.gwt.client.ui.IToolkitOverlay;
+
+public final class IDebugConsole extends IToolkitOverlay implements Console {
+
+ /**
+ * Builds number. For example 0-custom_tag in 5.0.0-custom_tag.
+ */
+ public static final String VERSION;
+
+ /* Initialize version numbers from string replaced by build-script. */
+ static {
+ if ("@VERSION@".equals("@" + "VERSION" + "@")) {
+ VERSION = "5.9.9-INTERNAL-NONVERSIONED-DEBUG-BUILD";
+ } else {
+ VERSION = "@VERSION@";
+ }
+ }
+
+ Element caption = DOM.createDiv();
+
+ private Panel panel;
+
+ private Button clear = new Button("Clear console");
+ private Button restart = new Button("Restart app");
+ private Button forceLayout = new Button("Force layout");
+ private Button analyzeLayout = new Button("Analyze layouts");
+ private HorizontalPanel actions;
+ private boolean collapsed = false;
+
+ private boolean resizing;
+ private int startX;
+ private int startY;
+ private int initialW;
+ private int initialH;
+
+ private boolean moving = false;
+
+ private int origTop;
+
+ private int origLeft;
+
+ private ApplicationConnection client;
+
+ private static final String help = "Drag=move, shift-drag=resize, doubleclick=min/max."
+ + "Use debug=quiet to log only to browser console.";
+
+ public IDebugConsole(ApplicationConnection client,
+ ApplicationConfiguration cnf, boolean showWindow) {
+ super(false, false);
+
+ this.client = client;
+
+ panel = new FlowPanel();
+ if (showWindow) {
+ DOM.appendChild(getContainerElement(), caption);
+ setWidget(panel);
+ caption.setClassName("i-debug-console-caption");
+ setStyleName("i-debug-console");
+ DOM.setStyleAttribute(getElement(), "zIndex", 20000 + "");
+ DOM.setStyleAttribute(getElement(), "overflow", "hidden");
+
+ sinkEvents(Event.ONDBLCLICK);
+
+ sinkEvents(Event.MOUSEEVENTS);
+
+ panel.setStyleName("i-debug-console-content");
+
+ caption.setInnerHTML("Debug window");
+ caption.setTitle(help);
+
+ show();
+ minimize();
+
+ actions = new HorizontalPanel();
+ actions.add(clear);
+ actions.add(restart);
+ actions.add(forceLayout);
+ actions.add(analyzeLayout);
+
+ panel.add(actions);
+
+ panel.add(new HTML("<i>" + help + "</i>"));
+
+ clear.addClickListener(new ClickListener() {
+ public void onClick(Widget sender) {
+ int width = panel.getOffsetWidth();
+ int height = panel.getOffsetHeight();
+ panel = new FlowPanel();
+ panel.setPixelSize(width, height);
+ panel.setStyleName("i-debug-console-content");
+ panel.add(actions);
+ setWidget(panel);
+ }
+ });
+
+ restart.addClickListener(new ClickListener() {
+ public void onClick(Widget sender) {
+
+ String queryString = Window.Location.getQueryString();
+ if (queryString != null
+ && queryString.contains("restartApplications")) {
+ Window.Location.reload();
+ } else {
+ String url = Location.getHref();
+ String separator = "?";
+ if (url.contains("?")) {
+ separator = "&";
+ }
+ if (!url.contains("restartApplication")) {
+ url += separator;
+ url += "restartApplication";
+ }
+ if (!"".equals(Location.getHash())) {
+ String hash = Location.getHash();
+ url = url.replace(hash, "") + hash;
+ }
+ Window.Location.replace(url);
+ }
+
+ }
+ });
+
+ forceLayout.addClickListener(new ClickListener() {
+ public void onClick(Widget sender) {
+ IDebugConsole.this.client.forceLayout();
+ }
+ });
+
+ analyzeLayout.addClickListener(new ClickListener() {
+ public void onClick(Widget sender) {
+ List<ApplicationConnection> runningApplications = ApplicationConfiguration
+ .getRunningApplications();
+ for (ApplicationConnection applicationConnection : runningApplications) {
+ applicationConnection.analyzeLayouts();
+ }
+ }
+ });
+ analyzeLayout
+ .setTitle("Analyzes currently rendered view and "
+ + "reports possible common problems in usage of relative sizes."
+ + "Will cause server visit/rendering of whole screen + lose of"
+ + " all non committed variables form client side.");
+
+ }
+
+ log("Toolkit application servlet version: " + cnf.getServletVersion());
+ log("Widget set is built on version: " + VERSION);
+ log("Application version: " + cnf.getApplicationVersion());
+
+ if (!cnf.getServletVersion().equals(VERSION)) {
+ error("Warning: your widget set seems to be built with a different "
+ + "version than the one used on server. Unexpected "
+ + "behavior may occur.");
+ }
+ }
+
+ private EventPreview dragpreview = new EventPreview() {
+
+ public boolean onEventPreview(Event event) {
+ onBrowserEvent(event);
+ return false;
+ }
+ };
+
+ @Override
+ public void onBrowserEvent(Event event) {
+ super.onBrowserEvent(event);
+ switch (DOM.eventGetType(event)) {
+ case Event.ONMOUSEDOWN:
+ if (DOM.eventGetShiftKey(event)) {
+ resizing = true;
+ DOM.setCapture(getElement());
+ startX = DOM.eventGetScreenX(event);
+ startY = DOM.eventGetScreenY(event);
+ initialW = IDebugConsole.this.getOffsetWidth();
+ initialH = IDebugConsole.this.getOffsetHeight();
+ DOM.eventCancelBubble(event, true);
+ DOM.eventPreventDefault(event);
+ DOM.addEventPreview(dragpreview);
+ } else if (DOM.eventGetTarget(event) == caption) {
+ moving = true;
+ startX = DOM.eventGetScreenX(event);
+ startY = DOM.eventGetScreenY(event);
+ origTop = getAbsoluteTop();
+ origLeft = getAbsoluteLeft();
+ DOM.eventCancelBubble(event, true);
+ DOM.eventPreventDefault(event);
+ DOM.addEventPreview(dragpreview);
+ }
+
+ break;
+ case Event.ONMOUSEMOVE:
+ if (resizing) {
+ int deltaX = startX - DOM.eventGetScreenX(event);
+ int detalY = startY - DOM.eventGetScreenY(event);
+ int w = initialW - deltaX;
+ if (w < 30) {
+ w = 30;
+ }
+ int h = initialH - detalY;
+ if (h < 40) {
+ h = 40;
+ }
+ IDebugConsole.this.setPixelSize(w, h);
+ DOM.eventCancelBubble(event, true);
+ DOM.eventPreventDefault(event);
+ } else if (moving) {
+ int deltaX = startX - DOM.eventGetScreenX(event);
+ int detalY = startY - DOM.eventGetScreenY(event);
+ int left = origLeft - deltaX;
+ if (left < 0) {
+ left = 0;
+ }
+ int top = origTop - detalY;
+ if (top < 0) {
+ top = 0;
+ }
+ IDebugConsole.this.setPopupPosition(left, top);
+ DOM.eventCancelBubble(event, true);
+ DOM.eventPreventDefault(event);
+ }
+ break;
+ case Event.ONLOSECAPTURE:
+ case Event.ONMOUSEUP:
+ if (resizing) {
+ DOM.releaseCapture(getElement());
+ resizing = false;
+ } else if (moving) {
+ DOM.releaseCapture(getElement());
+ moving = false;
+ }
+ DOM.removeEventPreview(dragpreview);
+ break;
+ case Event.ONDBLCLICK:
+ if (DOM.eventGetTarget(event) == caption) {
+ if (collapsed) {
+ panel.setVisible(true);
+ setPixelSize(220, 300);
+ } else {
+ panel.setVisible(false);
+ setPixelSize(120, 20);
+ }
+ collapsed = !collapsed;
+ }
+ break;
+ default:
+ break;
+ }
+
+ }
+
+ private void minimize() {
+ setPixelSize(400, 150);
+ setPopupPosition(Window.getClientWidth() - 410, Window
+ .getClientHeight() - 160);
+ }
+
+ @Override
+ public void setPixelSize(int width, int height) {
+ panel.setHeight((height - 20) + "px");
+ panel.setWidth((width - 2) + "px");
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.terminal.gwt.client.Console#log(java.lang.String)
+ */
+ public void log(String msg) {
+ panel.add(new HTML(msg));
+ System.out.println(msg);
+ consoleLog(msg);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.terminal.gwt.client.Console#error(java.lang.String)
+ */
+ public void error(String msg) {
+ panel.add((new HTML(msg)));
+ System.err.println(msg);
+ consoleErr(msg);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.terminal.gwt.client.Console#printObject(java.lang.
+ * Object)
+ */
+ public void printObject(Object msg) {
+ panel.add((new Label(msg.toString())));
+ consoleLog(msg.toString());
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.terminal.gwt.client.Console#dirUIDL(com.vaadin
+ * .terminal.gwt.client.UIDL)
+ */
+ public void dirUIDL(UIDL u) {
+ panel.add(u.print_r());
+ consoleLog(u.getChildrenAsXML());
+ }
+
+ private static native void consoleLog(String msg)
+ /*-{
+ if($wnd.console && $wnd.console.log) {
+ $wnd.console.log(msg);
+ }
+ }-*/;
+
+ private static native void consoleErr(String msg)
+ /*-{
+ if($wnd.console) {
+ if ($wnd.console.error)
+ $wnd.console.error(msg);
+ else if ($wnd.console.log)
+ $wnd.console.log(msg);
+ }
+ }-*/;
+
+ public void printLayoutProblems(JSONArray array, ApplicationConnection ac,
+ Set<Paintable> zeroHeightComponents,
+ Set<Paintable> zeroWidthComponents) {
+ int size = array.size();
+ panel.add(new HTML("<div>************************</di>"
+ + "<h4>Layouts analyzed on server, total top level problems: "
+ + size + " </h4>"));
+ if (size > 0) {
+ Tree tree = new Tree();
+ TreeItem root = new TreeItem("Root problems");
+ for (int i = 0; i < size; i++) {
+ JSONObject error = array.get(i).isObject();
+ printLayoutError(error, root, ac);
+ }
+ panel.add(tree);
+ tree.addItem(root);
+
+ }
+ if (zeroHeightComponents.size() > 0 || zeroWidthComponents.size() > 0) {
+ panel.add(new HTML("<h4> Client side notifications</h4>"
+ + " <em>Following relative sized components where "
+ + "rendered to zero size container on client side."
+ + " Note that these are not necessary invalid "
+ + "states. Just reported here as they might be.</em>"));
+ if (zeroHeightComponents.size() > 0) {
+ panel.add(new HTML(
+ "<p><strong>Vertically zero size:</strong><p>"));
+ printClientSideDetectedIssues(zeroHeightComponents, ac);
+ }
+ if (zeroWidthComponents.size() > 0) {
+ panel.add(new HTML(
+ "<p><strong>Horizontally zero size:</strong><p>"));
+ printClientSideDetectedIssues(zeroWidthComponents, ac);
+ }
+ }
+ log("************************");
+ }
+
+ private void printClientSideDetectedIssues(
+ Set<Paintable> zeroHeightComponents, ApplicationConnection ac) {
+ for (final Paintable paintable : zeroHeightComponents) {
+ final Container layout = Util.getLayout((Widget) paintable);
+
+ VerticalPanel errorDetails = new VerticalPanel();
+ errorDetails.add(new Label("" + Util.getSimpleName(paintable)
+ + " inside " + Util.getSimpleName(layout)));
+ final CheckBox emphasisInUi = new CheckBox(
+ "Emphasis components parent in UI (actual component is not visible)");
+ emphasisInUi.addClickListener(new ClickListener() {
+ public void onClick(Widget sender) {
+ if (paintable != null) {
+ Element element2 = ((Widget) layout).getElement();
+ Widget.setStyleName(element2, "invalidlayout",
+ emphasisInUi.isChecked());
+ }
+ }
+ });
+ errorDetails.add(emphasisInUi);
+ panel.add(errorDetails);
+ }
+ }
+
+ private void printLayoutError(JSONObject error, TreeItem parent,
+ final ApplicationConnection ac) {
+ final String pid = error.get("id").isString().stringValue();
+ final Paintable paintable = ac.getPaintable(pid);
+
+ TreeItem errorNode = new TreeItem();
+ VerticalPanel errorDetails = new VerticalPanel();
+ errorDetails.add(new Label(Util.getSimpleName(paintable) + " id: "
+ + pid));
+ if (error.containsKey("heightMsg")) {
+ errorDetails.add(new Label("Height problem: "
+ + error.get("heightMsg")));
+ }
+ if (error.containsKey("widthMsg")) {
+ errorDetails.add(new Label("Width problem: "
+ + error.get("widthMsg")));
+ }
+ final CheckBox emphasisInUi = new CheckBox("Emphasis component in UI");
+ emphasisInUi.addClickListener(new ClickListener() {
+ public void onClick(Widget sender) {
+ if (paintable != null) {
+ Element element2 = ((Widget) paintable).getElement();
+ Widget.setStyleName(element2, "invalidlayout", emphasisInUi
+ .isChecked());
+ }
+ }
+ });
+ errorDetails.add(emphasisInUi);
+ errorNode.setWidget(errorDetails);
+ if (error.containsKey("subErrors")) {
+ HTML l = new HTML(
+ "<em>Expand this node to show problems that may be dependent on this problem.</em>");
+ errorDetails.add(l);
+ JSONArray array = error.get("subErrors").isArray();
+ for (int i = 0; i < array.size(); i++) {
+ JSONValue value = array.get(i);
+ if (value != null && value.isObject() != null) {
+ printLayoutError(value.isObject(), errorNode, ac);
+ } else {
+ System.out.print(value);
+ }
+ }
+
+ }
+ parent.addItem(errorNode);
+ }
+}
diff --git a/src/com/vaadin/terminal/gwt/client/IErrorMessage.java b/src/com/vaadin/terminal/gwt/client/IErrorMessage.java
new file mode 100644
index 0000000000..0f5f45663e
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/IErrorMessage.java
@@ -0,0 +1,73 @@
+/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.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.vaadin.terminal.gwt.client.ui.IToolkitOverlay;
+
+public class IErrorMessage extends FlowPanel {
+ public static final String CLASSNAME = "i-errormessage";
+
+ public IErrorMessage() {
+ 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 IErrorMessage childError = new IErrorMessage();
+ add(childError);
+ childError.updateFromUIDL((UIDL) child);
+ }
+ }
+ }
+ }
+
+ /**
+ * Shows this error message next to given element.
+ *
+ * @param indicatorElement
+ */
+ public void showAt(Element indicatorElement) {
+ IToolkitOverlay errorContainer = (IToolkitOverlay) getParent();
+ if (errorContainer == null) {
+ errorContainer = new IToolkitOverlay();
+ 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 IToolkitOverlay errorContainer = (IToolkitOverlay) getParent();
+ if (errorContainer != null) {
+ errorContainer.hide();
+ }
+ }
+}
diff --git a/src/com/vaadin/terminal/gwt/client/ITooltip.java b/src/com/vaadin/terminal/gwt/client/ITooltip.java
new file mode 100644
index 0000000000..bbea0b4bae
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ITooltip.java
@@ -0,0 +1,225 @@
+/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+package com.vaadin.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.vaadin.terminal.gwt.client.ui.IToolkitOverlay;
+
+/**
+ * TODO open for extension
+ */
+public class ITooltip extends IToolkitOverlay {
+ 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;
+ private static final int QUICK_OPEN_TIMEOUT = 1000;
+ private static final int CLOSE_TIMEOUT = 300;
+ private static final int OPEN_DELAY = 750;
+ private static final int QUICK_OPEN_DELAY = 100;
+ IErrorMessage em = new IErrorMessage();
+ Element description = DOM.createDiv();
+ private Paintable tooltipOwner;
+ private boolean closing = false;
+ private boolean opening = false;
+ private ApplicationConnection ac;
+ // Open next tooltip faster. Disabled after 2 sec of showTooltip-silence.
+ private boolean justClosed = false;
+
+ public ITooltip(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) {
+ // return to same tooltip, cancel closing
+ closeTimer.cancel();
+ closing = false;
+ justClosedTimer.cancel();
+ justClosed = false;
+ return;
+ }
+
+ if (closing) {
+ closeNow();
+ }
+
+ updatePosition(event);
+
+ if (opening) {
+ showTimer.cancel();
+ }
+ tooltipOwner = owner;
+ if (justClosed) {
+ showTimer.schedule(QUICK_OPEN_DELAY);
+ } else {
+ showTimer.schedule(OPEN_DELAY);
+ }
+ opening = true;
+ }
+
+ private void closeNow() {
+ if (closing) {
+ hide();
+ tooltipOwner = null;
+ setWidth("");
+ closing = false;
+ }
+ }
+
+ private Timer showTimer = new Timer() {
+ @Override
+ public void run() {
+ TooltipInfo info = ac.getTitleInfo(tooltipOwner);
+ if (null != info) {
+ show(info);
+ }
+ opening = false;
+ }
+ };
+
+ private Timer closeTimer = new Timer() {
+ @Override
+ public void run() {
+ closeNow();
+ justClosedTimer.schedule(2000);
+ justClosed = true;
+ }
+ };
+
+ private Timer justClosedTimer = new Timer() {
+ @Override
+ public void run() {
+ justClosed = false;
+ }
+ };
+
+ public void hideTooltip() {
+ if (opening) {
+ showTimer.cancel();
+ opening = false;
+ tooltipOwner = null;
+ }
+ if (!isAttached()) {
+ return;
+ }
+ if (closing) {
+ // already about to close
+ return;
+ }
+ closeTimer.schedule(CLOSE_TIMEOUT);
+ closing = true;
+ justClosed = true;
+ justClosedTimer.schedule(QUICK_OPEN_TIMEOUT);
+
+ }
+
+ 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 ((ITooltip.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();
+ }
+ }
+
+ @Override
+ 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/vaadin/terminal/gwt/client/LocaleNotLoadedException.java b/src/com/vaadin/terminal/gwt/client/LocaleNotLoadedException.java
new file mode 100644
index 0000000000..6312801daf
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/LocaleNotLoadedException.java
@@ -0,0 +1,13 @@
+/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client;
+
+@SuppressWarnings("serial")
+public class LocaleNotLoadedException extends Exception {
+
+ public LocaleNotLoadedException(String locale) {
+ super(locale);
+ }
+}
diff --git a/src/com/vaadin/terminal/gwt/client/LocaleService.java b/src/com/vaadin/terminal/gwt/client/LocaleService.java
new file mode 100644
index 0000000000..b62709fb2a
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/LocaleService.java
@@ -0,0 +1,197 @@
+/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client;
+
+import java.util.HashMap;
+import java.util.Map;
+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;
+
+/**
+ * Date / time etc. localisation service for all widgets. Caches all loaded
+ * locales as JSONObjects.
+ *
+ * @author IT Mill Ltd.
+ *
+ */
+public class LocaleService {
+
+ private static Map cache = new HashMap();
+ private static String defaultLocale;
+
+ public static void addLocale(JSONObject json) {
+ final String key = ((JSONString) json.get("name")).stringValue();
+ if (cache.containsKey(key)) {
+ cache.remove(key);
+ }
+ cache.put(key, json);
+ if (cache.size() == 1) {
+ setDefaultLocale(key);
+ }
+ }
+
+ public static void setDefaultLocale(String locale) {
+ defaultLocale = locale;
+ }
+
+ public static String getDefaultLocale() {
+ return defaultLocale;
+ }
+
+ public static Set getAvailableLocales() {
+ return cache.keySet();
+ }
+
+ public static String[] getMonthNames(String locale)
+ throws LocaleNotLoadedException {
+ if (cache.containsKey(locale)) {
+ final JSONObject l = (JSONObject) cache.get(locale);
+ final JSONArray mn = (JSONArray) l.get("mn");
+ final String[] temp = new String[12];
+ temp[0] = ((JSONString) mn.get(0)).stringValue();
+ temp[1] = ((JSONString) mn.get(1)).stringValue();
+ temp[2] = ((JSONString) mn.get(2)).stringValue();
+ temp[3] = ((JSONString) mn.get(3)).stringValue();
+ temp[4] = ((JSONString) mn.get(4)).stringValue();
+ temp[5] = ((JSONString) mn.get(5)).stringValue();
+ temp[6] = ((JSONString) mn.get(6)).stringValue();
+ temp[7] = ((JSONString) mn.get(7)).stringValue();
+ temp[8] = ((JSONString) mn.get(8)).stringValue();
+ temp[9] = ((JSONString) mn.get(9)).stringValue();
+ temp[10] = ((JSONString) mn.get(10)).stringValue();
+ temp[11] = ((JSONString) mn.get(11)).stringValue();
+ return temp;
+ } else {
+ throw new LocaleNotLoadedException(locale);
+ }
+ }
+
+ public static String[] getShortMonthNames(String locale)
+ throws LocaleNotLoadedException {
+ if (cache.containsKey(locale)) {
+ final JSONObject l = (JSONObject) cache.get(locale);
+ final JSONArray smn = (JSONArray) l.get("smn");
+ final String[] temp = new String[12];
+ temp[0] = ((JSONString) smn.get(0)).stringValue();
+ temp[1] = ((JSONString) smn.get(1)).stringValue();
+ temp[2] = ((JSONString) smn.get(2)).stringValue();
+ temp[3] = ((JSONString) smn.get(3)).stringValue();
+ temp[4] = ((JSONString) smn.get(4)).stringValue();
+ temp[5] = ((JSONString) smn.get(5)).stringValue();
+ temp[6] = ((JSONString) smn.get(6)).stringValue();
+ temp[7] = ((JSONString) smn.get(7)).stringValue();
+ temp[8] = ((JSONString) smn.get(8)).stringValue();
+ temp[9] = ((JSONString) smn.get(9)).stringValue();
+ temp[10] = ((JSONString) smn.get(10)).stringValue();
+ temp[11] = ((JSONString) smn.get(11)).stringValue();
+ return temp;
+ } else {
+ throw new LocaleNotLoadedException(locale);
+ }
+ }
+
+ public static String[] getDayNames(String locale)
+ throws LocaleNotLoadedException {
+ if (cache.containsKey(locale)) {
+ final JSONObject l = (JSONObject) cache.get(locale);
+ final JSONArray dn = (JSONArray) l.get("dn");
+ final String[] temp = new String[7];
+ temp[0] = ((JSONString) dn.get(0)).stringValue();
+ temp[1] = ((JSONString) dn.get(1)).stringValue();
+ temp[2] = ((JSONString) dn.get(2)).stringValue();
+ temp[3] = ((JSONString) dn.get(3)).stringValue();
+ temp[4] = ((JSONString) dn.get(4)).stringValue();
+ temp[5] = ((JSONString) dn.get(5)).stringValue();
+ temp[6] = ((JSONString) dn.get(6)).stringValue();
+ return temp;
+ } else {
+ throw new LocaleNotLoadedException(locale);
+ }
+ }
+
+ public static String[] getShortDayNames(String locale)
+ throws LocaleNotLoadedException {
+ if (cache.containsKey(locale)) {
+ final JSONObject l = (JSONObject) cache.get(locale);
+ final JSONArray sdn = (JSONArray) l.get("sdn");
+ final String[] temp = new String[7];
+ temp[0] = ((JSONString) sdn.get(0)).stringValue();
+ temp[1] = ((JSONString) sdn.get(1)).stringValue();
+ temp[2] = ((JSONString) sdn.get(2)).stringValue();
+ temp[3] = ((JSONString) sdn.get(3)).stringValue();
+ temp[4] = ((JSONString) sdn.get(4)).stringValue();
+ temp[5] = ((JSONString) sdn.get(5)).stringValue();
+ temp[6] = ((JSONString) sdn.get(6)).stringValue();
+ return temp;
+ } else {
+ throw new LocaleNotLoadedException(locale);
+ }
+ }
+
+ public static int getFirstDayOfWeek(String locale)
+ throws LocaleNotLoadedException {
+ if (cache.containsKey(locale)) {
+ final JSONObject l = (JSONObject) cache.get(locale);
+ final JSONNumber fdow = (JSONNumber) l.get("fdow");
+ return (int) fdow.getValue();
+ } else {
+ throw new LocaleNotLoadedException(locale);
+ }
+ }
+
+ public static String getDateFormat(String locale)
+ throws LocaleNotLoadedException {
+ if (cache.containsKey(locale)) {
+ final JSONObject l = (JSONObject) cache.get(locale);
+ final JSONString df = (JSONString) l.get("df");
+ return df.stringValue();
+ } else {
+ throw new LocaleNotLoadedException(locale);
+ }
+ }
+
+ public static boolean isTwelveHourClock(String locale)
+ throws LocaleNotLoadedException {
+ if (cache.containsKey(locale)) {
+ final JSONObject l = (JSONObject) cache.get(locale);
+ final JSONBoolean thc = (JSONBoolean) l.get("thc");
+ return thc.booleanValue();
+ } else {
+ throw new LocaleNotLoadedException(locale);
+ }
+ }
+
+ public static String getClockDelimiter(String locale)
+ throws LocaleNotLoadedException {
+ if (cache.containsKey(locale)) {
+ final JSONObject l = (JSONObject) cache.get(locale);
+ final JSONString hmd = (JSONString) l.get("hmd");
+ return hmd.stringValue();
+ } else {
+ throw new LocaleNotLoadedException(locale);
+ }
+ }
+
+ public static String[] getAmPmStrings(String locale)
+ throws LocaleNotLoadedException {
+ if (cache.containsKey(locale)) {
+ final JSONObject l = (JSONObject) cache.get(locale);
+ final JSONArray ampm = (JSONArray) l.get("ampm");
+ final String[] temp = new String[2];
+ temp[0] = ((JSONString) ampm.get(0)).stringValue();
+ temp[1] = ((JSONString) ampm.get(1)).stringValue();
+ return temp;
+ } else {
+ throw new LocaleNotLoadedException(locale);
+ }
+
+ }
+
+}
diff --git a/src/com/vaadin/terminal/gwt/client/MouseEventDetails.java b/src/com/vaadin/terminal/gwt/client/MouseEventDetails.java
new file mode 100644
index 0000000000..175dfd8ce2
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/MouseEventDetails.java
@@ -0,0 +1,93 @@
+package com.vaadin.terminal.gwt.client;
+
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.Event;
+
+/**
+ * Helper class to store and transfer mouse event details.
+ */
+public class MouseEventDetails {
+ public static final int BUTTON_LEFT = Event.BUTTON_LEFT;
+ public static final int BUTTON_MIDDLE = Event.BUTTON_MIDDLE;
+ public static final int BUTTON_RIGHT = Event.BUTTON_RIGHT;
+
+ private static final char DELIM = ',';
+
+ private int button;
+ private int clientX;
+ private int clientY;
+ private boolean altKey;
+ private boolean ctrlKey;
+ private boolean metaKey;
+ private boolean shiftKey;
+ private int type;
+
+ public int getButton() {
+ return button;
+ }
+
+ public int getClientX() {
+ return clientX;
+ }
+
+ public int getClientY() {
+ return clientY;
+ }
+
+ public boolean isAltKey() {
+ return altKey;
+ }
+
+ public boolean isCtrlKey() {
+ return ctrlKey;
+ }
+
+ public boolean isMetaKey() {
+ return metaKey;
+ }
+
+ public boolean isShiftKey() {
+ return shiftKey;
+ }
+
+ public MouseEventDetails(Event evt) {
+ button = DOM.eventGetButton(evt);
+ clientX = DOM.eventGetClientX(evt);
+ clientY = DOM.eventGetClientY(evt);
+ altKey = DOM.eventGetAltKey(evt);
+ ctrlKey = DOM.eventGetCtrlKey(evt);
+ metaKey = DOM.eventGetMetaKey(evt);
+ shiftKey = DOM.eventGetShiftKey(evt);
+ type = DOM.eventGetType(evt);
+ }
+
+ private MouseEventDetails() {
+ }
+
+ @Override
+ public String toString() {
+ return "" + button + DELIM + clientX + DELIM + clientY + DELIM + altKey
+ + DELIM + ctrlKey + DELIM + metaKey + DELIM + shiftKey + DELIM
+ + type;
+ }
+
+ public static MouseEventDetails deSerialize(String serializedString) {
+ MouseEventDetails instance = new MouseEventDetails();
+ String[] fields = serializedString.split(",");
+
+ instance.button = Integer.parseInt(fields[0]);
+ instance.clientX = Integer.parseInt(fields[1]);
+ instance.clientY = Integer.parseInt(fields[2]);
+ instance.altKey = Boolean.valueOf(fields[3]).booleanValue();
+ instance.ctrlKey = Boolean.valueOf(fields[4]).booleanValue();
+ instance.metaKey = Boolean.valueOf(fields[5]).booleanValue();
+ instance.shiftKey = Boolean.valueOf(fields[6]).booleanValue();
+ instance.type = Integer.parseInt(fields[7]);
+ return instance;
+ }
+
+ public boolean isDoubleClick() {
+ return type == Event.ONDBLCLICK;
+ }
+
+}
diff --git a/src/com/vaadin/terminal/gwt/client/NullConsole.java b/src/com/vaadin/terminal/gwt/client/NullConsole.java
new file mode 100644
index 0000000000..24c692d44a
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/NullConsole.java
@@ -0,0 +1,36 @@
+/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client;
+
+import java.util.Set;
+
+import com.google.gwt.json.client.JSONArray;
+
+/**
+ * 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) {
+ }
+
+ public void printLayoutProblems(JSONArray array,
+ ApplicationConnection applicationConnection,
+ Set<Paintable> zeroHeightComponents,
+ Set<Paintable> zeroWidthComponents) {
+ }
+
+}
diff --git a/src/com/vaadin/terminal/gwt/client/Paintable.java b/src/com/vaadin/terminal/gwt/client/Paintable.java
new file mode 100644
index 0000000000..d4ff755763
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/Paintable.java
@@ -0,0 +1,10 @@
+/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client;
+
+public interface Paintable {
+
+ public void updateFromUIDL(UIDL uidl, ApplicationConnection client);
+}
diff --git a/src/com/vaadin/terminal/gwt/client/RenderInformation.java b/src/com/vaadin/terminal/gwt/client/RenderInformation.java
new file mode 100644
index 0000000000..4dd7d1ee2f
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/RenderInformation.java
@@ -0,0 +1,125 @@
+package com.vaadin.terminal.gwt.client;
+
+import com.google.gwt.user.client.Element;
+
+/**
+ * Contains size information about a rendered container and its content area.
+ *
+ * @author Artur Signell
+ *
+ */
+public class RenderInformation {
+
+ private RenderSpace contentArea = new RenderSpace();
+ private Size renderedSize = new Size(-1, -1);
+
+ public void setContentAreaWidth(int w) {
+ contentArea.setWidth(w);
+ }
+
+ public void setContentAreaHeight(int h) {
+ contentArea.setHeight(h);
+ }
+
+ public RenderSpace getContentAreaSize() {
+ return contentArea;
+
+ }
+
+ public Size getRenderedSize() {
+ return renderedSize;
+ }
+
+ /**
+ * Update the size of the widget.
+ *
+ * @param widget
+ *
+ * @return true if the size has changed since last update
+ */
+ public boolean updateSize(Element element) {
+ Size newSize = new Size(element.getOffsetWidth(), element
+ .getOffsetHeight());
+ if (newSize.equals(renderedSize)) {
+ return false;
+ } else {
+ renderedSize = newSize;
+ return true;
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "RenderInformation [contentArea=" + contentArea
+ + ",renderedSize=" + renderedSize + "]";
+
+ }
+
+ public static class FloatSize {
+
+ private float width, height;
+
+ public FloatSize(float width, float height) {
+ this.width = width;
+ this.height = height;
+ }
+
+ public float getWidth() {
+ return width;
+ }
+
+ public void setWidth(float width) {
+ this.width = width;
+ }
+
+ public float getHeight() {
+ return height;
+ }
+
+ public void setHeight(float height) {
+ this.height = height;
+ }
+
+ }
+
+ public static class Size {
+
+ private int width, height;
+
+ @Override
+ public boolean equals(Object obj) {
+ Size other = (Size) obj;
+ return other.width == width && other.height == height;
+ }
+
+ public Size() {
+ }
+
+ public Size(int width, int height) {
+ this.height = height;
+ this.width = width;
+ }
+
+ public int getWidth() {
+ return width;
+ }
+
+ public void setWidth(int width) {
+ this.width = width;
+ }
+
+ public int getHeight() {
+ return height;
+ }
+
+ public void setHeight(int height) {
+ this.height = height;
+ }
+
+ @Override
+ public String toString() {
+ return "Size [width=" + width + ",height=" + height + "]";
+ }
+ }
+
+}
diff --git a/src/com/vaadin/terminal/gwt/client/RenderSpace.java b/src/com/vaadin/terminal/gwt/client/RenderSpace.java
new file mode 100644
index 0000000000..42ec8f7fcb
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/RenderSpace.java
@@ -0,0 +1,53 @@
+package com.vaadin.terminal.gwt.client;
+
+import com.vaadin.terminal.gwt.client.RenderInformation.Size;
+
+/**
+ * Contains information about render area.
+ */
+public class RenderSpace extends Size {
+
+ private int scrollBarSize = 0;
+
+ public RenderSpace(int width, int height) {
+ super(width, height);
+ }
+
+ public RenderSpace() {
+ }
+
+ public RenderSpace(int width, int height, boolean useNativeScrollbarSize) {
+ super(width, height);
+ if (useNativeScrollbarSize) {
+ scrollBarSize = Util.getNativeScrollbarSize();
+ }
+ }
+
+ /**
+ * Returns pixels available vertically for contained widget, including
+ * possible scrollbars.
+ */
+ @Override
+ public int getHeight() {
+ return super.getHeight();
+ }
+
+ /**
+ * Returns pixels available horizontally for contained widget, including
+ * possible scrollbars.
+ */
+ @Override
+ public int getWidth() {
+ return super.getWidth();
+ }
+
+ /**
+ * In case containing block has oveflow: auto, this method must return
+ * number of pixels used by scrollbar. Returning zero means either that no
+ * scrollbar will be visible.
+ */
+ public int getScrollbarSize() {
+ return scrollBarSize;
+ }
+
+}
diff --git a/src/com/vaadin/terminal/gwt/client/StyleConstants.java b/src/com/vaadin/terminal/gwt/client/StyleConstants.java
new file mode 100644
index 0000000000..9d8f228d3d
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/StyleConstants.java
@@ -0,0 +1,17 @@
+/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.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/vaadin/terminal/gwt/client/TooltipInfo.java b/src/com/vaadin/terminal/gwt/client/TooltipInfo.java
new file mode 100644
index 0000000000..9a66bb14c8
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/TooltipInfo.java
@@ -0,0 +1,28 @@
+/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+package com.vaadin.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/vaadin/terminal/gwt/client/UIDL.java b/src/com/vaadin/terminal/gwt/client/UIDL.java
new file mode 100644
index 0000000000..2205f6b6dd
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/UIDL.java
@@ -0,0 +1,472 @@
+/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.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;
+ }
+
+ @Override
+ 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 IUIDLBrowser print_r() {
+ return new IUIDLBrowser();
+ }
+
+ private class IUIDLBrowser extends Tree {
+ public IUIDLBrowser() {
+
+ 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);
+ IUIDLBrowser.this.removeTreeListener(this);
+ addItem(dir());
+ final Iterator it = treeItemIterator();
+ while (it.hasNext()) {
+ ((TreeItem) it.next()).setState(true);
+ }
+ }
+ }
+
+ public void onTreeItemSelected(TreeItem item) {
+ }
+
+ });
+
+ }
+
+ @Override
+ 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) {
+ // Ignored, 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<String> getStringArrayVariableAsSet(String name) {
+ final JSONArray a = getArrayVariable(name);
+ final HashSet<String> s = new HashSet<String>();
+ 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/vaadin/terminal/gwt/client/Util.java b/src/com/vaadin/terminal/gwt/client/Util.java
new file mode 100644
index 0000000000..99e9e8cce1
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/Util.java
@@ -0,0 +1,703 @@
+/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Map;
+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.Timer;
+import com.google.gwt.user.client.Window;
+import com.google.gwt.user.client.ui.HasWidgets;
+import com.google.gwt.user.client.ui.RootPanel;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.terminal.gwt.client.RenderInformation.FloatSize;
+
+public class Util {
+
+ /**
+ * Helper method for debugging purposes.
+ *
+ * Stops execution on firefox browsers on a breakpoint.
+ *
+ */
+ public static native void browserDebugger()
+ /*-{
+ if($wnd.console)
+ debugger;
+ }-*/;
+
+ private static final int LAZY_SIZE_CHANGE_TIMEOUT = 400;
+ private static Set<Paintable> latelyChangedWidgets = new HashSet<Paintable>();
+
+ private static Timer lazySizeChangeTimer = new Timer() {
+ private boolean lazySizeChangeTimerScheduled = false;
+
+ @Override
+ public void run() {
+ componentSizeUpdated(latelyChangedWidgets);
+ latelyChangedWidgets.clear();
+ lazySizeChangeTimerScheduled = false;
+ }
+
+ @Override
+ public void schedule(int delayMillis) {
+ if (lazySizeChangeTimerScheduled) {
+ cancel();
+ } else {
+ lazySizeChangeTimerScheduled = true;
+ }
+ super.schedule(delayMillis);
+ }
+ };
+
+ /**
+ * This helper method can be called if components size have been changed
+ * outside rendering phase. It notifies components parent about the size
+ * change so it can react.
+ *
+ * When using this method, developer should consider if size changes could
+ * be notified lazily. If lazy flag is true, method will save widget and
+ * wait for a moment until it notifies parents in chunks. This may vastly
+ * optimize layout in various situation. Example: if component have a lot of
+ * images their onload events may fire "layout phase" many times in a short
+ * period.
+ *
+ * @param widget
+ * @param lazy
+ * run componentSizeUpdated lazyly
+ */
+ public static void notifyParentOfSizeChange(Paintable widget, boolean lazy) {
+ if (lazy) {
+ latelyChangedWidgets.add(widget);
+ lazySizeChangeTimer.schedule(LAZY_SIZE_CHANGE_TIMEOUT);
+ } else {
+ Set<Paintable> widgets = new HashSet<Paintable>();
+ widgets.add(widget);
+ Util.componentSizeUpdated(widgets);
+ }
+ }
+
+ /**
+ * Called when the size of one or more widgets have changed during
+ * rendering. Finds parent container and notifies them of the size change.
+ *
+ * @param widgets
+ */
+ public static void componentSizeUpdated(Set<Paintable> widgets) {
+ if (widgets.isEmpty()) {
+ return;
+ }
+
+ Map<Container, Set<Paintable>> childWidgets = new HashMap<Container, Set<Paintable>>();
+
+ for (Paintable widget : widgets) {
+ // ApplicationConnection.getConsole().log(
+ // "Widget " + Util.getSimpleName(widget) + " size updated");
+ Widget parent = ((Widget) widget).getParent();
+ while (parent != null && !(parent instanceof Container)) {
+ parent = parent.getParent();
+ }
+ if (parent != null) {
+ Set<Paintable> set = childWidgets.get(parent);
+ if (set == null) {
+ set = new HashSet<Paintable>();
+ childWidgets.put((Container) parent, set);
+ }
+ set.add(widget);
+ }
+ }
+
+ Set<Paintable> parentChanges = new HashSet<Paintable>();
+ for (Container parent : childWidgets.keySet()) {
+ if (!parent.requestLayout(childWidgets.get(parent))) {
+ parentChanges.add(parent);
+ }
+ }
+
+ componentSizeUpdated(parentChanges);
+ }
+
+ public static float parseRelativeSize(String size) {
+ if (size == null || !size.endsWith("%")) {
+ return -1;
+ }
+
+ try {
+ return Float.parseFloat(size.substring(0, size.length() - 1));
+ } catch (Exception e) {
+ ClientExceptionHandler.displayError(
+ "Unable to parse relative size", e);
+ }
+
+ return -1;
+ }
+
+ /**
+ * Returns closest parent Widget in hierarchy that implements Container
+ * interface
+ *
+ * @param component
+ * @return closest parent Container
+ */
+ public static Container getLayout(Widget component) {
+ Widget parent = component.getParent();
+ while (parent != null && !(parent instanceof Container)) {
+ parent = parent.getParent();
+ }
+ if (parent != null) {
+ assert ((Container) parent).hasChildComponent(component);
+
+ return (Container) parent;
+ }
+ return null;
+ }
+
+ /**
+ * Detects if current browser is IE.
+ *
+ * @deprecated use BrowserInfo class instead
+ *
+ * @return true if IE
+ */
+ @Deprecated
+ public static boolean isIE() {
+ return BrowserInfo.get().isIE();
+ }
+
+ /**
+ * Detects if current browser is IE6.
+ *
+ * @deprecated use BrowserInfo class instead
+ *
+ * @return true if IE6
+ */
+ @Deprecated
+ public static boolean isIE6() {
+ return BrowserInfo.get().isIE6();
+ }
+
+ /**
+ * @deprecated use BrowserInfo class instead
+ * @return
+ */
+ @Deprecated
+ public static boolean isIE7() {
+ return BrowserInfo.get().isIE7();
+ }
+
+ /**
+ * @deprecated use BrowserInfo class instead
+ * @return
+ */
+ @Deprecated
+ 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.padding = "0px";
+ el.style.filter = "progid:DXImageTransform.Microsoft.AlphaImageLoader(src='"+src+"', sizingMethod='scale')";
+ },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);
+ }-*/;
+
+ public static int measureHorizontalPaddingAndBorder(Element element,
+ int paddingGuess) {
+ String originalWidth = DOM.getStyleAttribute(element, "width");
+ int originalOffsetWidth = element.getOffsetWidth();
+ int widthGuess = (originalOffsetWidth - paddingGuess);
+ if (widthGuess < 1) {
+ widthGuess = 1;
+ }
+ DOM.setStyleAttribute(element, "width", widthGuess + "px");
+ int padding = element.getOffsetWidth() - widthGuess;
+
+ DOM.setStyleAttribute(element, "width", originalWidth);
+ return padding;
+ }
+
+ public static int measureVerticalPaddingAndBorder(Element element,
+ int paddingGuess) {
+ String originalHeight = DOM.getStyleAttribute(element, "height");
+ int originalOffsetHeight = element.getOffsetHeight();
+ int widthGuess = (originalOffsetHeight - paddingGuess);
+ if (widthGuess < 1) {
+ widthGuess = 1;
+ }
+ DOM.setStyleAttribute(element, "height", widthGuess + "px");
+ int padding = element.getOffsetHeight() - widthGuess;
+
+ DOM.setStyleAttribute(element, "height", originalHeight);
+ return padding;
+ }
+
+ public static int measureHorizontalBorder(Element element) {
+ int borders;
+ if (BrowserInfo.get().isIE()) {
+ String width = element.getStyle().getProperty("width");
+ String height = element.getStyle().getProperty("height");
+
+ int offsetWidth = element.getOffsetWidth();
+ int offsetHeight = element.getOffsetHeight();
+ if (BrowserInfo.get().isIE6()) {
+ if (offsetHeight < 1) {
+ offsetHeight = 1;
+ }
+ if (offsetWidth < 1) {
+ offsetWidth = 10;
+ }
+ element.getStyle().setPropertyPx("height", offsetHeight);
+ }
+ element.getStyle().setPropertyPx("width", offsetWidth);
+
+ borders = element.getOffsetWidth()
+ - element.getPropertyInt("clientWidth");
+
+ element.getStyle().setProperty("width", width);
+ if (BrowserInfo.get().isIE6()) {
+ element.getStyle().setProperty("height", height);
+ }
+ } else {
+ borders = element.getOffsetWidth()
+ - element.getPropertyInt("clientWidth");
+ }
+ assert borders >= 0;
+
+ return borders;
+ }
+
+ public static int measureVerticalBorder(Element element) {
+ int borders;
+ if (BrowserInfo.get().isIE()) {
+ String width = element.getStyle().getProperty("width");
+ String height = element.getStyle().getProperty("height");
+
+ int offsetWidth = element.getOffsetWidth();
+ int offsetHeight = element.getOffsetHeight();
+ // if (BrowserInfo.get().isIE6()) {
+ if (offsetHeight < 1) {
+ offsetHeight = 1;
+ }
+ if (offsetWidth < 1) {
+ offsetWidth = 10;
+ }
+ element.getStyle().setPropertyPx("width", offsetWidth);
+ // }
+
+ element.getStyle().setPropertyPx("height", offsetHeight);
+
+ borders = element.getOffsetHeight()
+ - element.getPropertyInt("clientHeight");
+
+ element.getStyle().setProperty("height", height);
+ // if (BrowserInfo.get().isIE6()) {
+ element.getStyle().setProperty("width", width);
+ // }
+ } else {
+ borders = element.getOffsetHeight()
+ - element.getPropertyInt("clientHeight");
+ }
+ assert borders >= 0;
+
+ return borders;
+ }
+
+ public static int measureMarginLeft(Element element) {
+ return element.getAbsoluteLeft()
+ - element.getParentElement().getAbsoluteLeft();
+ }
+
+ public static int setHeightExcludingPaddingAndBorder(Widget widget,
+ String height, int paddingBorderGuess) {
+ if (height.equals("")) {
+ setHeight(widget, "");
+ return paddingBorderGuess;
+ } else if (height.endsWith("px")) {
+ int pixelHeight = Integer.parseInt(height.substring(0, height
+ .length() - 2));
+ return setHeightExcludingPaddingAndBorder(widget.getElement(),
+ pixelHeight, paddingBorderGuess, false);
+ } else {
+ // Set the height in unknown units
+ setHeight(widget, height);
+ // Use the offsetWidth
+ return setHeightExcludingPaddingAndBorder(widget.getElement(),
+ widget.getOffsetHeight(), paddingBorderGuess, true);
+ }
+ }
+
+ private static void setWidth(Widget widget, String width) {
+ DOM.setStyleAttribute(widget.getElement(), "width", width);
+ }
+
+ private static void setHeight(Widget widget, String height) {
+ DOM.setStyleAttribute(widget.getElement(), "height", height);
+ }
+
+ public static int setWidthExcludingPaddingAndBorder(Widget widget,
+ String width, int paddingBorderGuess) {
+ if (width.equals("")) {
+ setWidth(widget, "");
+ return paddingBorderGuess;
+ } else if (width.endsWith("px")) {
+ int pixelWidth = Integer.parseInt(width.substring(0,
+ width.length() - 2));
+ return setWidthExcludingPaddingAndBorder(widget.getElement(),
+ pixelWidth, paddingBorderGuess, false);
+ } else {
+ setWidth(widget, width);
+ return setWidthExcludingPaddingAndBorder(widget.getElement(),
+ widget.getOffsetWidth(), paddingBorderGuess, true);
+ }
+ }
+
+ public static int setWidthExcludingPaddingAndBorder(Element element,
+ int requestedWidth, int horizontalPaddingBorderGuess,
+ boolean requestedWidthIncludesPaddingBorder) {
+
+ int widthGuess = requestedWidth - horizontalPaddingBorderGuess;
+ if (widthGuess < 0) {
+ widthGuess = 0;
+ }
+
+ DOM.setStyleAttribute(element, "width", widthGuess + "px");
+ int captionOffsetWidth = DOM.getElementPropertyInt(element,
+ "offsetWidth");
+
+ int actualPadding = captionOffsetWidth - widthGuess;
+
+ if (requestedWidthIncludesPaddingBorder) {
+ actualPadding += actualPadding;
+ }
+
+ if (actualPadding != horizontalPaddingBorderGuess) {
+ int w = requestedWidth - actualPadding;
+ if (w < 0) {
+ // Cannot set negative width even if we would want to
+ w = 0;
+ }
+ DOM.setStyleAttribute(element, "width", w + "px");
+
+ }
+
+ return actualPadding;
+
+ }
+
+ public static int setHeightExcludingPaddingAndBorder(Element element,
+ int requestedHeight, int verticalPaddingBorderGuess,
+ boolean requestedHeightIncludesPaddingBorder) {
+
+ int heightGuess = requestedHeight - verticalPaddingBorderGuess;
+ if (heightGuess < 0) {
+ heightGuess = 0;
+ }
+
+ DOM.setStyleAttribute(element, "height", heightGuess + "px");
+ int captionOffsetHeight = DOM.getElementPropertyInt(element,
+ "offsetHeight");
+
+ int actualPadding = captionOffsetHeight - heightGuess;
+
+ if (requestedHeightIncludesPaddingBorder) {
+ actualPadding += actualPadding;
+ }
+
+ if (actualPadding != verticalPaddingBorderGuess) {
+ int h = requestedHeight - actualPadding;
+ if (h < 0) {
+ // Cannot set negative height even if we would want to
+ h = 0;
+ }
+ DOM.setStyleAttribute(element, "height", h + "px");
+
+ }
+
+ return actualPadding;
+
+ }
+
+ public static String getSimpleName(Object widget) {
+ if (widget == null) {
+ return "(null)";
+ }
+
+ String name = widget.getClass().getName();
+ return name.substring(name.lastIndexOf('.') + 1);
+ }
+
+ public static void setFloat(Element element, String value) {
+ if (BrowserInfo.get().isIE()) {
+ DOM.setStyleAttribute(element, "styleFloat", value);
+ } else {
+ DOM.setStyleAttribute(element, "cssFloat", value);
+ }
+ }
+
+ private static int detectedScrollbarSize = -1;
+
+ public static int getNativeScrollbarSize() {
+ if (detectedScrollbarSize < 0) {
+ Element scroller = DOM.createDiv();
+ scroller.getStyle().setProperty("width", "50px");
+ scroller.getStyle().setProperty("height", "50px");
+ scroller.getStyle().setProperty("overflow", "scroll");
+ scroller.getStyle().setProperty("position", "absolute");
+ scroller.getStyle().setProperty("marginLeft", "-5000px");
+ RootPanel.getBodyElement().appendChild(scroller);
+ detectedScrollbarSize = scroller.getOffsetWidth()
+ - scroller.getPropertyInt("clientWidth");
+
+ // Asserting the detected value causes a problem
+ // at least in Hosted Mode Browser/Linux/GWT-1.5.3, so
+ // use a default if detection fails.
+ if (detectedScrollbarSize == 0) {
+ detectedScrollbarSize = 20;
+ }
+
+ RootPanel.getBodyElement().removeChild(scroller);
+
+ }
+ return detectedScrollbarSize;
+ }
+
+ /**
+ * Run workaround for webkits overflow auto issue.
+ *
+ * See: our buh #2138 and https://bugs.webkit.org/show_bug.cgi?id=21462
+ *
+ * @param elem
+ * with overflow auto
+ */
+ public static void runWebkitOverflowAutoFix(final Element elem) {
+ // add max version if fix landes sometime to webkit
+ if (BrowserInfo.get().getWebkitVersion() > 0) {
+ elem.getStyle().setProperty("overflow", "hidden");
+
+ DeferredCommand.addCommand(new Command() {
+ public void execute() {
+ // Dough, safari scoll auto means actually just a moped
+ elem.getStyle().setProperty("overflow", "auto");
+ }
+ });
+ }
+
+ }
+
+ /**
+ * Parses the UIDL parameter and fetches the relative size of the component.
+ * If a dimension is not specified as relative it will return -1. If the
+ * UIDL does not contain width or height specifications this will return
+ * null.
+ *
+ * @param uidl
+ * @return
+ */
+ public static FloatSize parseRelativeSize(UIDL uidl) {
+ boolean hasAttribute = false;
+ String w = "";
+ String h = "";
+ if (uidl.hasAttribute("width")) {
+ hasAttribute = true;
+ w = uidl.getStringAttribute("width");
+ }
+ if (uidl.hasAttribute("height")) {
+ hasAttribute = true;
+ h = uidl.getStringAttribute("height");
+ }
+
+ if (!hasAttribute) {
+ return null;
+ }
+
+ float relativeWidth = Util.parseRelativeSize(w);
+ float relativeHeight = Util.parseRelativeSize(h);
+
+ FloatSize relativeSize = new FloatSize(relativeWidth, relativeHeight);
+ return relativeSize;
+
+ }
+
+ public static boolean isCached(UIDL uidl) {
+ return uidl.getBooleanAttribute("cached");
+ }
+
+ public static void alert(String string) {
+ if (true) {
+ Window.alert(string);
+ }
+ }
+
+ public static boolean equals(Object a, Object b) {
+ if (a == null) {
+ return b == null;
+ }
+
+ return a.equals(b);
+ }
+
+ public static void updateRelativeChildrenAndSendSizeUpdateEvent(
+ ApplicationConnection client, HasWidgets container) {
+ updateRelativeChildrenAndSendSizeUpdateEvent(client, container,
+ (Paintable) container);
+ }
+
+ public static void updateRelativeChildrenAndSendSizeUpdateEvent(
+ ApplicationConnection client, HasWidgets container, Paintable widget) {
+ /*
+ * Relative sized children must be updated first so the component has
+ * the correct outer dimensions when signaling a size change to the
+ * parent.
+ */
+ Iterator<Widget> childIterator = container.iterator();
+ while (childIterator.hasNext()) {
+ Widget w = childIterator.next();
+ client.handleComponentRelativeSize(w);
+ }
+
+ HashSet<Paintable> widgets = new HashSet<Paintable>();
+ widgets.add(widget);
+ Util.componentSizeUpdated(widgets);
+ }
+
+ public static native int getRequiredWidth(
+ com.google.gwt.dom.client.Element element)
+ /*-{
+ var width;
+ if (element.getBoundingClientRect != null) {
+ var rect = element.getBoundingClientRect();
+ width = Math.ceil(rect.right - rect.left);
+ } else {
+ width = element.offsetWidth;
+ }
+ return width;
+ }-*/;
+
+ public static native int getRequiredHeight(
+ com.google.gwt.dom.client.Element element)
+ /*-{
+ var height;
+ if (element.getBoundingClientRect != null) {
+ var rect = element.getBoundingClientRect();
+ height = Math.ceil(rect.bottom - rect.top);
+ } else {
+ height = element.offsetHeight;
+ }
+ return height;
+ }-*/;
+
+ public static int getRequiredWidth(Widget widget) {
+ return getRequiredWidth(widget.getElement());
+ }
+
+ public static int getRequiredHeight(Widget widget) {
+ return getRequiredHeight(widget.getElement());
+ }
+
+ /**
+ * Detects what is currently the overflow style attribute in given element.
+ *
+ * @param pe
+ * the element to detect
+ * @return true if auto or scroll
+ */
+ public static boolean mayHaveScrollBars(com.google.gwt.dom.client.Element pe) {
+ String overflow = getComputedStyle(pe, "overflow");
+ if (overflow != null) {
+ if (overflow.equals("auto") || overflow.equals("scroll")) {
+ return true;
+ } else {
+ return false;
+ }
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * A simple helper method to detect "computed style" (aka style sheets +
+ * element styles). Values returned differ a lot depending on browsers.
+ * Always be very careful when using this.
+ *
+ * @param el
+ * the element from which the style property is detected
+ * @param p
+ * the property to detect
+ * @return String value of style property
+ */
+ private static native String getComputedStyle(
+ com.google.gwt.dom.client.Element el, String p)
+ /*-{
+ try {
+
+ if (el.currentStyle) {
+ // IE
+ return el.currentStyle[p];
+ } else if (window.getComputedStyle) {
+ // Sa, FF, Opera
+ var view = el.ownerDocument.defaultView;
+ return view.getComputedStyle(el,null).getPropertyValue(p);
+ } else {
+ // fall back for non IE, Sa, FF, Opera
+ return "";
+ }
+ } catch (e) {
+ return "";
+ }
+
+ }-*/;
+
+}
diff --git a/src/com/vaadin/terminal/gwt/client/WidgetSet.java b/src/com/vaadin/terminal/gwt/client/WidgetSet.java
new file mode 100644
index 0000000000..df005353c0
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/WidgetSet.java
@@ -0,0 +1,34 @@
+/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.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. The
+ * component must be a {@link Widget} that implements {@link Paintable}.
+ *
+ * @param uidl
+ * UIDL to be painted with returned component.
+ * @return New uninitialized and unregistered component that can paint given
+ * UIDL.
+ */
+ public Paintable 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/vaadin/terminal/gwt/client/ui/Action.java b/src/com/vaadin/terminal/gwt/client/ui/Action.java
new file mode 100644
index 0000000000..4d02f0a259
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/Action.java
@@ -0,0 +1,55 @@
+/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.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/vaadin/terminal/gwt/client/ui/ActionOwner.java b/src/com/vaadin/terminal/gwt/client/ui/ActionOwner.java
new file mode 100644
index 0000000000..42ea146837
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/ActionOwner.java
@@ -0,0 +1,16 @@
+package com.vaadin.terminal.gwt.client.ui;
+
+import com.vaadin.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/vaadin/terminal/gwt/client/ui/AlignmentInfo.java b/src/com/vaadin/terminal/gwt/client/ui/AlignmentInfo.java
new file mode 100644
index 0000000000..28d06c0cc2
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/AlignmentInfo.java
@@ -0,0 +1,89 @@
+/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui;
+
+public final class AlignmentInfo {
+ /** Bitmask values for client server communication */
+ public static class Bits {
+ 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;
+ }
+
+ public static final AlignmentInfo LEFT = new AlignmentInfo(
+ Bits.ALIGNMENT_LEFT);
+ public static final AlignmentInfo RIGHT = new AlignmentInfo(
+ Bits.ALIGNMENT_RIGHT);
+ public static final AlignmentInfo TOP = new AlignmentInfo(
+ Bits.ALIGNMENT_TOP);
+ public static final AlignmentInfo BOTTOM = new AlignmentInfo(
+ Bits.ALIGNMENT_BOTTOM);
+ public static final AlignmentInfo CENTER = new AlignmentInfo(
+ Bits.ALIGNMENT_HORIZONTAL_CENTER);
+ public static final AlignmentInfo MIDDLE = new AlignmentInfo(
+ Bits.ALIGNMENT_VERTICAL_CENTER);
+ public static final AlignmentInfo TOP_LEFT = new AlignmentInfo(
+ Bits.ALIGNMENT_TOP + Bits.ALIGNMENT_LEFT);
+
+ private final int bitMask;
+
+ public AlignmentInfo(int bitMask) {
+ this.bitMask = bitMask;
+ }
+
+ public AlignmentInfo(AlignmentInfo horizontal, AlignmentInfo vertical) {
+ this(horizontal.getBitMask() + vertical.getBitMask());
+ }
+
+ public int getBitMask() {
+ return bitMask;
+ }
+
+ public boolean isTop() {
+ return (bitMask & Bits.ALIGNMENT_TOP) == Bits.ALIGNMENT_TOP;
+ }
+
+ public boolean isBottom() {
+ return (bitMask & Bits.ALIGNMENT_BOTTOM) == Bits.ALIGNMENT_BOTTOM;
+ }
+
+ public boolean isLeft() {
+ return (bitMask & Bits.ALIGNMENT_LEFT) == Bits.ALIGNMENT_LEFT;
+ }
+
+ public boolean isRight() {
+ return (bitMask & Bits.ALIGNMENT_RIGHT) == Bits.ALIGNMENT_RIGHT;
+ }
+
+ public boolean isVerticalCenter() {
+ return (bitMask & Bits.ALIGNMENT_VERTICAL_CENTER) == Bits.ALIGNMENT_VERTICAL_CENTER;
+ }
+
+ public boolean isHorizontalCenter() {
+ return (bitMask & Bits.ALIGNMENT_HORIZONTAL_CENTER) == Bits.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/vaadin/terminal/gwt/client/ui/CalendarEntry.java b/src/com/vaadin/terminal/gwt/client/ui/CalendarEntry.java
new file mode 100644
index 0000000000..cfff056e69
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/CalendarEntry.java
@@ -0,0 +1,126 @@
+/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui;
+
+import java.util.Date;
+
+import com.vaadin.terminal.gwt.client.DateTimeService;
+
+public class CalendarEntry {
+ private final String styleName;
+ private Date start;
+ private Date end;
+ private String title;
+ private String description;
+ private boolean notime;
+
+ public CalendarEntry(String styleName, Date start, Date end, String title,
+ String description, boolean notime) {
+ this.styleName = styleName;
+ if (notime) {
+ Date d = new Date(start.getTime());
+ d.setSeconds(0);
+ d.setMinutes(0);
+ this.start = d;
+ if (end != null) {
+ d = new Date(end.getTime());
+ d.setSeconds(0);
+ d.setMinutes(0);
+ this.end = d;
+ } else {
+ end = start;
+ }
+ } else {
+ this.start = start;
+ this.end = end;
+ }
+ this.title = title;
+ this.description = description;
+ this.notime = notime;
+ }
+
+ public CalendarEntry(String styleName, Date start, Date end, String title,
+ String description) {
+ this(styleName, start, end, title, description, false);
+ }
+
+ public String getStyleName() {
+ return styleName;
+ }
+
+ public Date getStart() {
+ return start;
+ }
+
+ public void setStart(Date start) {
+ this.start = start;
+ }
+
+ public Date getEnd() {
+ return end;
+ }
+
+ public void setEnd(Date end) {
+ this.end = end;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ public boolean isNotime() {
+ return notime;
+ }
+
+ public void setNotime(boolean notime) {
+ this.notime = notime;
+ }
+
+ public String getStringForDate(Date d) {
+ // TODO format from DateTimeService
+ String s = "";
+ if (!notime) {
+ if (!DateTimeService.isSameDay(d, start)) {
+ s += (start.getYear() + 1900) + "." + (start.getMonth() + 1)
+ + "." + start.getDate() + " ";
+ }
+ int i = start.getHours();
+ s += (i < 10 ? "0" : "") + i;
+ s += ":";
+ i = start.getMinutes();
+ s += (i < 10 ? "0" : "") + i;
+ if (!start.equals(end)) {
+ s += " - ";
+ if (!DateTimeService.isSameDay(start, end)) {
+ s += (end.getYear() + 1900) + "." + (end.getMonth() + 1)
+ + "." + end.getDate() + " ";
+ }
+ i = end.getHours();
+ s += (i < 10 ? "0" : "") + i;
+ s += ":";
+ i = end.getMinutes();
+ s += (i < 10 ? "0" : "") + i;
+ }
+ s += " ";
+ }
+ if (title != null) {
+ s += title;
+ }
+ return s;
+ }
+
+} \ No newline at end of file
diff --git a/src/com/vaadin/terminal/gwt/client/ui/Field.java b/src/com/vaadin/terminal/gwt/client/ui/Field.java
new file mode 100644
index 0000000000..66f6497230
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/Field.java
@@ -0,0 +1,13 @@
+/**
+ *
+ */
+package com.vaadin.terminal.gwt.client.ui;
+
+/**
+ * This interface indicates that the component is a Field (serverside), and
+ * wants (for instance) to automatically get the i-modified classname.
+ *
+ */
+public interface Field {
+
+}
diff --git a/src/com/vaadin/terminal/gwt/client/ui/IAbsoluteLayout.java b/src/com/vaadin/terminal/gwt/client/ui/IAbsoluteLayout.java
new file mode 100644
index 0000000000..06134e9515
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/IAbsoluteLayout.java
@@ -0,0 +1,378 @@
+package com.vaadin.terminal.gwt.client.ui;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+import java.util.Map.Entry;
+
+import com.google.gwt.dom.client.DivElement;
+import com.google.gwt.dom.client.Document;
+import com.google.gwt.dom.client.Style;
+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.SimplePanel;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.BrowserInfo;
+import com.vaadin.terminal.gwt.client.Container;
+import com.vaadin.terminal.gwt.client.ICaption;
+import com.vaadin.terminal.gwt.client.Paintable;
+import com.vaadin.terminal.gwt.client.RenderSpace;
+import com.vaadin.terminal.gwt.client.UIDL;
+
+public class IAbsoluteLayout extends ComplexPanel implements Container {
+
+ /** Tag name for widget creation */
+ public static final String TAGNAME = "absolutelayout";
+
+ /** Class name, prefix in styling */
+ public static final String CLASSNAME = "i-absolutelayout";
+
+ private DivElement marginElement;
+
+ protected final Element canvas = DOM.createDiv();
+
+ private int excessPixelsHorizontal;
+
+ private int excessPixelsVertical;
+
+ private Object previousStyleName;
+
+ private Map<String, AbsoluteWrapper> pidToComponentWrappper = new HashMap<String, AbsoluteWrapper>();
+
+ protected ApplicationConnection client;
+
+ private boolean rendering;
+
+ public IAbsoluteLayout() {
+ setElement(Document.get().createDivElement());
+ setStyleName(CLASSNAME);
+ marginElement = Document.get().createDivElement();
+ canvas.getStyle().setProperty("position", "relative");
+ marginElement.appendChild(canvas);
+ getElement().appendChild(marginElement);
+ }
+
+ public RenderSpace getAllocatedSpace(Widget child) {
+ // TODO needs some special handling for components with only on edge
+ // horizontally or vertically defined
+ AbsoluteWrapper wrapper = (AbsoluteWrapper) child.getParent();
+ int w;
+ if (wrapper.left != null && wrapper.right != null) {
+ w = wrapper.getOffsetWidth();
+ } else if (wrapper.right != null) {
+ // left == null
+ // available width == right edge == offsetleft + width
+ w = wrapper.getOffsetWidth() + wrapper.getElement().getOffsetLeft();
+ } else {
+ // left != null && right == null || left == null &&
+ // right == null
+ // available width == canvas width - offset left
+ w = canvas.getOffsetWidth() - wrapper.getElement().getOffsetLeft();
+ }
+ int h;
+ if (wrapper.top != null && wrapper.bottom != null) {
+ h = wrapper.getOffsetHeight();
+ } else if (wrapper.bottom != null) {
+ // top not defined, available space 0... bottom of wrapper
+ h = wrapper.getElement().getOffsetTop() + wrapper.getOffsetHeight();
+ } else {
+ // top defined or both undefined, available space == canvas - top
+ h = canvas.getOffsetHeight() - wrapper.getElement().getOffsetTop();
+ }
+
+ return new RenderSpace(w, h);
+ }
+
+ public boolean hasChildComponent(Widget component) {
+ for (Iterator<Entry<String, AbsoluteWrapper>> iterator = pidToComponentWrappper
+ .entrySet().iterator(); iterator.hasNext();) {
+ if (iterator.next().getValue().paintable == component) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public void replaceChildComponent(Widget oldComponent, Widget newComponent) {
+ for (Widget wrapper : getChildren()) {
+ AbsoluteWrapper w = (AbsoluteWrapper) wrapper;
+ if (w.getWidget() == oldComponent) {
+ w.setWidget(newComponent);
+ return;
+ }
+ }
+ }
+
+ public boolean requestLayout(Set<Paintable> children) {
+ // component inside an absolute panel never affects parent nor the
+ // layout
+ return true;
+ }
+
+ public void updateCaption(Paintable component, UIDL uidl) {
+ AbsoluteWrapper parent2 = (AbsoluteWrapper) ((Widget) component)
+ .getParent();
+ parent2.updateCaption(uidl);
+ }
+
+ public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+ rendering = true;
+ this.client = client;
+ // TODO margin handling
+ if (client.updateComponent(this, uidl, true)) {
+ rendering = false;
+ return;
+ }
+
+ HashSet<String> unrenderedPids = new HashSet<String>(
+ pidToComponentWrappper.keySet());
+
+ for (Iterator<UIDL> childIterator = uidl.getChildIterator(); childIterator
+ .hasNext();) {
+ UIDL cc = childIterator.next();
+ UIDL componentUIDL = cc.getChildUIDL(0);
+ unrenderedPids.remove(componentUIDL.getId());
+ getWrapper(client, componentUIDL).updateFromUIDL(cc);
+ }
+
+ for (String pid : unrenderedPids) {
+ AbsoluteWrapper absoluteWrapper = pidToComponentWrappper.get(pid);
+ pidToComponentWrappper.remove(pid);
+ absoluteWrapper.destroy();
+ }
+ rendering = false;
+ }
+
+ private AbsoluteWrapper getWrapper(ApplicationConnection client,
+ UIDL componentUIDL) {
+ AbsoluteWrapper wrapper = pidToComponentWrappper.get(componentUIDL
+ .getId());
+ if (wrapper == null) {
+ wrapper = new AbsoluteWrapper(client.getPaintable(componentUIDL));
+ pidToComponentWrappper.put(componentUIDL.getId(), wrapper);
+ add(wrapper);
+ }
+ return wrapper;
+
+ }
+
+ @Override
+ public void add(Widget child) {
+ super.add(child, canvas);
+ }
+
+ @Override
+ public void setStyleName(String style) {
+ super.setStyleName(style);
+ if (previousStyleName == null || !previousStyleName.equals(style)) {
+ excessPixelsHorizontal = -1;
+ excessPixelsVertical = -1;
+ }
+ }
+
+ @Override
+ public void setWidth(String width) {
+ super.setWidth(width);
+ // TODO do this so that canvas gets the sized properly (the area
+ // inside marginals)
+ canvas.getStyle().setProperty("width", width);
+
+ if (!rendering) {
+ if (BrowserInfo.get().isIE6()) {
+ relayoutWrappersForIe6();
+ }
+ relayoutRelativeChildren();
+ }
+ }
+
+ private void relayoutRelativeChildren() {
+ for (Widget widget : getChildren()) {
+ if (widget instanceof AbsoluteWrapper) {
+ AbsoluteWrapper w = (AbsoluteWrapper) widget;
+ client.handleComponentRelativeSize(w.getWidget());
+ w.updateCaptionPosition();
+ }
+ }
+ }
+
+ @Override
+ public void setHeight(String height) {
+ super.setHeight(height);
+ // TODO do this so that canvas gets the sized properly (the area
+ // inside marginals)
+ canvas.getStyle().setProperty("height", height);
+
+ if (!rendering) {
+ if (BrowserInfo.get().isIE6()) {
+ relayoutWrappersForIe6();
+ }
+ relayoutRelativeChildren();
+ }
+ }
+
+ private void relayoutWrappersForIe6() {
+ for (Widget wrapper : getChildren()) {
+ if (wrapper instanceof AbsoluteWrapper) {
+ ((AbsoluteWrapper) wrapper).ie6Layout();
+ }
+ }
+ }
+
+ public class AbsoluteWrapper extends SimplePanel {
+ private String css;
+ private String left;
+ private String top;
+ private String right;
+ private String bottom;
+ private String zIndex;
+
+ private Paintable paintable;
+ private ICaption caption;
+
+ public AbsoluteWrapper(Paintable paintable) {
+ this.paintable = paintable;
+ setStyleName(CLASSNAME + "-wrapper");
+ }
+
+ public void updateCaption(UIDL uidl) {
+
+ boolean captionIsNeeded = ICaption.isNeeded(uidl);
+ if (captionIsNeeded) {
+ if (caption == null) {
+ caption = new ICaption(paintable, client);
+ IAbsoluteLayout.this.add(caption);
+ }
+ caption.updateCaption(uidl);
+ updateCaptionPosition();
+ } else {
+ if (caption != null) {
+ caption.removeFromParent();
+ caption = null;
+ }
+ }
+ }
+
+ public void destroy() {
+ if (caption != null) {
+ caption.removeFromParent();
+ }
+ client.unregisterPaintable(paintable);
+ removeFromParent();
+ }
+
+ public void updateFromUIDL(UIDL componentUIDL) {
+ setPosition(componentUIDL.getStringAttribute("css"));
+ if (getWidget() != paintable) {
+ setWidget((Widget) paintable);
+ }
+ UIDL childUIDL = componentUIDL.getChildUIDL(0);
+ paintable.updateFromUIDL(childUIDL, client);
+ if (childUIDL.hasAttribute("cached")) {
+ // child may need relative size adjustment if wrapper details
+ // have changed this could be optimized (check if wrapper size
+ // has changed)
+ client.handleComponentRelativeSize((Widget) paintable);
+ }
+ }
+
+ public void setPosition(String stringAttribute) {
+ if (css == null || !css.equals(stringAttribute)) {
+ css = stringAttribute;
+ top = right = bottom = left = zIndex = null;
+ if (!css.equals("")) {
+ String[] properties = css.split(";");
+ for (int i = 0; i < properties.length; i++) {
+ String[] keyValue = properties[i].split(":");
+ if (keyValue[0].equals("left")) {
+ left = keyValue[1];
+ } else if (keyValue[0].equals("top")) {
+ top = keyValue[1];
+ } else if (keyValue[0].equals("right")) {
+ right = keyValue[1];
+ } else if (keyValue[0].equals("bottom")) {
+ bottom = keyValue[1];
+ } else if (keyValue[0].equals("z-index")) {
+ zIndex = keyValue[1];
+ }
+ }
+ }
+ // ensure ne values
+ Style style = getElement().getStyle();
+ style.setProperty("zIndex", zIndex);
+ style.setProperty("top", top);
+ style.setProperty("left", left);
+ style.setProperty("right", right);
+ style.setProperty("bottom", bottom);
+
+ if (BrowserInfo.get().isIE6()) {
+ ie6Layout();
+ }
+ }
+ updateCaptionPosition();
+ }
+
+ private void updateCaptionPosition() {
+ if (caption != null) {
+ Style style = caption.getElement().getStyle();
+ style.setProperty("position", "absolute");
+ style.setPropertyPx("left", getElement().getOffsetLeft());
+ style.setPropertyPx("top", getElement().getOffsetTop()
+ - caption.getHeight());
+ }
+ }
+
+ private void ie6Layout() {
+ // special handling for IE6 is needed, it does not support
+ // setting both left/right or top/bottom
+ Style style = getElement().getStyle();
+ if (bottom != null && top != null) {
+ // define height for wrapper to simulate bottom property
+ int bottompixels = measureForIE6(bottom);
+ ApplicationConnection.getConsole().log("ALB" + bottompixels);
+ int height = canvas.getOffsetHeight() - bottompixels
+ - getElement().getOffsetTop();
+ ApplicationConnection.getConsole().log("ALB" + height);
+ if (height < 0) {
+ height = 0;
+ }
+ style.setPropertyPx("height", height);
+ } else {
+ // reset possibly existing value
+ style.setProperty("height", "");
+ }
+ if (left != null && right != null) {
+ // define width for wrapper to simulate right property
+ int rightPixels = measureForIE6(right);
+ ApplicationConnection.getConsole().log("ALR" + rightPixels);
+ int width = canvas.getOffsetWidth() - rightPixels
+ - getElement().getOffsetWidth();
+ ApplicationConnection.getConsole().log("ALR" + width);
+ if (width < 0) {
+ width = 0;
+ }
+ style.setPropertyPx("width", width);
+ } else {
+ // reset possibly existing value
+ style.setProperty("width", "");
+ }
+ }
+
+ }
+
+ private Element measureElement;
+
+ private int measureForIE6(String cssLength) {
+ if (measureElement == null) {
+ measureElement = DOM.createDiv();
+ measureElement.getStyle().setProperty("position", "absolute");
+ canvas.appendChild(measureElement);
+ }
+ measureElement.getStyle().setProperty("width", cssLength);
+ return measureElement.getOffsetWidth();
+ }
+
+}
diff --git a/src/com/vaadin/terminal/gwt/client/ui/IAccordion.java b/src/com/vaadin/terminal/gwt/client/ui/IAccordion.java
new file mode 100644
index 0000000000..43c92cab32
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/IAccordion.java
@@ -0,0 +1,647 @@
+package com.vaadin.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.ui.ClickListener;
+import com.google.gwt.user.client.ui.ComplexPanel;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.BrowserInfo;
+import com.vaadin.terminal.gwt.client.ContainerResizedListener;
+import com.vaadin.terminal.gwt.client.ICaption;
+import com.vaadin.terminal.gwt.client.Paintable;
+import com.vaadin.terminal.gwt.client.RenderInformation;
+import com.vaadin.terminal.gwt.client.RenderSpace;
+import com.vaadin.terminal.gwt.client.UIDL;
+import com.vaadin.terminal.gwt.client.Util;
+
+public class IAccordion extends ITabsheetBase implements
+ ContainerResizedListener {
+
+ public static final String CLASSNAME = "i-accordion";
+
+ private Set<Paintable> paintables = new HashSet<Paintable>();
+
+ private String height;
+
+ private String width = "";
+
+ private HashMap<StackItem, UIDL> lazyUpdateMap = new HashMap<StackItem, UIDL>();
+
+ private RenderSpace renderSpace = new RenderSpace(0, 0, true);
+
+ private StackItem openTab = null;
+
+ private boolean rendering = false;
+
+ private int selectedUIDLItemIndex = -1;
+
+ private RenderInformation renderInformation = new RenderInformation();
+
+ public IAccordion() {
+ super(CLASSNAME);
+ // IE6 needs this to calculate offsetHeight correctly
+ if (BrowserInfo.get().isIE6()) {
+ DOM.setStyleAttribute(getElement(), "zoom", "1");
+ }
+ }
+
+ @Override
+ public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+ rendering = true;
+ selectedUIDLItemIndex = -1;
+ super.updateFromUIDL(uidl, client);
+ /*
+ * Render content after all tabs have been created and we know how large
+ * the content area is
+ */
+ if (selectedUIDLItemIndex >= 0) {
+ StackItem selectedItem = getStackItem(selectedUIDLItemIndex);
+ UIDL selectedTabUIDL = lazyUpdateMap.remove(selectedItem);
+ open(selectedUIDLItemIndex);
+
+ selectedItem.setContent(selectedTabUIDL);
+ } else if (!uidl.getBooleanAttribute("cached") && openTab != null) {
+ close(openTab);
+ }
+
+ iLayout();
+ // finally render possible hidden tabs
+ if (lazyUpdateMap.size() > 0) {
+ for (Iterator iterator = lazyUpdateMap.keySet().iterator(); iterator
+ .hasNext();) {
+ StackItem item = (StackItem) iterator.next();
+ item.setContent(lazyUpdateMap.get(item));
+ }
+ lazyUpdateMap.clear();
+ }
+
+ renderInformation.updateSize(getElement());
+
+ rendering = false;
+ }
+
+ @Override
+ protected void renderTab(UIDL tabUidl, int index, boolean selected,
+ boolean hidden) {
+ StackItem item;
+ int itemIndex;
+ if (getWidgetCount() <= index) {
+ // Create stackItem and render caption
+ item = new StackItem(tabUidl);
+ if (getWidgetCount() == 0) {
+ item.addStyleDependentName("first");
+ }
+ itemIndex = getWidgetCount();
+ add(item, getElement());
+ } else {
+ item = getStackItem(index);
+ item = moveStackItemIfNeeded(item, index, tabUidl);
+ itemIndex = index;
+ }
+ item.updateCaption(tabUidl);
+
+ item.setVisible(!hidden);
+
+ if (selected) {
+ selectedUIDLItemIndex = itemIndex;
+ }
+
+ if (tabUidl.getChildCount() > 0) {
+ lazyUpdateMap.put(item, tabUidl.getChildUIDL(0));
+ }
+ }
+
+ /**
+ * This method tries to find out if a tab has been rendered with a different
+ * index previously. If this is the case it re-orders the children so the
+ * same StackItem is used for rendering this time. E.g. if the first tab has
+ * been removed all tabs which contain cached content must be moved 1 step
+ * up to preserve the cached content.
+ *
+ * @param item
+ * @param newIndex
+ * @param tabUidl
+ * @return
+ */
+ private StackItem moveStackItemIfNeeded(StackItem item, int newIndex,
+ UIDL tabUidl) {
+ UIDL tabContentUIDL = null;
+ Paintable tabContent = null;
+ if (tabUidl.getChildCount() > 0) {
+ tabContentUIDL = tabUidl.getChildUIDL(0);
+ tabContent = client.getPaintable(tabContentUIDL);
+ }
+
+ Widget itemWidget = item.getComponent();
+ if (tabContent != null) {
+ if (tabContent != itemWidget) {
+ /*
+ * This is not the same widget as before, find out if it has
+ * been moved
+ */
+ int oldIndex = -1;
+ StackItem oldItem = null;
+ for (int i = 0; i < getWidgetCount(); i++) {
+ Widget w = getWidget(i);
+ oldItem = (StackItem) w;
+ if (tabContent == oldItem.getComponent()) {
+ oldIndex = i;
+ break;
+ }
+ }
+
+ if (oldIndex != -1 && oldIndex > newIndex) {
+ /*
+ * The tab has previously been rendered in another position
+ * so we must move the cached content to correct position.
+ * We move only items with oldIndex > newIndex to prevent
+ * moving items already rendered in this update. If for
+ * instance tabs 1,2,3 are removed and added as 3,2,1 we
+ * cannot re-use "1" when we get to the third tab.
+ */
+ insert(oldItem, getElement(), newIndex, true);
+ return oldItem;
+ }
+ }
+ } else {
+ // Tab which has never been loaded. Must assure we use an empty
+ // StackItem
+ Widget oldWidget = item.getComponent();
+ if (oldWidget != null) {
+ item = new StackItem(tabUidl);
+ insert(item, getElement(), newIndex, true);
+ }
+ }
+ return item;
+ }
+
+ private void open(int itemIndex) {
+ StackItem item = (StackItem) getWidget(itemIndex);
+ boolean alreadyOpen = false;
+ if (openTab != null) {
+ if (openTab.isOpen()) {
+ if (openTab == item) {
+ alreadyOpen = true;
+ } else {
+ openTab.close();
+ }
+ }
+ }
+
+ if (!alreadyOpen) {
+ item.open();
+ activeTabIndex = itemIndex;
+ openTab = item;
+ }
+
+ // Update the size for the open tab
+ updateOpenTabSize();
+ }
+
+ private void close(StackItem item) {
+ if (!item.isOpen()) {
+ return;
+ }
+
+ item.close();
+ activeTabIndex = -1;
+ openTab = null;
+
+ }
+
+ @Override
+ protected void selectTab(final int index, final UIDL contentUidl) {
+ StackItem item = getStackItem(index);
+ if (index != activeTabIndex) {
+ open(index);
+ iLayout();
+ // TODO Check if this is needed
+ client.runDescendentsLayout(this);
+
+ }
+ item.setContent(contentUidl);
+ }
+
+ public void onSelectTab(StackItem item) {
+ final int index = getWidgetIndex(item);
+ if (index != activeTabIndex && !disabled && !readonly
+ && !disabledTabKeys.contains(tabKeys.get(index))) {
+ addStyleDependentName("loading");
+ client
+ .updateVariable(id, "selected", "" + tabKeys.get(index),
+ true);
+ }
+ }
+
+ @Override
+ public void setWidth(String width) {
+ if (this.width.equals(width)) {
+ return;
+ }
+
+ super.setWidth(width);
+ this.width = width;
+ if (!rendering) {
+ updateOpenTabSize();
+
+ if (isDynamicHeight()) {
+ Util.updateRelativeChildrenAndSendSizeUpdateEvent(client,
+ openTab, this);
+ updateOpenTabSize();
+ }
+
+ if (isDynamicHeight()) {
+ openTab.setHeightFromWidget();
+ }
+ iLayout();
+ }
+ }
+
+ @Override
+ public void setHeight(String height) {
+ super.setHeight(height);
+ this.height = height;
+
+ if (!rendering) {
+ updateOpenTabSize();
+ }
+
+ }
+
+ /**
+ * Sets the size of the open tab
+ */
+ private void updateOpenTabSize() {
+ if (openTab == null) {
+ renderSpace.setHeight(0);
+ renderSpace.setWidth(0);
+ return;
+ }
+
+ // WIDTH
+ if (!isDynamicWidth()) {
+ int w = getOffsetWidth();
+ openTab.setWidth(w);
+ renderSpace.setWidth(w);
+ } else {
+ renderSpace.setWidth(0);
+ }
+
+ // HEIGHT
+ if (!isDynamicHeight()) {
+ int usedPixels = 0;
+ for (Widget w : getChildren()) {
+ StackItem item = (StackItem) w;
+ if (item == openTab) {
+ usedPixels += item.getCaptionHeight();
+ } else {
+ // This includes the captionNode borders
+ usedPixels += item.getHeight();
+ }
+ }
+
+ int offsetHeight = getOffsetHeight();
+
+ int spaceForOpenItem = offsetHeight - usedPixels;
+
+ if (spaceForOpenItem < 0) {
+ spaceForOpenItem = 0;
+ }
+
+ renderSpace.setHeight(spaceForOpenItem);
+ openTab.setHeight(spaceForOpenItem);
+ } else {
+ renderSpace.setHeight(0);
+ openTab.setHeightFromWidget();
+
+ }
+
+ }
+
+ public void iLayout() {
+ if (openTab == null) {
+ return;
+ }
+
+ if (isDynamicWidth()) {
+ int maxWidth = 40;
+ for (Widget w : getChildren()) {
+ StackItem si = (StackItem) w;
+ int captionWidth = si.getCaptionWidth();
+ if (captionWidth > maxWidth) {
+ maxWidth = captionWidth;
+ }
+ }
+ int widgetWidth = openTab.getWidgetWidth();
+ if (widgetWidth > maxWidth) {
+ maxWidth = widgetWidth;
+ }
+ super.setWidth(maxWidth + "px");
+ openTab.setWidth(maxWidth);
+ }
+
+ Util.runWebkitOverflowAutoFix(openTab.getContainerElement());
+
+ }
+
+ /**
+ *
+ */
+ protected class StackItem extends ComplexPanel implements ClickListener {
+
+ public void setHeight(int height) {
+ if (height == -1) {
+ super.setHeight("");
+ DOM.setStyleAttribute(content, "height", "0px");
+ } else {
+ super.setHeight((height + getCaptionHeight()) + "px");
+ DOM.setStyleAttribute(content, "height", height + "px");
+ DOM
+ .setStyleAttribute(content, "top", getCaptionHeight()
+ + "px");
+
+ }
+ }
+
+ public Widget getComponent() {
+ if (getWidgetCount() < 2) {
+ return null;
+ }
+ return getWidget(1);
+ }
+
+ @Override
+ public void setVisible(boolean visible) {
+ super.setVisible(visible);
+ }
+
+ public void setHeightFromWidget() {
+ Widget paintable = getPaintable();
+ if (paintable == null) {
+ return;
+ }
+
+ int paintableHeight = (paintable).getElement().getOffsetHeight();
+ setHeight(paintableHeight);
+
+ }
+
+ /**
+ * Returns caption width including padding
+ *
+ * @return
+ */
+ public int getCaptionWidth() {
+ if (caption == null) {
+ return 0;
+ }
+
+ int captionWidth = caption.getRequiredWidth();
+ int padding = Util.measureHorizontalPaddingAndBorder(caption
+ .getElement(), 18);
+ return captionWidth + padding;
+ }
+
+ public void setWidth(int width) {
+ if (width == -1) {
+ super.setWidth("");
+ } else {
+ super.setWidth(width + "px");
+ }
+ }
+
+ public int getHeight() {
+ return getOffsetHeight();
+ }
+
+ public int getCaptionHeight() {
+ return captionNode.getOffsetHeight();
+ }
+
+ private ICaption caption;
+ private boolean open = false;
+ private Element content = DOM.createDiv();
+ private Element captionNode = DOM.createDiv();
+
+ public StackItem(UIDL tabUidl) {
+ setElement(DOM.createDiv());
+ caption = new ICaption(null, client);
+ caption.addClickListener(this);
+ if (BrowserInfo.get().isIE6()) {
+ DOM.setEventListener(captionNode, this);
+ DOM.sinkEvents(captionNode, Event.BUTTON_LEFT);
+ }
+ 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");
+ close();
+ }
+
+ @Override
+ public void onBrowserEvent(Event event) {
+ onSelectTab(this);
+ }
+
+ public Element getContainerElement() {
+ return content;
+ }
+
+ public Widget getPaintable() {
+ if (getWidgetCount() > 1) {
+ return getWidget(1);
+ } else {
+ return null;
+ }
+ }
+
+ public void replacePaintable(Paintable newPntbl) {
+ if (getWidgetCount() > 1) {
+ client.unregisterPaintable((Paintable) getWidget(1));
+ paintables.remove(getWidget(1));
+ remove(1);
+ }
+ add((Widget) newPntbl, content);
+ paintables.add(newPntbl);
+ }
+
+ public void open() {
+ open = true;
+ DOM.setStyleAttribute(content, "top", getCaptionHeight() + "px");
+ DOM.setStyleAttribute(content, "left", "0px");
+ DOM.setStyleAttribute(content, "visibility", "");
+ addStyleDependentName("open");
+ }
+
+ public void hide() {
+ DOM.setStyleAttribute(content, "visibility", "hidden");
+ }
+
+ public void close() {
+ DOM.setStyleAttribute(content, "visibility", "hidden");
+ DOM.setStyleAttribute(content, "top", "-100000px");
+ DOM.setStyleAttribute(content, "left", "-100000px");
+ removeStyleDependentName("open");
+ setHeight(-1);
+ open = false;
+ }
+
+ public boolean isOpen() {
+ return open;
+ }
+
+ public void setContent(UIDL contentUidl) {
+ final Paintable newPntbl = client.getPaintable(contentUidl);
+ if (getPaintable() == null) {
+ add((Widget) newPntbl, content);
+ paintables.add(newPntbl);
+ } else if (getPaintable() != newPntbl) {
+ replacePaintable(newPntbl);
+ }
+ newPntbl.updateFromUIDL(contentUidl, client);
+ if (contentUidl.getBooleanAttribute("cached")) {
+ /*
+ * The size of a cached, relative sized component must be
+ * updated to report correct size.
+ */
+ client.handleComponentRelativeSize((Widget) newPntbl);
+ }
+ if (isOpen() && isDynamicHeight()) {
+ setHeightFromWidget();
+ }
+ }
+
+ public void onClick(Widget sender) {
+ onSelectTab(this);
+ }
+
+ public void updateCaption(UIDL uidl) {
+ caption.updateCaption(uidl);
+ }
+
+ public int getWidgetWidth() {
+ return DOM.getFirstChild(content).getOffsetWidth();
+ }
+
+ public boolean contains(Paintable p) {
+ return (getPaintable() == p);
+ }
+
+ public boolean isCaptionVisible() {
+ return caption.isVisible();
+ }
+
+ }
+
+ @Override
+ protected void clearPaintables() {
+ clear();
+ }
+
+ public boolean isDynamicHeight() {
+ return height == null || height.equals("");
+ }
+
+ public boolean isDynamicWidth() {
+ return width == null || width.equals("");
+ }
+
+ @Override
+ protected Iterator getPaintableIterator() {
+ return paintables.iterator();
+ }
+
+ public boolean hasChildComponent(Widget component) {
+ if (paintables.contains(component)) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ public void replaceChildComponent(Widget oldComponent, Widget newComponent) {
+ for (Widget w : getChildren()) {
+ StackItem item = (StackItem) w;
+ if (item.getPaintable() == oldComponent) {
+ item.replacePaintable((Paintable) newComponent);
+ return;
+ }
+ }
+ }
+
+ public void updateCaption(Paintable component, UIDL uidl) {
+ /* Accordion does not render its children's captions */
+ }
+
+ public boolean requestLayout(Set<Paintable> child) {
+ if (!isDynamicHeight() && !isDynamicWidth()) {
+ /*
+ * If the height and width has been specified for this container the
+ * child components cannot make the size of the layout change
+ */
+
+ return true;
+ }
+
+ updateOpenTabSize();
+
+ if (renderInformation.updateSize(getElement())) {
+ /*
+ * Size has changed so we let the child components know about the
+ * new size.
+ */
+ iLayout();
+ // TODO Check if this is needed
+ client.runDescendentsLayout(this);
+
+ return false;
+ } else {
+ /*
+ * Size has not changed so we do not need to propagate the event
+ * further
+ */
+ return true;
+ }
+
+ }
+
+ public RenderSpace getAllocatedSpace(Widget child) {
+ return renderSpace;
+ }
+
+ @Override
+ protected int getTabCount() {
+ return getWidgetCount();
+ }
+
+ @Override
+ protected void removeTab(int index) {
+ StackItem item = getStackItem(index);
+ remove(item);
+ }
+
+ @Override
+ protected Paintable getTab(int index) {
+ if (index < getWidgetCount()) {
+ return (Paintable) (getStackItem(index)).getPaintable();
+ }
+
+ return null;
+ }
+
+ private StackItem getStackItem(int index) {
+ return (StackItem) getWidget(index);
+ }
+}
diff --git a/src/com/vaadin/terminal/gwt/client/ui/IButton.java b/src/com/vaadin/terminal/gwt/client/ui/IButton.java
new file mode 100644
index 0000000000..4a1dcbad07
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/IButton.java
@@ -0,0 +1,190 @@
+/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.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.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.BrowserInfo;
+import com.vaadin.terminal.gwt.client.ITooltip;
+import com.vaadin.terminal.gwt.client.Paintable;
+import com.vaadin.terminal.gwt.client.UIDL;
+import com.vaadin.terminal.gwt.client.Util;
+
+public class IButton extends Button implements Paintable {
+
+ private String width = null;
+
+ public static final String CLASSNAME = "i-button";
+
+ // Used only for IE, because it doesn't support :active CSS selector
+ private static final String CLASSNAME_DOWN = "i-pressed";
+
+ String id;
+
+ ApplicationConnection client;
+
+ private Element errorIndicatorElement;
+
+ private final Element captionElement = DOM.createSpan();
+
+ private Icon icon;
+
+ /**
+ * Helper flat to handle special-case where the button is moved from under
+ * mouse while clicking it. In this case mouse leaves the button without
+ * moving.
+ */
+ private boolean clickPending;
+
+ public IButton() {
+ setStyleName(CLASSNAME);
+
+ DOM.appendChild(getElement(), captionElement);
+
+ addClickListener(new ClickListener() {
+ public void onClick(Widget sender) {
+ if (id == null || client == null) {
+ return;
+ }
+ /*
+ * TODO isolate 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);
+ clickPending = false;
+ }
+ });
+ sinkEvents(ITooltip.TOOLTIP_EVENTS);
+ sinkEvents(Event.ONMOUSEDOWN);
+ sinkEvents(Event.ONMOUSEUP);
+ }
+
+ 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;
+ }
+ }
+ if (BrowserInfo.get().isIE7()) {
+ if (width.equals("")) {
+ setWidth(getOffsetWidth() + "px");
+ }
+ }
+ }
+
+ @Override
+ public void setText(String text) {
+ DOM.setInnerText(captionElement, text);
+ }
+
+ @Override
+ public void onBrowserEvent(Event event) {
+ super.onBrowserEvent(event);
+
+ if (DOM.eventGetType(event) == Event.ONLOAD) {
+ Util.notifyParentOfSizeChange(this, true);
+
+ } else if (DOM.eventGetType(event) == Event.ONMOUSEDOWN
+ && event.getButton() == Event.BUTTON_LEFT) {
+ clickPending = true;
+ if (BrowserInfo.get().isIE()) {
+ // Only for IE, because it doesn't support :active CSS selector
+ // Simple check is cheaper than DOM manipulation
+ addStyleName(CLASSNAME_DOWN);
+ }
+ } else if (DOM.eventGetType(event) == Event.ONMOUSEMOVE) {
+ clickPending = false;
+ } else if (DOM.eventGetType(event) == Event.ONMOUSEOUT) {
+ if (clickPending) {
+ click();
+ }
+ if (BrowserInfo.get().isIE()) {
+ removeStyleName(CLASSNAME_DOWN);
+ }
+ clickPending = false;
+ } else if (DOM.eventGetType(event) == Event.ONMOUSEUP) {
+ if (BrowserInfo.get().isIE()) {
+ removeStyleName(CLASSNAME_DOWN);
+ }
+ }
+
+ if (client != null) {
+ client.handleTooltipEvent(event, this);
+ }
+ }
+
+ @Override
+ public void setWidth(String width) {
+ /* Workaround for IE7 button size part 1 (#2014) */
+ if (BrowserInfo.get().isIE7() && this.width != null) {
+ if (this.width.equals(width)) {
+ return;
+ }
+
+ if (width == null) {
+ width = "";
+ }
+ }
+
+ this.width = width;
+ super.setWidth(width);
+
+ /* Workaround for IE7 button size part 2 (#2014) */
+ if (BrowserInfo.get().isIE7()) {
+ super.setWidth(width);
+ }
+ }
+
+}
diff --git a/src/com/vaadin/terminal/gwt/client/ui/ICalendarPanel.java b/src/com/vaadin/terminal/gwt/client/ui/ICalendarPanel.java
new file mode 100644
index 0000000000..31566a75b9
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/ICalendarPanel.java
@@ -0,0 +1,520 @@
+/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui;
+
+import java.util.Date;
+import java.util.Iterator;
+import java.util.List;
+
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.Event;
+import com.google.gwt.user.client.Timer;
+import com.google.gwt.user.client.ui.FlexTable;
+import com.google.gwt.user.client.ui.MouseListener;
+import com.google.gwt.user.client.ui.MouseListenerCollection;
+import com.google.gwt.user.client.ui.SourcesMouseEvents;
+import com.google.gwt.user.client.ui.SourcesTableEvents;
+import com.google.gwt.user.client.ui.TableListener;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.terminal.gwt.client.DateTimeService;
+import com.vaadin.terminal.gwt.client.LocaleService;
+
+public class ICalendarPanel extends FlexTable implements MouseListener {
+
+ private final IDateField datefield;
+
+ private IEventButton prevYear;
+
+ private IEventButton nextYear;
+
+ private IEventButton prevMonth;
+
+ private IEventButton nextMonth;
+
+ private ITime time;
+
+ private Date minDate = null;
+
+ private Date maxDate = null;
+
+ private CalendarEntrySource entrySource;
+
+ /* Needed to identify resolution changes */
+ private int resolution = IDateField.RESOLUTION_YEAR;
+
+ /* Needed to identify locale changes */
+ private String locale = LocaleService.getDefaultLocale();
+
+ public ICalendarPanel(IDateField parent) {
+ datefield = parent;
+ setStyleName(IDateField.CLASSNAME + "-calendarpanel");
+ // buildCalendar(true);
+ addTableListener(new DateClickListener(this));
+ }
+
+ public ICalendarPanel(IDateField parent, Date min, Date max) {
+ datefield = parent;
+ setStyleName(IDateField.CLASSNAME + "-calendarpanel");
+ // buildCalendar(true);
+ addTableListener(new DateClickListener(this));
+
+ }
+
+ private void buildCalendar(boolean forceRedraw) {
+ final boolean needsMonth = datefield.getCurrentResolution() > IDateField.RESOLUTION_YEAR;
+ boolean needsBody = datefield.getCurrentResolution() >= IDateField.RESOLUTION_DAY;
+ final boolean needsTime = datefield.getCurrentResolution() >= IDateField.RESOLUTION_HOUR;
+ forceRedraw = prevYear == null ? true : forceRedraw;
+ buildCalendarHeader(forceRedraw, needsMonth);
+ clearCalendarBody(!needsBody);
+ if (needsBody) {
+ buildCalendarBody();
+ }
+ if (needsTime) {
+ buildTime(forceRedraw);
+ } else if (time != null) {
+ remove(time);
+ time = null;
+ }
+ }
+
+ private void clearCalendarBody(boolean remove) {
+ if (!remove) {
+ for (int row = 2; row < 8; row++) {
+ for (int col = 0; col < 7; col++) {
+ setHTML(row, col, "&nbsp;");
+ }
+ }
+ } else if (getRowCount() > 2) {
+ while (getRowCount() > 2) {
+ removeRow(2);
+ }
+ }
+ }
+
+ private void buildCalendarHeader(boolean forceRedraw, boolean needsMonth) {
+ if (forceRedraw) {
+ if (prevMonth == null) { // Only do once
+ prevYear = new IEventButton();
+ prevYear.setHTML("&laquo;");
+ prevYear.setStyleName("i-button-prevyear");
+ nextYear = new IEventButton();
+ nextYear.setHTML("&raquo;");
+ nextYear.setStyleName("i-button-nextyear");
+ prevYear.addMouseListener(this);
+ nextYear.addMouseListener(this);
+ setWidget(0, 0, prevYear);
+ setWidget(0, 4, nextYear);
+
+ if (needsMonth) {
+ prevMonth = new IEventButton();
+ prevMonth.setHTML("&lsaquo;");
+ prevMonth.setStyleName("i-button-prevmonth");
+ nextMonth = new IEventButton();
+ nextMonth.setHTML("&rsaquo;");
+ nextMonth.setStyleName("i-button-nextmonth");
+ prevMonth.addMouseListener(this);
+ nextMonth.addMouseListener(this);
+ setWidget(0, 3, nextMonth);
+ setWidget(0, 1, prevMonth);
+ }
+
+ getFlexCellFormatter().setColSpan(0, 2, 3);
+ getRowFormatter().addStyleName(0,
+ IDateField.CLASSNAME + "-calendarpanel-header");
+ } else if (!needsMonth) {
+ // Remove month traverse buttons
+ prevMonth.removeMouseListener(this);
+ nextMonth.removeMouseListener(this);
+ remove(prevMonth);
+ remove(nextMonth);
+ prevMonth = null;
+ nextMonth = null;
+ }
+
+ // Print weekday names
+ final int firstDay = datefield.getDateTimeService()
+ .getFirstDayOfWeek();
+ for (int i = 0; i < 7; i++) {
+ int day = i + firstDay;
+ if (day > 6) {
+ day = 0;
+ }
+ if (datefield.getCurrentResolution() > IDateField.RESOLUTION_MONTH) {
+ setHTML(1, i, "<strong>"
+ + datefield.getDateTimeService().getShortDay(day)
+ + "</strong>");
+ } else {
+ setHTML(1, i, "");
+ }
+ }
+ }
+
+ final String monthName = needsMonth ? datefield.getDateTimeService()
+ .getMonth(datefield.getShowingDate().getMonth()) : "";
+ final int year = datefield.getShowingDate().getYear() + 1900;
+ setHTML(0, 2, "<span class=\"" + IDateField.CLASSNAME
+ + "-calendarpanel-month\">" + monthName + " " + year
+ + "</span>");
+ }
+
+ private void buildCalendarBody() {
+ // date actually selected?
+ Date currentDate = datefield.getCurrentDate();
+ Date showing = datefield.getShowingDate();
+ boolean selected = (currentDate != null
+ && currentDate.getMonth() == showing.getMonth() && currentDate
+ .getYear() == showing.getYear());
+
+ final int startWeekDay = datefield.getDateTimeService()
+ .getStartWeekDay(datefield.getShowingDate());
+ final int numDays = DateTimeService.getNumberOfDaysInMonth(datefield
+ .getShowingDate());
+ int dayCount = 0;
+ final Date today = new Date();
+ final Date curr = new Date(datefield.getShowingDate().getTime());
+ for (int row = 2; row < 8; row++) {
+ for (int col = 0; col < 7; col++) {
+ if (!(row == 2 && col < startWeekDay)) {
+ if (dayCount < numDays) {
+ final int selectedDate = ++dayCount;
+ String title = "";
+ if (entrySource != null) {
+ curr.setDate(dayCount);
+ final List entries = entrySource.getEntries(curr,
+ IDateField.RESOLUTION_DAY);
+ if (entries != null) {
+ for (final Iterator it = entries.iterator(); it
+ .hasNext();) {
+ final CalendarEntry entry = (CalendarEntry) it
+ .next();
+ title += (title.length() > 0 ? ", " : "")
+ + entry.getStringForDate(curr);
+ }
+ }
+ }
+ final String baseclass = IDateField.CLASSNAME
+ + "-calendarpanel-day";
+ String cssClass = baseclass;
+ if (!isEnabledDate(curr)) {
+ cssClass += " " + baseclass + "-disabled";
+ }
+ if (selected
+ && datefield.getShowingDate().getDate() == dayCount) {
+ cssClass += " " + baseclass + "-selected";
+ }
+ if (today.getDate() == dayCount
+ && today.getMonth() == datefield
+ .getShowingDate().getMonth()
+ && today.getYear() == datefield
+ .getShowingDate().getYear()) {
+ cssClass += " " + baseclass + "-today";
+ }
+ if (title.length() > 0) {
+ cssClass += " " + baseclass + "-entry";
+ }
+ setHTML(row, col, "<span title=\"" + title
+ + "\" class=\"" + cssClass + "\">"
+ + selectedDate + "</span>");
+ } else {
+ break;
+ }
+
+ }
+ }
+ }
+ }
+
+ private void buildTime(boolean forceRedraw) {
+ if (time == null) {
+ time = new ITime(datefield);
+ setText(8, 0, ""); // Add new row
+ getFlexCellFormatter().setColSpan(8, 0, 7);
+ setWidget(8, 0, time);
+ }
+ time.updateTime(forceRedraw);
+ }
+
+ /**
+ *
+ * @param forceRedraw
+ * Build all from scratch, in case of e.g. locale changes
+ */
+ public void updateCalendar() {
+ // Locale and resolution changes force a complete redraw
+ buildCalendar(locale != datefield.getCurrentLocale()
+ || resolution != datefield.getCurrentResolution());
+ if (datefield instanceof ITextualDate) {
+ ((ITextualDate) datefield).buildDate();
+ }
+ locale = datefield.getCurrentLocale();
+ resolution = datefield.getCurrentResolution();
+ }
+
+ private boolean isEnabledDate(Date date) {
+ if ((minDate != null && date.before(minDate))
+ || (maxDate != null && date.after(maxDate))) {
+ return false;
+ }
+ return true;
+ }
+
+ private void processClickEvent(Widget sender, boolean updateVariable) {
+ if (!datefield.isEnabled() || datefield.isReadonly()) {
+ return;
+ }
+ Date showingDate = datefield.getShowingDate();
+ if (!updateVariable) {
+ if (sender == prevYear) {
+ showingDate.setYear(showingDate.getYear() - 1);
+ updateCalendar();
+ } else if (sender == nextYear) {
+ showingDate.setYear(showingDate.getYear() + 1);
+ updateCalendar();
+ } else if (sender == prevMonth) {
+ int currentMonth = showingDate.getMonth();
+ showingDate.setMonth(currentMonth - 1);
+
+ /*
+ * If the selected date was e.g. 31.12 the new date would be
+ * 31.11 but this date is invalid so the new date will be 1.12.
+ * This is taken care of by decreasing the date until we have
+ * the correct month.
+ */
+ while (showingDate.getMonth() == currentMonth) {
+ showingDate.setDate(showingDate.getDate() - 1);
+ }
+
+ updateCalendar();
+ } else if (sender == nextMonth) {
+ int currentMonth = showingDate.getMonth();
+ showingDate.setMonth(currentMonth + 1);
+ int requestedMonth = (currentMonth + 1) % 12;
+
+ /*
+ * If the selected date was e.g. 31.3 the new date would be 31.4
+ * but this date is invalid so the new date will be 1.5. This is
+ * taken care of by decreasing the date until we have the
+ * correct month.
+ */
+ while (showingDate.getMonth() != requestedMonth) {
+ showingDate.setDate(showingDate.getDate() - 1);
+ }
+
+ updateCalendar();
+ }
+ } else {
+ if (datefield.getCurrentResolution() == IDateField.RESOLUTION_YEAR
+ || datefield.getCurrentResolution() == IDateField.RESOLUTION_MONTH) {
+ // Due to current UI, update variable if res=year/month
+ datefield.setCurrentDate(new Date(showingDate.getTime()));
+ if (datefield.getCurrentResolution() == IDateField.RESOLUTION_MONTH) {
+ datefield.getClient().updateVariable(datefield.getId(),
+ "month", datefield.getCurrentDate().getMonth() + 1,
+ false);
+ }
+ datefield.getClient().updateVariable(datefield.getId(), "year",
+ datefield.getCurrentDate().getYear() + 1900,
+ datefield.isImmediate());
+
+ /* Must update the value in the textfield also */
+ updateCalendar();
+ }
+ }
+ }
+
+ private Timer timer;
+
+ public void onMouseDown(final Widget sender, int x, int y) {
+ // Allow user to click-n-hold for fast-forward or fast-rewind.
+ // Timer is first used for a 500ms delay after mousedown. After that has
+ // elapsed, another timer is triggered to go off every 150ms. Both
+ // timers are cancelled on mouseup or mouseout.
+ if (sender instanceof IEventButton) {
+ processClickEvent(sender, false);
+ timer = new Timer() {
+ @Override
+ public void run() {
+ timer = new Timer() {
+ @Override
+ public void run() {
+ processClickEvent(sender, false);
+ }
+ };
+ timer.scheduleRepeating(150);
+ }
+ };
+ timer.schedule(500);
+ }
+ }
+
+ public void onMouseEnter(Widget sender) {
+ }
+
+ public void onMouseLeave(Widget sender) {
+ if (timer != null) {
+ timer.cancel();
+ }
+ }
+
+ public void onMouseMove(Widget sender, int x, int y) {
+ }
+
+ public void onMouseUp(Widget sender, int x, int y) {
+ if (timer != null) {
+ timer.cancel();
+ }
+ processClickEvent(sender, true);
+ }
+
+ private class IEventButton extends IButton implements SourcesMouseEvents {
+
+ private MouseListenerCollection mouseListeners;
+
+ public IEventButton() {
+ super();
+ sinkEvents(Event.FOCUSEVENTS | Event.KEYEVENTS | Event.ONCLICK
+ | Event.MOUSEEVENTS);
+ }
+
+ public void addMouseListener(MouseListener listener) {
+ if (mouseListeners == null) {
+ mouseListeners = new MouseListenerCollection();
+ }
+ mouseListeners.add(listener);
+ }
+
+ public void removeMouseListener(MouseListener listener) {
+ if (mouseListeners != null) {
+ mouseListeners.remove(listener);
+ }
+ }
+
+ @Override
+ public void onBrowserEvent(Event event) {
+ super.onBrowserEvent(event);
+ switch (DOM.eventGetType(event)) {
+ case Event.ONMOUSEDOWN:
+ case Event.ONMOUSEUP:
+ case Event.ONMOUSEMOVE:
+ case Event.ONMOUSEOVER:
+ case Event.ONMOUSEOUT:
+ if (mouseListeners != null) {
+ mouseListeners.fireMouseEvent(this, event);
+ }
+ break;
+ }
+ }
+ }
+
+ private class DateClickListener implements TableListener {
+
+ private final ICalendarPanel cal;
+
+ public DateClickListener(ICalendarPanel panel) {
+ cal = panel;
+ }
+
+ public void onCellClicked(SourcesTableEvents sender, int row, int col) {
+ if (sender != cal || row < 2 || row > 7
+ || !cal.datefield.isEnabled() || cal.datefield.isReadonly()) {
+ return;
+ }
+
+ final String text = cal.getText(row, col);
+ if (text.equals(" ")) {
+ return;
+ }
+
+ try {
+ final Integer day = new Integer(text);
+ final Date newDate = cal.datefield.getShowingDate();
+ newDate.setDate(day.intValue());
+ if (!isEnabledDate(newDate)) {
+ return;
+ }
+ if (cal.datefield.getCurrentDate() == null) {
+ cal.datefield.setCurrentDate(new Date(newDate.getTime()));
+
+ // Init variables with current time
+ datefield.getClient().updateVariable(cal.datefield.getId(),
+ "hour", newDate.getHours(), false);
+ datefield.getClient().updateVariable(cal.datefield.getId(),
+ "min", newDate.getMinutes(), false);
+ datefield.getClient().updateVariable(cal.datefield.getId(),
+ "sec", newDate.getSeconds(), false);
+ datefield.getClient().updateVariable(cal.datefield.getId(),
+ "msec", datefield.getMilliseconds(), false);
+ }
+
+ cal.datefield.getCurrentDate().setTime(newDate.getTime());
+ cal.datefield.getClient().updateVariable(cal.datefield.getId(),
+ "day", cal.datefield.getCurrentDate().getDate(), false);
+ cal.datefield.getClient().updateVariable(cal.datefield.getId(),
+ "month", cal.datefield.getCurrentDate().getMonth() + 1,
+ false);
+ cal.datefield.getClient().updateVariable(cal.datefield.getId(),
+ "year",
+ cal.datefield.getCurrentDate().getYear() + 1900,
+ cal.datefield.isImmediate());
+
+ if (datefield instanceof ITextualDate
+ && resolution < IDateField.RESOLUTION_HOUR) {
+ ((IToolkitOverlay) getParent()).hide();
+ } else {
+ updateCalendar();
+ }
+
+ } catch (final NumberFormatException e) {
+ // Not a number, ignore and stop here
+ return;
+ }
+ }
+
+ }
+
+ public void setLimits(Date min, Date max) {
+ if (min != null) {
+ final Date d = new Date(min.getTime());
+ d.setHours(0);
+ d.setMinutes(0);
+ d.setSeconds(1);
+ minDate = d;
+ } else {
+ minDate = null;
+ }
+ if (max != null) {
+ final Date d = new Date(max.getTime());
+ d.setHours(24);
+ d.setMinutes(59);
+ d.setSeconds(59);
+ maxDate = d;
+ } else {
+ maxDate = null;
+ }
+ }
+
+ public void setCalendarEntrySource(CalendarEntrySource entrySource) {
+ this.entrySource = entrySource;
+ }
+
+ public CalendarEntrySource getCalendarEntrySource() {
+ return entrySource;
+ }
+
+ public interface CalendarEntrySource {
+ public List getEntries(Date date, int resolution);
+ }
+
+ /**
+ * Sets focus to Calendar panel.
+ *
+ * @param focus
+ */
+ public void setFocus(boolean focus) {
+ nextYear.setFocus(focus);
+ }
+
+}
diff --git a/src/com/vaadin/terminal/gwt/client/ui/ICheckBox.java b/src/com/vaadin/terminal/gwt/client/ui/ICheckBox.java
new file mode 100644
index 0000000000..397dc89828
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/ICheckBox.java
@@ -0,0 +1,140 @@
+/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.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.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.ITooltip;
+import com.vaadin.terminal.gwt.client.Paintable;
+import com.vaadin.terminal.gwt.client.UIDL;
+import com.vaadin.terminal.gwt.client.Util;
+
+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(ITooltip.TOOLTIP_EVENTS);
+ Element el = DOM.getFirstChild(getElement());
+ while (el != null) {
+ DOM.sinkEvents(el,
+ (DOM.getEventsSunk(el) | ITooltip.TOOLTIP_EVENTS));
+ el = DOM.getNextSibling(el);
+ }
+ }
+
+ 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();
+ errorIndicatorElement.setInnerHTML("&nbsp;");
+ DOM.setElementProperty(errorIndicatorElement, "className",
+ "i-errorindicator");
+ DOM.appendChild(getElement(), errorIndicatorElement);
+ DOM.sinkEvents(errorIndicatorElement, ITooltip.TOOLTIP_EVENTS
+ | Event.ONCLICK);
+ }
+ } 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.sinkEvents(ITooltip.TOOLTIP_EVENTS);
+ icon.sinkEvents(Event.ONCLICK);
+ }
+ 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");
+ }
+
+ @Override
+ public void onBrowserEvent(Event event) {
+ if (icon != null && (event.getTypeInt() == Event.ONCLICK)
+ && (event.getTarget() == icon.getElement())) {
+ setChecked(!isChecked());
+ }
+ super.onBrowserEvent(event);
+ if (event.getTypeInt() == Event.ONLOAD) {
+ Util.notifyParentOfSizeChange(this, true);
+ }
+ if (client != null) {
+ client.handleTooltipEvent(event, this);
+ }
+ }
+
+ @Override
+ public void setWidth(String width) {
+ setBlockMode();
+ super.setWidth(width);
+ }
+
+ @Override
+ 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/vaadin/terminal/gwt/client/ui/IContextMenu.java b/src/com/vaadin/terminal/gwt/client/ui/IContextMenu.java
new file mode 100644
index 0000000000..72a87ea188
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/IContextMenu.java
@@ -0,0 +1,158 @@
+/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui;
+
+import com.google.gwt.dom.client.NodeList;
+import com.google.gwt.dom.client.TableRowElement;
+import com.google.gwt.dom.client.TableSectionElement;
+import com.google.gwt.user.client.Element;
+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;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+
+public class IContextMenu extends IToolkitOverlay implements SubPartAware {
+
+ 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 IContextMenu() {
+ 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 = IContextMenu.this.left;
+ int top = IContextMenu.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);
+ }
+
+ @Override
+ public void onPopupClosed(PopupPanel sender, boolean autoClosed) {
+ super.onPopupClosed(sender, autoClosed);
+ 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); }
+ */
+ }
+
+ public Element getSubPartElement(String subPart) {
+ int index = Integer.parseInt(subPart.substring(6));
+ ApplicationConnection.getConsole().log(
+ "Searching element for selection index " + index);
+ Element wrapperdiv = menu.getElement();
+ com.google.gwt.dom.client.TableSectionElement tBody = (TableSectionElement) wrapperdiv
+ .getFirstChildElement().getFirstChildElement();
+ TableRowElement item = tBody.getRows().getItem(index);
+ com.google.gwt.dom.client.Element clickableDivElement = item
+ .getFirstChildElement().getFirstChildElement();
+ return clickableDivElement.cast();
+ }
+
+ public String getSubPartName(Element subElement) {
+ if (getElement().isOrHasChild(subElement)) {
+ com.google.gwt.dom.client.Element e = subElement;
+ {
+ while (e != null && !e.getTagName().toLowerCase().equals("tr")) {
+ e = e.getParentElement();
+ ApplicationConnection.getConsole().log("Found row");
+ }
+ }
+ com.google.gwt.dom.client.TableSectionElement parentElement = (TableSectionElement) e
+ .getParentElement();
+ NodeList<TableRowElement> rows = parentElement.getRows();
+ for (int i = 0; i < rows.getLength(); i++) {
+ if (rows.getItem(i) == e) {
+ ApplicationConnection.getConsole().log(
+ "Found index for row" + 1);
+ return "option" + i;
+ }
+ }
+ return null;
+ } else {
+ return null;
+ }
+ }
+}
diff --git a/src/com/vaadin/terminal/gwt/client/ui/ICustomComponent.java b/src/com/vaadin/terminal/gwt/client/ui/ICustomComponent.java
new file mode 100644
index 0000000000..1e92111efa
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/ICustomComponent.java
@@ -0,0 +1,153 @@
+/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui;
+
+import java.util.Set;
+
+import com.google.gwt.user.client.Command;
+import com.google.gwt.user.client.DeferredCommand;
+import com.google.gwt.user.client.ui.SimplePanel;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.Container;
+import com.vaadin.terminal.gwt.client.Paintable;
+import com.vaadin.terminal.gwt.client.RenderSpace;
+import com.vaadin.terminal.gwt.client.UIDL;
+import com.vaadin.terminal.gwt.client.Util;
+
+public class ICustomComponent extends SimplePanel implements Container {
+
+ private static final String CLASSNAME = "i-customcomponent";
+ private String height;
+ private ApplicationConnection client;
+ private boolean rendering;
+ private String width;
+ private RenderSpace renderSpace = new RenderSpace();
+
+ public ICustomComponent() {
+ super();
+ setStyleName(CLASSNAME);
+ }
+
+ public void updateFromUIDL(UIDL uidl, final ApplicationConnection client) {
+ rendering = true;
+ if (client.updateComponent(this, uidl, true)) {
+ rendering = false;
+ return;
+ }
+ this.client = client;
+
+ 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);
+ }
+
+ boolean updateDynamicSize = updateDynamicSize();
+ if (updateDynamicSize) {
+ DeferredCommand.addCommand(new Command() {
+ public void execute() {
+ // FIXME deferred relative size update needed to fix some
+ // scrollbar issues in sampler. This must be the wrong way
+ // to do it. Might be that some other component is broken.
+ client.handleComponentRelativeSize(ICustomComponent.this);
+
+ }
+ });
+ }
+
+ renderSpace.setWidth(getElement().getOffsetWidth());
+ renderSpace.setHeight(getElement().getOffsetHeight());
+
+ rendering = false;
+ }
+
+ private boolean updateDynamicSize() {
+ boolean updated = false;
+ if (isDynamicWidth()) {
+ int childWidth = Util.getRequiredWidth(getWidget());
+ getElement().getStyle().setPropertyPx("width", childWidth);
+ updated = true;
+ }
+ if (isDynamicHeight()) {
+ int childHeight = Util.getRequiredHeight(getWidget());
+ getElement().getStyle().setPropertyPx("height", childHeight);
+ updated = true;
+ }
+
+ return updated;
+ }
+
+ private boolean isDynamicWidth() {
+ return width == null || width.equals("");
+ }
+
+ private boolean isDynamicHeight() {
+ return height == null || height.equals("");
+ }
+
+ 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) {
+ // NOP, custom component dont render composition roots caption
+ }
+
+ public boolean requestLayout(Set<Paintable> child) {
+ return !updateDynamicSize();
+ }
+
+ public RenderSpace getAllocatedSpace(Widget child) {
+ return renderSpace;
+ }
+
+ @Override
+ public void setHeight(String height) {
+ super.setHeight(height);
+ renderSpace.setHeight(getElement().getOffsetHeight());
+
+ if (!height.equals(this.height)) {
+ this.height = height;
+ if (!rendering) {
+ client.runDescendentsLayout(this);
+ }
+ }
+ }
+
+ @Override
+ public void setWidth(String width) {
+ super.setWidth(width);
+ renderSpace.setWidth(getElement().getOffsetWidth());
+
+ if (!width.equals(this.width)) {
+ this.width = width;
+ if (!rendering) {
+ client.runDescendentsLayout(this);
+ }
+ }
+ }
+
+}
diff --git a/src/com/vaadin/terminal/gwt/client/ui/ICustomLayout.java b/src/com/vaadin/terminal/gwt/client/ui/ICustomLayout.java
new file mode 100644
index 0000000000..ac7827f22c
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/ICustomLayout.java
@@ -0,0 +1,644 @@
+/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Set;
+
+import com.google.gwt.dom.client.ImageElement;
+import com.google.gwt.dom.client.NodeList;
+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.ComplexPanel;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.BrowserInfo;
+import com.vaadin.terminal.gwt.client.Container;
+import com.vaadin.terminal.gwt.client.ContainerResizedListener;
+import com.vaadin.terminal.gwt.client.ICaption;
+import com.vaadin.terminal.gwt.client.ICaptionWrapper;
+import com.vaadin.terminal.gwt.client.Paintable;
+import com.vaadin.terminal.gwt.client.RenderSpace;
+import com.vaadin.terminal.gwt.client.UIDL;
+import com.vaadin.terminal.gwt.client.Util;
+import com.vaadin.terminal.gwt.client.RenderInformation.FloatSize;
+
+/**
+ * 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<String, Widget> locationToWidget = new HashMap<String, Widget>();
+
+ /** Widget to captionwrapper map */
+ private final HashMap widgetToCaptionWrapper = new HashMap();
+
+ /** Name of the currently rendered style */
+ String currentTemplateName;
+
+ /** Unexecuted scripts loaded from the template */
+ private String scripts = "";
+
+ /** Paintable ID of this paintable */
+ private String pid;
+
+ private ApplicationConnection client;
+
+ /** Has the template been loaded from contents passed in UIDL **/
+ private boolean hasTemplateContents = false;
+
+ private Element elementWithNativeResizeFunction;
+
+ private String height = "";
+
+ private String width = "";
+
+ private HashMap<String, FloatSize> locationToExtraSize = new HashMap<String, FloatSize>();
+
+ public ICustomLayout() {
+ setElement(DOM.createDiv());
+ // Clear any unwanted styling
+ DOM.setStyleAttribute(getElement(), "border", "none");
+ DOM.setStyleAttribute(getElement(), "margin", "0");
+ DOM.setStyleAttribute(getElement(), "padding", "0");
+
+ if (BrowserInfo.get().isIE()) {
+ DOM.setStyleAttribute(getElement(), "position", "relative");
+ }
+
+ 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 = locationToWidget.get(location);
+ // NOP if given widget already exists in this location
+ if (previous == widget) {
+ return;
+ }
+
+ if (previous != null) {
+ 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;
+ // ApplicationConnection manages generic component features
+ if (client.updateComponent(this, uidl, true)) {
+ return;
+ }
+
+ pid = uidl.getId();
+ if (!hasTemplate()) {
+ // Update HTML template only once
+ initializeHTML(uidl, client);
+ }
+
+ // Evaluate scripts
+ eval(scripts);
+ scripts = null;
+
+ iLayout();
+ // TODO Check if this is needed
+ client.runDescendentsLayout(this);
+
+ 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();
+ // TODO Check if this is needed
+ client.runDescendentsLayout(this);
+
+ }
+
+ /** Initialize HTML-layout. */
+ private void initializeHTML(UIDL uidl, ApplicationConnection client) {
+
+ final String newTemplateContents = uidl
+ .getStringAttribute("templateContents");
+ final String newTemplate = uidl.getStringAttribute("template");
+
+ currentTemplateName = null;
+ hasTemplateContents = false;
+
+ String template = "";
+ if (newTemplate != null) {
+ // Get the HTML-template from client
+ 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 {
+ currentTemplateName = newTemplate;
+ }
+ } else {
+ hasTemplateContents = true;
+ template = newTemplateContents;
+ }
+
+ // Connect body of the template to DOM
+ template = extractBodyAndScriptsFromTemplate(template);
+
+ // TODO prefix img src:s here with a regeps, cannot work further with IE
+
+ String themeUri = client.getThemeUri();
+ String relImgPrefix = themeUri + "/layouts/";
+
+ // prefix all relative image elements to point to theme dir with a
+ // regexp search
+ template = template.replaceAll(
+ "<((?:img)|(?:IMG))\\s([^>]*)src=\"((?![a-z]+:)[^/][^\"]+)\"",
+ "<$1 $2src=\"" + relImgPrefix + "$3\"");
+ // also support src attributes without quotes
+ template = template
+ .replaceAll(
+ "<((?:img)|(?:IMG))\\s([^>]*)src=[^\"]((?![a-z]+:)[^/][^ />]+)[ />]",
+ "<$1 $2src=\"" + relImgPrefix + "$3\"");
+ // also prefix relative style="...url(...)..."
+ template = template
+ .replaceAll(
+ "(<[^>]+style=\"[^\"]*url\\()((?![a-z]+:)[^/][^\"]+)(\\)[^>]*>)",
+ "$1 " + relImgPrefix + "$2 $3");
+
+ getElement().setInnerHTML(template);
+
+ // Remap locations to elements
+ locationToElement.clear();
+ scanForLocations(getElement());
+
+ initImgElements();
+
+ elementWithNativeResizeFunction = DOM.getFirstChild(getElement());
+ if (elementWithNativeResizeFunction == null) {
+ elementWithNativeResizeFunction = getElement();
+ }
+ publishResizedFunction(elementWithNativeResizeFunction);
+
+ }
+
+ private native boolean uriEndsWithSlash()
+ /*-{
+ var path = $wnd.location.pathname;
+ if(path.charAt(path.length - 1) == "/")
+ return true;
+ return false;
+ }-*/;
+
+ private boolean hasTemplate() {
+ if (currentTemplateName == null && !hasTemplateContents) {
+ return false;
+ } else {
+ return true;
+ }
+ }
+
+ /** Collect locations from template */
+ private void scanForLocations(Element elem) {
+
+ final String location = elem.getAttribute("location");
+ if (!"".equals(location)) {
+ locationToElement.put(location, elem);
+ elem.setInnerHTML("");
+ int x = Util.measureHorizontalPaddingAndBorder(elem, 0);
+ int y = Util.measureVerticalPaddingAndBorder(elem, 0);
+
+ FloatSize fs = new FloatSize(x, y);
+
+ locationToExtraSize.put(location, fs);
+
+ } else {
+ final int len = DOM.getChildCount(elem);
+ for (int i = 0; i < len; i++) {
+ scanForLocations(DOM.getChild(elem, i));
+ }
+ }
+ }
+
+ /** 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) {
+ }
+ }-*/;
+
+ /**
+ * Img elements needs some special handling in custom layout. Img elements
+ * will get their onload events sunk. This way custom layout can notify
+ * parent about possible size change.
+ */
+ private void initImgElements() {
+ NodeList<com.google.gwt.dom.client.Element> nodeList = getElement()
+ .getElementsByTagName("IMG");
+ for (int i = 0; i < nodeList.getLength(); i++) {
+ com.google.gwt.dom.client.ImageElement img = (ImageElement) nodeList
+ .getItem(i);
+ DOM.sinkEvents((Element) img.cast(), Event.ONLOAD);
+ }
+ }
+
+ /**
+ * 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) {
+ ICaptionWrapper wrapper = (ICaptionWrapper) widgetToCaptionWrapper
+ .get(component);
+ if (ICaption.isNeeded(uidl)) {
+ if (wrapper == null) {
+ final String loc = getLocation((Widget) component);
+ super.remove((Widget) component);
+ wrapper = new ICaptionWrapper(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 */
+ @Override
+ public boolean remove(Widget w) {
+ client.unregisterPaintable((Paintable) w);
+ final String location = getLocation(w);
+ if (location != null) {
+ locationToWidget.remove(location);
+ }
+ final ICaptionWrapper cw = (ICaptionWrapper) 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 */
+ @Override
+ public void add(Widget w) {
+ throw new UnsupportedOperationException();
+ }
+
+ /** Clear all widgets from the layout */
+ @Override
+ public void clear() {
+ super.clear();
+ locationToWidget.clear();
+ widgetToCaptionWrapper.clear();
+ }
+
+ public void iLayout() {
+ iLayoutJS(DOM.getFirstChild(getElement()));
+ }
+
+ /**
+ * 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() {
+ client.runDescendentsLayout(this);
+ }
+
+ @Override
+ public void onDetach() {
+ super.onDetach();
+ detachResizedFunction(elementWithNativeResizeFunction);
+ }
+
+ private native void detachResizedFunction(Element element)
+ /*-{
+ element.notifyChildrenOfSizeChange = null;
+ }-*/;
+
+ private native void publishResizedFunction(Element element)
+ /*-{
+ var self = this;
+ element.notifyChildrenOfSizeChange = function() {
+ self.@com.vaadin.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;
+ }
+ }-*/;
+
+ public boolean requestLayout(Set<Paintable> child) {
+ updateRelativeSizedComponents(true, true);
+
+ if (width.equals("") || height.equals("")) {
+ /* Automatically propagated upwards if the size can change */
+ return false;
+ }
+
+ return true;
+ }
+
+ public RenderSpace getAllocatedSpace(Widget child) {
+ com.google.gwt.dom.client.Element pe = child.getElement()
+ .getParentElement();
+
+ FloatSize extra = locationToExtraSize.get(getLocation(child));
+ return new RenderSpace(pe.getOffsetWidth() - (int) extra.getWidth(), pe
+ .getOffsetHeight()
+ - (int) extra.getHeight(), Util.mayHaveScrollBars(pe));
+ }
+
+ @Override
+ public void onBrowserEvent(Event event) {
+ super.onBrowserEvent(event);
+ if (event.getTypeInt() == Event.ONLOAD) {
+ Util.notifyParentOfSizeChange(this, true);
+ event.cancelBubble(true);
+ }
+ }
+
+ @Override
+ public void setHeight(String height) {
+ if (this.height.equals(height)) {
+ return;
+ }
+
+ boolean shrinking = true;
+ if (isLarger(height, this.height)) {
+ shrinking = false;
+ }
+
+ this.height = height;
+ super.setHeight(height);
+
+ /*
+ * If the height shrinks we must remove all components with relative
+ * height from the DOM, update their height when they do not affect the
+ * available space and finally restore them to the original state
+ */
+ if (shrinking) {
+ updateRelativeSizedComponents(false, true);
+ }
+ }
+
+ @Override
+ public void setWidth(String width) {
+ if (this.width.equals(width)) {
+ return;
+ }
+
+ boolean shrinking = true;
+ if (isLarger(width, this.width)) {
+ shrinking = false;
+ }
+
+ super.setWidth(width);
+ this.width = width;
+
+ /*
+ * If the width shrinks we must remove all components with relative
+ * width from the DOM, update their width when they do not affect the
+ * available space and finally restore them to the original state
+ */
+ if (shrinking) {
+ updateRelativeSizedComponents(true, false);
+ }
+ }
+
+ private void updateRelativeSizedComponents(boolean relativeWidth,
+ boolean relativeHeight) {
+
+ Set<Widget> relativeSizeWidgets = new HashSet<Widget>();
+
+ for (Widget widget : locationToWidget.values()) {
+ FloatSize relativeSize = client.getRelativeSize(widget);
+ if (relativeSize != null) {
+ if ((relativeWidth && (relativeSize.getWidth() >= 0.0f))
+ || (relativeHeight && (relativeSize.getHeight() >= 0.0f))) {
+
+ relativeSizeWidgets.add(widget);
+ widget.getElement().getStyle().setProperty("position",
+ "absolute");
+ }
+ }
+ }
+
+ for (Widget widget : relativeSizeWidgets) {
+ client.handleComponentRelativeSize(widget);
+ widget.getElement().getStyle().setProperty("position", "");
+ }
+ }
+
+ /**
+ * Compares newSize with currentSize and returns true if it is clear that
+ * newSize is larger than currentSize. Returns false if newSize is smaller
+ * or if it is unclear which one is smaller.
+ *
+ * @param newSize
+ * @param currentSize
+ * @return
+ */
+ private boolean isLarger(String newSize, String currentSize) {
+ if (newSize.equals("") || currentSize.equals("")) {
+ return false;
+ }
+
+ if (!newSize.endsWith("px") || !currentSize.endsWith("px")) {
+ return false;
+ }
+
+ int newSizePx = Integer.parseInt(newSize.substring(0,
+ newSize.length() - 2));
+ int currentSizePx = Integer.parseInt(currentSize.substring(0,
+ currentSize.length() - 2));
+
+ boolean larger = newSizePx > currentSizePx;
+ return larger;
+ }
+
+}
diff --git a/src/com/vaadin/terminal/gwt/client/ui/IDateField.java b/src/com/vaadin/terminal/gwt/client/ui/IDateField.java
new file mode 100644
index 0000000000..b62def236e
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/IDateField.java
@@ -0,0 +1,233 @@
+/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui;
+
+import java.util.Date;
+
+import com.google.gwt.user.client.Event;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.ClientExceptionHandler;
+import com.vaadin.terminal.gwt.client.DateTimeService;
+import com.vaadin.terminal.gwt.client.ITooltip;
+import com.vaadin.terminal.gwt.client.LocaleNotLoadedException;
+import com.vaadin.terminal.gwt.client.Paintable;
+import com.vaadin.terminal.gwt.client.UIDL;
+
+public class IDateField extends FlowPanel implements Paintable, Field {
+
+ public static final String CLASSNAME = "i-datefield";
+
+ protected String id;
+
+ protected ApplicationConnection client;
+
+ protected boolean immediate;
+
+ public static final int RESOLUTION_YEAR = 0;
+ public static final int RESOLUTION_MONTH = 1;
+ public static final int RESOLUTION_DAY = 2;
+ public static final int RESOLUTION_HOUR = 3;
+ public static final int RESOLUTION_MIN = 4;
+ public static final int RESOLUTION_SEC = 5;
+ public static final int RESOLUTION_MSEC = 6;
+
+ protected int currentResolution = RESOLUTION_YEAR;
+
+ protected String currentLocale;
+
+ protected boolean readonly;
+
+ protected boolean enabled;
+
+ protected Date date = null;
+ // e.g when paging a calendar, before actually selecting
+ protected Date showingDate = new Date();
+
+ protected DateTimeService dts;
+
+ public IDateField() {
+ setStyleName(CLASSNAME);
+ dts = new DateTimeService();
+ sinkEvents(ITooltip.TOOLTIP_EVENTS);
+ }
+
+ @Override
+ public void onBrowserEvent(Event event) {
+ super.onBrowserEvent(event);
+ if (client != null) {
+ client.handleTooltipEvent(event, this);
+ }
+ }
+
+ public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+ // Ensure correct implementation and let layout manage caption
+ if (client.updateComponent(this, uidl, true)) {
+ return;
+ }
+
+ // Save details
+ this.client = client;
+ id = uidl.getId();
+ immediate = uidl.getBooleanAttribute("immediate");
+
+ readonly = uidl.getBooleanAttribute("readonly");
+ enabled = !uidl.getBooleanAttribute("disabled");
+
+ if (uidl.hasAttribute("locale")) {
+ final String locale = uidl.getStringAttribute("locale");
+ try {
+ dts.setLocale(locale);
+ currentLocale = locale;
+ } catch (final LocaleNotLoadedException e) {
+ currentLocale = dts.getLocale();
+ ClientExceptionHandler.displayError(
+ "Tried to use an unloaded locale \"" + locale
+ + "\". Using default locale (" + currentLocale
+ + ").", e);
+ }
+ }
+
+ int newResolution;
+ if (uidl.hasVariable("msec")) {
+ newResolution = RESOLUTION_MSEC;
+ } else if (uidl.hasVariable("sec")) {
+ newResolution = RESOLUTION_SEC;
+ } else if (uidl.hasVariable("min")) {
+ newResolution = RESOLUTION_MIN;
+ } else if (uidl.hasVariable("hour")) {
+ newResolution = RESOLUTION_HOUR;
+ } else if (uidl.hasVariable("day")) {
+ newResolution = RESOLUTION_DAY;
+ } else if (uidl.hasVariable("month")) {
+ newResolution = RESOLUTION_MONTH;
+ } else {
+ newResolution = RESOLUTION_YEAR;
+ }
+
+ currentResolution = newResolution;
+
+ final int year = uidl.getIntVariable("year");
+ final int month = (currentResolution >= RESOLUTION_MONTH) ? uidl
+ .getIntVariable("month") : -1;
+ final int day = (currentResolution >= RESOLUTION_DAY) ? uidl
+ .getIntVariable("day") : -1;
+ final int hour = (currentResolution >= RESOLUTION_HOUR) ? uidl
+ .getIntVariable("hour") : 0;
+ final int min = (currentResolution >= RESOLUTION_MIN) ? uidl
+ .getIntVariable("min") : 0;
+ final int sec = (currentResolution >= RESOLUTION_SEC) ? uidl
+ .getIntVariable("sec") : 0;
+ final int msec = (currentResolution >= RESOLUTION_MSEC) ? uidl
+ .getIntVariable("msec") : 0;
+
+ // Construct new date for this datefield (only if not null)
+ if (year > -1) {
+ date = new Date((long) getTime(year, month, day, hour, min, sec,
+ msec));
+ showingDate.setTime(date.getTime());
+ } else {
+ date = null;
+ showingDate = new Date();
+ }
+
+ }
+
+ /*
+ * We need this redundant native function because Java's Date object doesn't
+ * have a setMilliseconds method.
+ */
+ private static native double getTime(int y, int m, int d, int h, int mi,
+ int s, int ms)
+ /*-{
+ try {
+ var date = new Date(2000,1,1,1); // don't use current date here
+ if(y && y >= 0) date.setFullYear(y);
+ if(m && m >= 1) date.setMonth(m-1);
+ if(d && d >= 0) date.setDate(d);
+ if(h >= 0) date.setHours(h);
+ if(mi >= 0) date.setMinutes(mi);
+ if(s >= 0) date.setSeconds(s);
+ if(ms >= 0) date.setMilliseconds(ms);
+ return date.getTime();
+ } catch (e) {
+ // TODO print some error message on the console
+ //console.log(e);
+ return (new Date()).getTime();
+ }
+ }-*/;
+
+ public int getMilliseconds() {
+ return (int) (date.getTime() - date.getTime() / 1000 * 1000);
+ }
+
+ public void setMilliseconds(int ms) {
+ date.setTime(date.getTime() / 1000 * 1000 + ms);
+ }
+
+ public int getShowingMilliseconds() {
+ return (int) (showingDate.getTime() - showingDate.getTime() / 1000 * 1000);
+ }
+
+ public void setShowingMilliseconds(int ms) {
+ showingDate.setTime(showingDate.getTime() / 1000 * 1000 + ms);
+ }
+
+ public int getCurrentResolution() {
+ return currentResolution;
+ }
+
+ public void setCurrentResolution(int currentResolution) {
+ this.currentResolution = currentResolution;
+ }
+
+ public String getCurrentLocale() {
+ return currentLocale;
+ }
+
+ public void setCurrentLocale(String currentLocale) {
+ this.currentLocale = currentLocale;
+ }
+
+ public Date getCurrentDate() {
+ return date;
+ }
+
+ public void setCurrentDate(Date date) {
+ this.date = date;
+ }
+
+ public Date getShowingDate() {
+ return showingDate;
+ }
+
+ public void setShowingDate(Date date) {
+ showingDate = date;
+ }
+
+ public boolean isImmediate() {
+ return immediate;
+ }
+
+ public boolean isReadonly() {
+ return readonly;
+ }
+
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ public DateTimeService getDateTimeService() {
+ return dts;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public ApplicationConnection getClient() {
+ return client;
+ }
+}
diff --git a/src/com/vaadin/terminal/gwt/client/ui/IDateFieldCalendar.java b/src/com/vaadin/terminal/gwt/client/ui/IDateFieldCalendar.java
new file mode 100644
index 0000000000..24f614702e
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/IDateFieldCalendar.java
@@ -0,0 +1,26 @@
+/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui;
+
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.UIDL;
+
+public class IDateFieldCalendar extends IDateField {
+
+ private final ICalendarPanel date;
+
+ public IDateFieldCalendar() {
+ super();
+ date = new ICalendarPanel(this);
+ add(date);
+ }
+
+ @Override
+ public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+ super.updateFromUIDL(uidl, client);
+ date.updateCalendar();
+ }
+
+}
diff --git a/src/com/vaadin/terminal/gwt/client/ui/IEmbedded.java b/src/com/vaadin/terminal/gwt/client/ui/IEmbedded.java
new file mode 100644
index 0000000000..8c8340b470
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/IEmbedded.java
@@ -0,0 +1,225 @@
+/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui;
+
+import java.util.Iterator;
+
+import com.google.gwt.dom.client.Document;
+import com.google.gwt.dom.client.Node;
+import com.google.gwt.dom.client.NodeList;
+import com.google.gwt.dom.client.ObjectElement;
+import com.google.gwt.dom.client.Style;
+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.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.Paintable;
+import com.vaadin.terminal.gwt.client.UIDL;
+import com.vaadin.terminal.gwt.client.Util;
+
+public class IEmbedded extends HTML implements Paintable {
+ private static String CLASSNAME = "i-embedded";
+
+ private String height;
+ private String width;
+ private Element browserElement;
+
+ private ApplicationConnection client;
+
+ public IEmbedded() {
+ setStyleName(CLASSNAME);
+ }
+
+ public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+ if (client.updateComponent(this, uidl, true)) {
+ return;
+ }
+ this.client = client;
+
+ boolean clearBrowserElement = true;
+
+ if (uidl.hasAttribute("type")) {
+ final String type = uidl.getStringAttribute("type");
+ if (type.equals("image")) {
+ Element el = null;
+ boolean created = false;
+ NodeList nodes = getElement().getChildNodes();
+ if (nodes != null && nodes.getLength() == 1) {
+ Node n = nodes.getItem(0);
+ if (n.getNodeType() == Node.ELEMENT_NODE) {
+ Element e = (Element) n;
+ if (e.getTagName().equals("IMG")) {
+ el = e;
+ }
+ }
+ }
+ if (el == null) {
+ setHTML("");
+ el = DOM.createImg();
+ created = true;
+ client.addPngFix(el);
+ DOM.sinkEvents(el, Event.ONLOAD);
+ }
+
+ // Set attributes
+ Style style = el.getStyle();
+ String w = uidl.getStringAttribute("width");
+ if (w != null) {
+ style.setProperty("width", w);
+ } else {
+ style.setProperty("width", "");
+ }
+ String h = uidl.getStringAttribute("height");
+ if (h != null) {
+ style.setProperty("height", h);
+ } else {
+ style.setProperty("height", "");
+ }
+ DOM.setElementProperty(el, "src", getSrc(uidl, client));
+
+ if (created) {
+ // insert in dom late
+ getElement().appendChild(el);
+ }
+
+ } else if (type.equals("browser")) {
+ if (browserElement == null) {
+ setHTML("<iframe width=\"100%\" height=\"100%\" frameborder=\"0\" src=\""
+ + getSrc(uidl, client)
+ + "\" name=\""
+ + uidl.getId() + "\"></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=\"" + height
+ + "\"><param name=\"movie\" value=\""
+ + getSrc(uidl, client) + "\"><embed src=\""
+ + getSrc(uidl, client) + "\" width=\"" + width
+ + "\" height=\"" + height + "\"></embed></object>");
+ } else if (mime.equals("image/svg+xml")) {
+ String data;
+ if (getParameter("data", uidl) == null) {
+ data = getSrc(uidl, client);
+ } else {
+ data = "data:image/svg+xml," + getParameter("data", uidl);
+ }
+ setHTML("");
+ ObjectElement obj = Document.get().createObjectElement();
+ obj.setType(mime);
+ obj.setData(data);
+ if (width != null) {
+ obj.getStyle().setProperty("width", "100%");
+ }
+ if (height != null) {
+ obj.getStyle().setProperty("height", "100%");
+ }
+ getElement().appendChild(obj);
+
+ } else {
+ ApplicationConnection.getConsole().log(
+ "Unknown Embedded mimetype '" + mime + "'");
+ }
+ } else {
+ ApplicationConnection.getConsole().log(
+ "Unknown Embedded; no type or mimetype attribute");
+ }
+
+ if (clearBrowserElement) {
+ browserElement = null;
+ }
+
+ }
+
+ private static String getParameter(String paramName, UIDL uidl) {
+ Iterator childIterator = uidl.getChildIterator();
+ while (childIterator.hasNext()) {
+ Object child = childIterator.next();
+ if (child instanceof UIDL) {
+ UIDL childUIDL = (UIDL) child;
+ if (childUIDL.getTag().equals("embeddedparam")
+ && childUIDL.getStringAttribute("name").equals(
+ paramName)) {
+ return childUIDL.getStringAttribute("value");
+ }
+
+ }
+ }
+ return 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;
+ }
+
+ @Override
+ public void setWidth(String width) {
+ this.width = width;
+ if (isDynamicHeight()) {
+ int oldHeight = getOffsetHeight();
+ super.setWidth(width);
+ int newHeight = getOffsetHeight();
+ /*
+ * Must notify parent if the height changes as a result of a width
+ * change
+ */
+ if (oldHeight != newHeight) {
+ Util.notifyParentOfSizeChange(this, false);
+ }
+ } else {
+ super.setWidth(width);
+ }
+
+ }
+
+ private boolean isDynamicHeight() {
+ return height == null || height.equals("");
+ }
+
+ @Override
+ public void setHeight(String height) {
+ this.height = height;
+ super.setHeight(height);
+ }
+
+ @Override
+ 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();
+ }
+
+ @Override
+ public void onBrowserEvent(Event event) {
+ super.onBrowserEvent(event);
+ if (DOM.eventGetType(event) == Event.ONLOAD) {
+ Util.notifyParentOfSizeChange(this, true);
+ }
+ }
+}
diff --git a/src/com/vaadin/terminal/gwt/client/ui/IFilterSelect.java b/src/com/vaadin/terminal/gwt/client/ui/IFilterSelect.java
new file mode 100644
index 0000000000..ec9ecf1cf6
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/IFilterSelect.java
@@ -0,0 +1,1059 @@
+/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Date;
+import java.util.Iterator;
+import java.util.List;
+
+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.LoadListener;
+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.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.BrowserInfo;
+import com.vaadin.terminal.gwt.client.Focusable;
+import com.vaadin.terminal.gwt.client.ITooltip;
+import com.vaadin.terminal.gwt.client.Paintable;
+import com.vaadin.terminal.gwt.client.UIDL;
+import com.vaadin.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=\"\" class=\"i-icon\" />");
+ }
+ sb.append("<span>" + Util.escapeHTML(caption) + "</span>");
+ 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);
+ }
+ }
+
+ public class SuggestionPopup extends IToolkitOverlay implements
+ PositionCallback, PopupListener {
+
+ 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;
+
+ private int popupOuterPadding = -1;
+
+ private int topPosition;
+
+ 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<FilterSelectSuggestion> currentSuggestions,
+ int currentPage, int totalSuggestions) {
+
+ // Add TT anchor point
+ DOM.setElementProperty(getElement(), "id",
+ "TOOLKIT_COMBOBOX_OPTIONLIST");
+
+ menu.setSuggestions(currentSuggestions);
+ final int x = IFilterSelect.this.getAbsoluteLeft();
+ topPosition = tb.getAbsoluteTop();
+ topPosition += tb.getOffsetHeight();
+ setPopupPosition(x, topPosition);
+
+ 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()) {
+ lastIndex = index - 1; // save for paging
+ 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) {
+ lastIndex = index + 1; // save for paging
+ 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());
+ }
+ }
+
+ @Override
+ 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 = getMainWidth();
+ int naturalMenuWidth = DOM.getElementPropertyInt(DOM
+ .getFirstChild(menu.getElement()), "offsetWidth");
+
+ if (popupOuterPadding == -1) {
+ popupOuterPadding = Util.measureHorizontalPaddingAndBorder(
+ getElement(), 2);
+ }
+
+ if (naturalMenuWidth < desiredWidth) {
+ menu.setWidth((desiredWidth - popupOuterPadding) + "px");
+ DOM.setStyleAttribute(DOM.getFirstChild(menu.getElement()),
+ "width", "100%");
+ naturalMenuWidth = desiredWidth;
+ }
+
+ if (BrowserInfo.get().isIE()) {
+ /*
+ * IE requires us to specify the width for the container
+ * element. Otherwise it will be 100% wide
+ */
+ int rootWidth = naturalMenuWidth - popupOuterPadding;
+ DOM.setStyleAttribute(getContainerElement(), "width", rootWidth
+ + "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();
+ /*
+ * Take popup top margin into account. getPopupTop() returns the
+ * top value including the margin but the value we give must not
+ * include the margin.
+ */
+ int topMargin = (top - topPosition);
+ top -= topMargin;
+ }
+
+ // 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();
+ }
+ }
+
+ /**
+ * Updates style names in suggestion popup to help theme building.
+ */
+ public void updateStyleNames(UIDL uidl) {
+ if (uidl.hasAttribute("style")) {
+ setStyleName(CLASSNAME + "-suggestpopup");
+ final String[] styles = uidl.getStringAttribute("style").split(
+ " ");
+ for (int i = 0; i < styles.length; i++) {
+ addStyleDependentName(styles[i]);
+ }
+ }
+ }
+
+ }
+
+ 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<FilterSelectSuggestion> suggestions) {
+ clearItems();
+ final Iterator<FilterSelectSuggestion> it = suggestions.iterator();
+ while (it.hasNext()) {
+ final FilterSelectSuggestion s = it.next();
+ final MenuItem mi = new MenuItem(s.getDisplayString(), true, s);
+
+ com.google.gwt.dom.client.Element child = mi.getElement()
+ .getFirstChildElement();
+ while (child != null) {
+ if (child.getNodeName().toLowerCase().equals("img")) {
+ DOM
+ .sinkEvents((Element) child.cast(),
+ (DOM.getEventsSunk((Element) child
+ .cast()) | Event.ONLOAD));
+ }
+ child = child.getNextSiblingElement();
+ }
+
+ 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 (!prompting && !enteredItemValue.equals(lastNewItemString)) {
+ /*
+ * Store last sent new item string to avoid double sends
+ */
+ lastNewItemString = enteredItemValue;
+ 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();
+ /* TODO?
+ if (text.equals("")) {
+ addStyleDependentName(CLASSNAME_PROMPT);
+ tb.setText(inputPrompt);
+ prompting = true;
+ } else {
+ tb.setText(text);
+ prompting = false;
+ removeStyleDependentName(CLASSNAME_PROMPT);
+ }
+ */
+ selectedOptionKey = currentSuggestion.key;
+ }
+ }
+ suggestionPopup.hide();
+ }
+
+ @Override
+ public void onBrowserEvent(Event event) {
+ if (event.getTypeInt() == Event.ONLOAD) {
+ if (suggestionPopup.isVisible()) {
+ setWidth("");
+ DOM.setStyleAttribute(DOM.getFirstChild(getElement()),
+ "width", "");
+ suggestionPopup.setPopupPositionAndShow(suggestionPopup);
+ }
+ }
+ super.onBrowserEvent(event);
+ }
+ }
+
+ 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() {
+ @Override
+ 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<FilterSelectSuggestion> currentSuggestions = new ArrayList<FilterSelectSuggestion>();
+
+ private boolean immediate;
+
+ private String selectedOptionKey;
+
+ private boolean filtering = false;
+
+ private String lastFilter = "";
+ private int lastIndex = -1; // last selected index when using arrows
+
+ 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 static final String CLASSNAME_PROMPT = "prompt";
+ private static final String ATTR_INPUTPROMPT = "prompt";
+ private String inputPrompt = "";
+ private boolean prompting = false;
+
+ // Set true when popupopened has been clicked. Cleared on each UIDL-update.
+ // This handles the special case where are not filtering yet and the
+ // selected value has changed on the server-side. See #2119
+ private boolean popupOpenerClicked;
+ private String width = null;
+ private int textboxPadding = -1;
+ private int componentPadding = -1;
+ private int suggestionPopupMinWidth = 0;
+ /*
+ * Stores the last new item string to avoid double submissions. Cleared on
+ * uidl updates
+ */
+ private String lastNewItemString;
+ private boolean focused = false;
+
+ public IFilterSelect() {
+ selectedItemIcon.setVisible(false);
+ selectedItemIcon.setStyleName("i-icon");
+ selectedItemIcon.addLoadListener(new LoadListener() {
+ public void onError(Widget sender) {
+ }
+
+ public void onLoad(Widget sender) {
+ updateRootWidth();
+ updateSelectedIconPosition();
+ }
+ });
+
+ panel.add(selectedItemIcon);
+ tb.sinkEvents(ITooltip.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;
+ }
+
+ // not a FocusWidget -> needs own tabindex handling
+ if (uidl.hasAttribute("tabindex")) {
+ tb.setTabIndex(uidl.getIntAttribute("tabindex"));
+ }
+
+ immediate = uidl.hasAttribute("immediate");
+
+ nullSelectionAllowed = uidl.hasAttribute("nullselect");
+
+ currentPage = uidl.getIntVariable("page");
+
+ if (uidl.hasAttribute(ATTR_INPUTPROMPT)) {
+ // input prompt changed from server
+ inputPrompt = uidl.getStringAttribute(ATTR_INPUTPROMPT);
+ } else {
+ inputPrompt = "";
+ }
+
+ suggestionPopup.setPagingEnabled(true);
+ suggestionPopup.updateStyleNames(uidl);
+
+ allowNewItem = uidl.hasAttribute("allownewitem");
+ lastNewItemString = null;
+
+ currentSuggestions.clear();
+ final UIDL options = uidl.getChildUIDL(0);
+ totalMatches = uidl.getIntAttribute("totalMatches");
+
+ String captions = inputPrompt;
+
+ 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 || popupOpenerClicked) {
+ tb.setText(suggestion.getReplacementString());
+ selectedOptionKey = "" + suggestion.getOptionKey();
+ }
+ currentSuggestion = suggestion;
+ setSelectedItemIcon(suggestion.getIconUri());
+ }
+
+ // Collect captions so we can calculate minimum width for textarea
+ if (captions.length() > 0) {
+ captions += "|";
+ }
+ captions += suggestion.getReplacementString();
+ }
+
+ if ((!filtering || popupOpenerClicked) && uidl.hasVariable("selected")
+ && uidl.getStringArrayVariable("selected").length == 0) {
+ // select nulled
+ if (!filtering || !popupOpenerClicked) {
+ setPromptingOn();
+ }
+ selectedOptionKey = null;
+ }
+
+ if (filtering
+ && lastFilter.toLowerCase().equals(
+ uidl.getStringVariable("filter"))) {
+ suggestionPopup.showSuggestions(currentSuggestions, currentPage,
+ totalMatches);
+ filtering = false;
+ if (!popupOpenerClicked && lastIndex != -1) {
+ // we're paging w/ arrows
+ if (lastIndex == 0) {
+ // going up, select last item
+ int lastItem = PAGELENTH - 1;
+ List items = suggestionPopup.menu.getItems();
+ /*
+ * The first page can contain less than 10 items if the null
+ * selection item is filtered away
+ */
+ if (lastItem >= items.size()) {
+ lastItem = items.size() - 1;
+ }
+ suggestionPopup.menu.selectItem((MenuItem) items
+ .get(lastItem));
+ } else {
+ // going down, select first item
+ suggestionPopup.menu
+ .selectItem((MenuItem) suggestionPopup.menu
+ .getItems().get(0));
+ }
+ lastIndex = -1; // reset
+ }
+ }
+
+ // Calculate minumum textarea width
+ suggestionPopupMinWidth = minWidth(captions);
+
+ popupOpenerClicked = false;
+
+ updateRootWidth();
+ }
+
+ private void setPromptingOn() {
+ prompting = true;
+ addStyleDependentName(CLASSNAME_PROMPT);
+ tb.setText(inputPrompt);
+ }
+
+ private void setPromptingOff(String text) {
+ tb.setText(text);
+ prompting = false;
+ removeStyleDependentName(CLASSNAME_PROMPT);
+ }
+
+ 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();
+ if ("".equals(newKey) && !focused) {
+ setPromptingOn();
+ } else {
+ setPromptingOff(text);
+ }
+ 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);
+ updateRootWidth();
+ } else {
+ selectedItemIcon.setUrl(iconUri);
+ selectedItemIcon.setVisible(true);
+ updateRootWidth();
+ updateSelectedIconPosition();
+ }
+ }
+
+ private void updateSelectedIconPosition() {
+ // Position icon vertically to middle
+ int availableHeight = getOffsetHeight();
+ int iconHeight = Util.getRequiredHeight(selectedItemIcon);
+ int marginTop = (availableHeight - iconHeight) / 2;
+ DOM.setStyleAttribute(selectedItemIcon.getElement(), "marginTop",
+ marginTop + "px");
+ }
+
+ 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:
+ case KeyboardListener.KEY_SHIFT:
+ case KeyboardListener.KEY_CTRL:
+ case KeyboardListener.KEY_ALT:
+ ; // 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();
+ setPromptingOff(text);
+ selectedOptionKey = currentSuggestion.key;
+ } else {
+ setPromptingOn();
+ 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, "");
+ popupOpenerClicked = true;
+ lastFilter = "";
+ } else if (selectedOptionKey == null) {
+ tb.setText(inputPrompt);
+ prompting = true;
+ }
+ 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) {
+ focused = true;
+ if (prompting) {
+ setPromptingOff("");
+ }
+ addStyleDependentName("focus");
+ }
+
+ public void onLostFocus(Widget sender) {
+ focused = false;
+ if (!suggestionPopup.isAttached() || suggestionPopup.isJustClosed()) {
+ // typing so fast the popup was never opened, or it's just closed
+ suggestionPopup.menu.doSelectedItemAction();
+ }
+ if (selectedOptionKey == null) {
+ setPromptingOn();
+ }
+ removeStyleDependentName("focus");
+ }
+
+ public void focus() {
+ focused = true;
+ if (prompting) {
+ setPromptingOff("");
+ }
+ tb.setFocus(true);
+ }
+
+ @Override
+ public void setWidth(String width) {
+ if (width == null || width.equals("")) {
+ this.width = null;
+ } else {
+ this.width = width;
+ }
+ Util.setWidthExcludingPaddingAndBorder(this, width, 4);
+ updateRootWidth();
+ }
+
+ @Override
+ public void setHeight(String height) {
+ super.setHeight(height);
+ Util.setHeightExcludingPaddingAndBorder(tb, height, 3);
+ }
+
+ private void updateRootWidth() {
+ if (width == null) {
+ /*
+ * When the width is not specified we must specify width for root
+ * div so the popupopener won't wrap to the next line and also so
+ * the size of the combobox won't change over time.
+ */
+ int tbWidth = Util.getRequiredWidth(tb);
+ int openerWidth = Util.getRequiredWidth(popupOpener);
+ int iconWidth = Util.getRequiredWidth(selectedItemIcon);
+
+ int w = tbWidth + openerWidth + iconWidth;
+ if (suggestionPopupMinWidth > w) {
+ setTextboxWidth(suggestionPopupMinWidth);
+ w = suggestionPopupMinWidth;
+ } else {
+ /*
+ * Firefox3 has its own way of doing rendering so we need to
+ * specify the width for the TextField to make sure it actually
+ * is rendered as wide as FF3 says it is
+ */
+ tb.setWidth((tbWidth - getTextboxPadding()) + "px");
+ }
+ super.setWidth((w) + "px");
+ // Freeze the initial width, so that it won't change even if the
+ // icon size changes
+ width = w + "px";
+
+ } else {
+ /*
+ * When the width is specified we also want to explicitly specify
+ * widths for textbox and popupopener
+ */
+ setTextboxWidth(getMainWidth() - getComponentPadding());
+
+ }
+ }
+
+ private int getMainWidth() {
+ int componentWidth;
+ if (BrowserInfo.get().isIE6()) {
+ // Required in IE when textfield is wider than this.width
+ DOM.setStyleAttribute(getElement(), "overflow", "hidden");
+ componentWidth = getOffsetWidth();
+ DOM.setStyleAttribute(getElement(), "overflow", "");
+ } else {
+ componentWidth = getOffsetWidth();
+ }
+ return componentWidth;
+ }
+
+ private void setTextboxWidth(int componentWidth) {
+ int padding = getTextboxPadding();
+ int popupOpenerWidth = Util.getRequiredWidth(popupOpener);
+ int iconWidth = Util.getRequiredWidth(selectedItemIcon);
+ int textboxWidth = componentWidth - padding - popupOpenerWidth
+ - iconWidth;
+ if (textboxWidth < 0) {
+ textboxWidth = 0;
+ }
+ tb.setWidth(textboxWidth + "px");
+ }
+
+ private int getTextboxPadding() {
+ if (textboxPadding < 0) {
+ textboxPadding = Util.measureHorizontalPaddingAndBorder(tb
+ .getElement(), 4);
+ }
+ return textboxPadding;
+ }
+
+ private int getComponentPadding() {
+ if (componentPadding < 0) {
+ componentPadding = Util.measureHorizontalPaddingAndBorder(
+ getElement(), 3);
+ }
+ return componentPadding;
+ }
+}
diff --git a/src/com/vaadin/terminal/gwt/client/ui/IForm.java b/src/com/vaadin/terminal/gwt/client/ui/IForm.java
new file mode 100644
index 0000000000..9e6c0f5ae2
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/IForm.java
@@ -0,0 +1,288 @@
+/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui;
+
+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.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.BrowserInfo;
+import com.vaadin.terminal.gwt.client.Container;
+import com.vaadin.terminal.gwt.client.IErrorMessage;
+import com.vaadin.terminal.gwt.client.Paintable;
+import com.vaadin.terminal.gwt.client.RenderInformation;
+import com.vaadin.terminal.gwt.client.RenderSpace;
+import com.vaadin.terminal.gwt.client.UIDL;
+import com.vaadin.terminal.gwt.client.Util;
+
+public class IForm extends ComplexPanel implements Container {
+
+ private String height = "";
+
+ private String width = "";
+
+ public static final String CLASSNAME = "i-form";
+
+ private Container lo;
+ private Element legend = DOM.createLegend();
+ private Element caption = DOM.createSpan();
+ private Element errorIndicatorElement = DOM.createDiv();
+ private Element desc = DOM.createDiv();
+ private Icon icon;
+ private IErrorMessage errorMessage = new IErrorMessage();
+
+ private Element fieldContainer = DOM.createDiv();
+
+ private Element footerContainer = DOM.createDiv();
+
+ private Element fieldSet = DOM.createFieldSet();
+
+ private Container footer;
+
+ private ApplicationConnection client;
+
+ private RenderInformation renderInformation = new RenderInformation();
+
+ private int borderPaddingHorizontal;
+
+ private int borderPaddingVertical;
+
+ private boolean rendering = false;
+
+ public IForm() {
+ setElement(DOM.createDiv());
+ DOM.appendChild(getElement(), fieldSet);
+ setStyleName(CLASSNAME);
+ DOM.appendChild(fieldSet, legend);
+ DOM.appendChild(legend, caption);
+ DOM.setElementProperty(errorIndicatorElement, "className",
+ "i-errorindicator");
+ DOM.setStyleAttribute(errorIndicatorElement, "display", "none");
+ DOM.setInnerText(errorIndicatorElement, " "); // needed for IE
+ DOM.setElementProperty(desc, "className", "i-form-description");
+ DOM.appendChild(fieldSet, desc);
+ DOM.appendChild(fieldSet, fieldContainer);
+ errorMessage.setVisible(false);
+ errorMessage.setStyleName(CLASSNAME + "-errormessage");
+ DOM.appendChild(fieldSet, errorMessage.getElement());
+ DOM.appendChild(fieldSet, footerContainer);
+ }
+
+ public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+ rendering = true;
+ boolean measure = false;
+ if (this.client == null) {
+ this.client = client;
+ measure = true;
+ }
+
+ if (client.updateComponent(this, uidl, false)) {
+ rendering = false;
+ return;
+ }
+
+ if (measure) {
+ // Measure the border when the style names have been set
+ borderPaddingVertical = getOffsetHeight();
+ int ow = getOffsetWidth();
+ int dow = desc.getOffsetWidth();
+ borderPaddingHorizontal = ow - dow;
+ }
+
+ boolean legendEmpty = true;
+ if (uidl.hasAttribute("caption")) {
+ DOM.setInnerText(caption, uidl.getStringAttribute("caption"));
+ legendEmpty = false;
+ } else {
+ DOM.setInnerText(caption, "");
+ }
+ if (uidl.hasAttribute("icon")) {
+ if (icon == null) {
+ icon = new Icon(client);
+ DOM.insertChild(legend, icon.getElement(), 0);
+ }
+ icon.setUri(uidl.getStringAttribute("icon"));
+ legendEmpty = false;
+ } else {
+ if (icon != null) {
+ DOM.removeChild(legend, icon.getElement());
+ }
+ }
+ if (legendEmpty) {
+ DOM.setStyleAttribute(legend, "display", "none");
+ } else {
+ DOM.setStyleAttribute(legend, "display", "");
+ }
+
+ if (uidl.hasAttribute("error")) {
+ final UIDL errorUidl = uidl.getErrors();
+ errorMessage.updateFromUIDL(errorUidl);
+ errorMessage.setVisible(true);
+
+ } else {
+ errorMessage.setVisible(false);
+ }
+
+ if (uidl.hasAttribute("description")) {
+ DOM.setInnerHTML(desc, uidl.getStringAttribute("description"));
+ } else {
+ DOM.setInnerHTML(desc, "");
+ }
+
+ updateSize();
+ // TODO Check if this is needed
+ client.runDescendentsLayout(this);
+
+ final UIDL layoutUidl = uidl.getChildUIDL(0);
+ Container newLo = (Container) client.getPaintable(layoutUidl);
+ if (lo == null) {
+ lo = newLo;
+ add((Widget) lo, fieldContainer);
+ } else if (lo != newLo) {
+ client.unregisterPaintable(lo);
+ remove((Widget) lo);
+ lo = newLo;
+ add((Widget) lo, fieldContainer);
+ }
+ lo.updateFromUIDL(layoutUidl, client);
+
+ if (uidl.getChildCount() > 1) {
+ // render footer
+ Container newFooter = (Container) client.getPaintable(uidl
+ .getChildUIDL(1));
+ if (footer == null) {
+ add((Widget) newFooter, footerContainer);
+ footer = newFooter;
+ } else if (newFooter != footer) {
+ remove((Widget) footer);
+ client.unregisterPaintable(footer);
+ add((Widget) newFooter, footerContainer);
+ }
+ footer = newFooter;
+ footer.updateFromUIDL(uidl.getChildUIDL(1), client);
+ } else {
+ if (footer != null) {
+ remove((Widget) footer);
+ client.unregisterPaintable(footer);
+ }
+ }
+
+ rendering = false;
+ }
+
+ public void updateSize() {
+
+ renderInformation.updateSize(getElement());
+
+ renderInformation.setContentAreaHeight(renderInformation
+ .getRenderedSize().getHeight()
+ - borderPaddingVertical);
+ if (BrowserInfo.get().isIE6()) {
+ getElement().getStyle().setProperty("overflow", "hidden");
+ }
+ renderInformation.setContentAreaWidth(renderInformation
+ .getRenderedSize().getWidth()
+ - borderPaddingHorizontal);
+ }
+
+ public RenderSpace getAllocatedSpace(Widget child) {
+ if (child == lo) {
+ int hPixels = 0;
+ if (!"".equals(height)) {
+ hPixels = getOffsetHeight();
+ hPixels -= borderPaddingVertical;
+ hPixels -= footerContainer.getOffsetHeight();
+ hPixels -= errorMessage.getOffsetHeight();
+ hPixels -= desc.getOffsetHeight();
+
+ }
+
+ return new RenderSpace(renderInformation.getContentAreaSize()
+ .getWidth(), hPixels);
+ } else if (child == footer) {
+ return new RenderSpace(footerContainer.getOffsetWidth(), 0);
+ } else {
+ ApplicationConnection.getConsole().error(
+ "Invalid child requested RenderSpace information");
+ return null;
+ }
+ }
+
+ public boolean hasChildComponent(Widget component) {
+ return component != null && (component == lo || component == footer);
+ }
+
+ public void replaceChildComponent(Widget oldComponent, Widget newComponent) {
+ if (!hasChildComponent(oldComponent)) {
+ throw new IllegalArgumentException(
+ "Old component is not inside this Container");
+ }
+ remove(oldComponent);
+ if (oldComponent == lo) {
+ lo = (Container) newComponent;
+ add((Widget) lo, fieldContainer);
+ } else {
+ footer = (Container) newComponent;
+ add((Widget) footer, footerContainer);
+ }
+
+ }
+
+ public boolean requestLayout(Set<Paintable> child) {
+
+ if (height != null && width != null) {
+ /*
+ * If the height and width has been specified the child components
+ * cannot make the size of the layout change
+ */
+
+ return true;
+ }
+
+ if (renderInformation.updateSize(getElement())) {
+ return false;
+ } else {
+ return true;
+ }
+
+ }
+
+ public void updateCaption(Paintable component, UIDL uidl) {
+ // NOP form don't render caption for neither field layout nor footer
+ // layout
+ }
+
+ @Override
+ public void setHeight(String height) {
+ if (this.height.equals(height)) {
+ return;
+ }
+
+ this.height = height;
+ super.setHeight(height);
+
+ updateSize();
+ }
+
+ @Override
+ public void setWidth(String width) {
+ if (Util.equals(this.width, width)) {
+ return;
+ }
+
+ this.width = width;
+ super.setWidth(width);
+
+ updateSize();
+
+ if (!rendering && height.equals("")) {
+ // Width might affect height
+ Util.updateRelativeChildrenAndSendSizeUpdateEvent(client, this);
+ }
+ }
+}
diff --git a/src/com/vaadin/terminal/gwt/client/ui/IFormLayout.java b/src/com/vaadin/terminal/gwt/client/ui/IFormLayout.java
new file mode 100644
index 0000000000..6d4e62b7a8
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/IFormLayout.java
@@ -0,0 +1,464 @@
+/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui;
+
+import java.util.HashMap;
+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.ui.FlexTable;
+import com.google.gwt.user.client.ui.HTML;
+import com.google.gwt.user.client.ui.SimplePanel;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.Container;
+import com.vaadin.terminal.gwt.client.ITooltip;
+import com.vaadin.terminal.gwt.client.Paintable;
+import com.vaadin.terminal.gwt.client.RenderSpace;
+import com.vaadin.terminal.gwt.client.StyleConstants;
+import com.vaadin.terminal.gwt.client.UIDL;
+import com.vaadin.terminal.gwt.client.Util;
+
+/**
+ * Two col Layout that places caption on left col and field on right col
+ */
+public class IFormLayout extends SimplePanel implements Container {
+
+ private final static String CLASSNAME = "i-formlayout";
+
+ private ApplicationConnection client;
+ private IFormLayoutTable table;
+
+ private String width = "";
+ private String height = "";
+
+ private boolean rendering = false;
+
+ public IFormLayout() {
+ super();
+ setStylePrimaryName(CLASSNAME);
+ table = new IFormLayoutTable();
+ setWidget(table);
+ }
+
+ public class IFormLayoutTable extends FlexTable {
+
+ private static final int COLUMN_CAPTION = 0;
+ private static final int COLUMN_ERRORFLAG = 1;
+ private static final int COLUMN_WIDGET = 2;
+
+ private HashMap<Paintable, Caption> componentToCaption = new HashMap<Paintable, Caption>();
+ private HashMap<Paintable, ErrorFlag> componentToError = new HashMap<Paintable, ErrorFlag>();
+
+ public IFormLayoutTable() {
+ DOM.setElementProperty(getElement(), "cellPadding", "0");
+ DOM.setElementProperty(getElement(), "cellSpacing", "0");
+ }
+
+ public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+ final IMarginInfo margins = new IMarginInfo(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 = componentToCaption.get(p);
+ if (caption == null) {
+ caption = new Caption(p, client);
+ componentToCaption.put(p, caption);
+ }
+ ErrorFlag error = componentToError.get(p);
+ if (error == null) {
+ error = new ErrorFlag();
+ componentToError.put(p, error);
+ }
+ prepareCell(i, COLUMN_WIDGET);
+ final Paintable oldComponent = (Paintable) getWidget(i,
+ COLUMN_WIDGET);
+ if (oldComponent == null) {
+ setWidget(i, COLUMN_WIDGET, (Widget) p);
+ } else if (oldComponent != p) {
+ client.unregisterPaintable(oldComponent);
+ setWidget(i, COLUMN_WIDGET, (Widget) p);
+ }
+ getCellFormatter().setStyleName(i, COLUMN_WIDGET,
+ CLASSNAME + "-contentcell");
+ getCellFormatter().setStyleName(i, COLUMN_CAPTION,
+ CLASSNAME + "-captioncell");
+ setWidget(i, COLUMN_CAPTION, caption);
+
+ setContentWidth(i);
+
+ getCellFormatter().setStyleName(i, COLUMN_ERRORFLAG,
+ CLASSNAME + "-errorcell");
+ setWidget(i, COLUMN_ERRORFLAG, 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, COLUMN_WIDGET);
+ client.unregisterPaintable(p);
+ componentToCaption.remove(p);
+ removeRow(i);
+ }
+
+ /*
+ * Must update relative sized fields last when it is clear how much
+ * space they are allowed to use
+ */
+ for (Paintable p : componentToCaption.keySet()) {
+ client.handleComponentRelativeSize((Widget) p);
+ }
+ }
+
+ public void setContentWidths() {
+ for (int row = 0; row < getRowCount(); row++) {
+ setContentWidth(row);
+ }
+ }
+
+ private void setContentWidth(int row) {
+ String width = "";
+ if (!isDynamicWidth()) {
+ width = "100%";
+ }
+ getCellFormatter().setWidth(row, COLUMN_WIDGET, width);
+ }
+
+ public void replaceChildComponent(Widget oldComponent,
+ Widget newComponent) {
+ int i;
+ for (i = 0; i < getRowCount(); i++) {
+ Widget candidate = getWidget(i, COLUMN_WIDGET);
+ if (oldComponent == candidate) {
+ final Caption newCap = new Caption(
+ (Paintable) newComponent, client);
+ componentToCaption.put((Paintable) newComponent, newCap);
+ ErrorFlag error = componentToError.get(newComponent);
+ if (error == null) {
+ error = new ErrorFlag();
+ componentToError.put((Paintable) newComponent, error);
+ }
+
+ setWidget(i, COLUMN_CAPTION, newCap);
+ setWidget(i, COLUMN_ERRORFLAG, error);
+ setWidget(i, COLUMN_WIDGET, newComponent);
+ break;
+ }
+ }
+
+ }
+
+ public boolean hasChildComponent(Widget component) {
+ return componentToCaption.containsKey(component);
+ }
+
+ public void updateCaption(Paintable component, UIDL uidl) {
+ final Caption c = componentToCaption.get(component);
+ if (c != null) {
+ c.updateCaption(uidl);
+ }
+ final ErrorFlag e = componentToError.get(component);
+ if (e != null) {
+ e.updateFromUIDL(uidl, component);
+ }
+
+ }
+
+ public int getAllocatedWidth(Widget child, int availableWidth) {
+ Caption caption = componentToCaption.get(child);
+ ErrorFlag error = componentToError.get(child);
+ int width = availableWidth;
+
+ if (caption != null) {
+ width -= DOM.getParent(caption.getElement()).getOffsetWidth();
+ }
+ if (error != null) {
+ width -= DOM.getParent(error.getElement()).getOffsetWidth();
+ }
+
+ return width;
+ }
+
+ }
+
+ public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+ rendering = true;
+
+ this.client = client;
+
+ if (client.updateComponent(this, uidl, true)) {
+ rendering = false;
+ return;
+ }
+
+ table.updateFromUIDL(uidl, client);
+
+ rendering = false;
+ }
+
+ public boolean isDynamicWidth() {
+ return width.equals("");
+ }
+
+ public boolean hasChildComponent(Widget component) {
+ return table.hasChildComponent(component);
+ }
+
+ public void replaceChildComponent(Widget oldComponent, Widget newComponent) {
+ table.replaceChildComponent(oldComponent, newComponent);
+ }
+
+ public void updateCaption(Paintable component, UIDL uidl) {
+ table.updateCaption(component, uidl);
+ }
+
+ 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);
+ sinkEvents(ITooltip.TOOLTIP_EVENTS);
+ }
+
+ 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;
+ }
+
+ @Override
+ 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 = IFormLayout.CLASSNAME
+ + "-error-indicator";
+ Element errorIndicatorElement;
+ private Paintable owner;
+
+ public ErrorFlag() {
+ setStyleName(CLASSNAME);
+ sinkEvents(ITooltip.TOOLTIP_EVENTS);
+ }
+
+ public void updateFromUIDL(UIDL uidl, Paintable component) {
+ owner = component;
+ if (uidl.hasAttribute("error")
+ && !uidl.getBooleanAttribute("hideErrors")) {
+ 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;
+ }
+ }
+
+ @Override
+ public void onBrowserEvent(Event event) {
+ super.onBrowserEvent(event);
+ if (owner != null) {
+ client.handleTooltipEvent(event, owner);
+ }
+ }
+
+ }
+
+ public boolean requestLayout(Set<Paintable> child) {
+ if (height.equals("") || width.equals("")) {
+ // A dynamic size might change due to children changes
+ return false;
+ }
+
+ return true;
+ }
+
+ public RenderSpace getAllocatedSpace(Widget child) {
+ int width = 0;
+ int height = 0;
+
+ if (!this.width.equals("")) {
+ int availableWidth = getOffsetWidth();
+ width = table.getAllocatedWidth(child, availableWidth);
+ }
+
+ return new RenderSpace(width, height, false);
+ }
+
+ @Override
+ public void setHeight(String height) {
+ if (this.height.equals(height)) {
+ return;
+ }
+
+ this.height = height;
+ super.setHeight(height);
+ }
+
+ @Override
+ public void setWidth(String width) {
+ if (this.width.equals(width)) {
+ return;
+ }
+
+ this.width = width;
+ super.setWidth(width);
+
+ if (!rendering) {
+ table.setContentWidths();
+ if (height.equals("")) {
+ // Width might affect height
+ Util.updateRelativeChildrenAndSendSizeUpdateEvent(client, this);
+ }
+ }
+ }
+
+}
diff --git a/src/com/vaadin/terminal/gwt/client/ui/IGridLayout.java b/src/com/vaadin/terminal/gwt/client/ui/IGridLayout.java
new file mode 100644
index 0000000000..07bb7584b4
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/IGridLayout.java
@@ -0,0 +1,1018 @@
+/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
+
+import com.google.gwt.dom.client.DivElement;
+import com.google.gwt.dom.client.Document;
+import com.google.gwt.user.client.Element;
+import com.google.gwt.user.client.ui.AbsolutePanel;
+import com.google.gwt.user.client.ui.SimplePanel;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.Container;
+import com.vaadin.terminal.gwt.client.Paintable;
+import com.vaadin.terminal.gwt.client.RenderSpace;
+import com.vaadin.terminal.gwt.client.StyleConstants;
+import com.vaadin.terminal.gwt.client.UIDL;
+import com.vaadin.terminal.gwt.client.Util;
+import com.vaadin.terminal.gwt.client.ui.layout.CellBasedLayout;
+import com.vaadin.terminal.gwt.client.ui.layout.ChildComponentContainer;
+
+public class IGridLayout extends SimplePanel implements Paintable, Container {
+
+ public static final String CLASSNAME = "i-gridlayout";
+
+ private DivElement margin = Document.get().createDivElement();
+
+ private final AbsolutePanel canvas = new AbsolutePanel();
+
+ private ApplicationConnection client;
+
+ protected HashMap<Widget, ChildComponentContainer> widgetToComponentContainer = new HashMap<Widget, ChildComponentContainer>();
+
+ private HashMap<Paintable, Cell> paintableToCell = new HashMap<Paintable, Cell>();
+
+ private int spacingPixelsHorizontal;
+ private int spacingPixelsVertical;
+
+ private int[] columnWidths;
+ private int[] rowHeights;
+
+ private String height;
+
+ private String width;
+
+ private int[] colExpandRatioArray;
+
+ private int[] rowExpandRatioArray;
+
+ private int[] minColumnWidths;
+
+ private int[] minRowHeights;
+
+ private boolean rendering;
+
+ private HashMap<Widget, ChildComponentContainer> nonRenderedWidgets;
+
+ private boolean sizeChangedDuringRendering = false;
+
+ public IGridLayout() {
+ super();
+ getElement().appendChild(margin);
+ setStyleName(CLASSNAME);
+ setWidget(canvas);
+ }
+
+ @Override
+ protected Element getContainerElement() {
+ return margin.cast();
+ }
+
+ public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+ rendering = true;
+ this.client = client;
+
+ if (client.updateComponent(this, uidl, true)) {
+ rendering = false;
+ return;
+ }
+
+ boolean mightToggleVScrollBar = "".equals(height) && !"".equals(width);
+ boolean mightToggleHScrollBar = "".equals(width) && !"".equals(height);
+ int wBeforeRender = 0;
+ int hBeforeRender = 0;
+ if (mightToggleHScrollBar || mightToggleVScrollBar) {
+ wBeforeRender = canvas.getOffsetWidth();
+ hBeforeRender = getOffsetHeight();
+ }
+ canvas.setWidth("0px");
+
+ handleMargins(uidl);
+ detectSpacing(uidl);
+
+ int cols = uidl.getIntAttribute("w");
+ int rows = uidl.getIntAttribute("h");
+
+ columnWidths = new int[cols];
+ rowHeights = new int[rows];
+
+ if (cells == null) {
+ cells = new Cell[cols][rows];
+ } else if (cells.length != cols || cells[0].length != rows) {
+ Cell[][] newCells = new Cell[cols][rows];
+ for (int i = 0; i < cells.length; i++) {
+ for (int j = 0; j < cells[i].length; j++) {
+ if (i < cols && j < rows) {
+ newCells[i][j] = cells[i][j];
+ }
+ }
+ }
+ cells = newCells;
+ }
+
+ nonRenderedWidgets = (HashMap<Widget, ChildComponentContainer>) widgetToComponentContainer
+ .clone();
+
+ final int[] alignments = uidl.getIntArrayAttribute("alignments");
+ int alignmentIndex = 0;
+
+ LinkedList<Cell> pendingCells = new LinkedList<Cell>();
+
+ LinkedList<Cell> relativeHeighted = new LinkedList<Cell>();
+
+ for (final Iterator i = uidl.getChildIterator(); i.hasNext();) {
+ final UIDL r = (UIDL) i.next();
+ if ("gr".equals(r.getTag())) {
+ for (final Iterator j = r.getChildIterator(); j.hasNext();) {
+ final UIDL c = (UIDL) j.next();
+ if ("gc".equals(c.getTag())) {
+ Cell cell = getCell(c);
+ if (cell.hasContent()) {
+ boolean rendered = cell.renderIfNoRelativeWidth();
+ cell.alignment = alignments[alignmentIndex++];
+ if (!rendered) {
+ pendingCells.add(cell);
+ }
+
+ if (cell.colspan > 1) {
+ storeColSpannedCell(cell);
+ } else if (rendered) {
+ // strore non-colspanned widths to columnWidth
+ // array
+ if (columnWidths[cell.col] < cell.getWidth()) {
+ columnWidths[cell.col] = cell.getWidth();
+ }
+ }
+ if (cell.hasRelativeHeight()) {
+ relativeHeighted.add(cell);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ distributeColSpanWidths();
+ colExpandRatioArray = uidl.getIntArrayAttribute("colExpand");
+ rowExpandRatioArray = uidl.getIntArrayAttribute("rowExpand");
+
+ minColumnWidths = cloneArray(columnWidths);
+ expandColumns();
+
+ renderRemainingComponentsWithNoRelativeHeight(pendingCells);
+
+ detectRowHeights();
+
+ expandRows();
+
+ renderRemainingComponents(pendingCells);
+
+ for (Cell cell : relativeHeighted) {
+ Widget widget2 = cell.cc.getWidget();
+ client.handleComponentRelativeSize(widget2);
+ cell.cc.updateWidgetSize();
+ }
+
+ layoutCells();
+
+ // clean non rendered components
+ for (Widget w : nonRenderedWidgets.keySet()) {
+ ChildComponentContainer childComponentContainer = widgetToComponentContainer
+ .get(w);
+ paintableToCell.remove(w);
+ widgetToComponentContainer.remove(w);
+ childComponentContainer.removeFromParent();
+ client.unregisterPaintable((Paintable) w);
+ }
+ nonRenderedWidgets = null;
+
+ rendering = false;
+ sizeChangedDuringRendering = false;
+
+ boolean needsRelativeSizeCheck = false;
+
+ if (mightToggleHScrollBar && wBeforeRender != canvas.getOffsetWidth()) {
+ needsRelativeSizeCheck = true;
+ }
+ if (mightToggleVScrollBar && hBeforeRender != getOffsetHeight()) {
+ needsRelativeSizeCheck = true;
+ }
+ if (needsRelativeSizeCheck) {
+ client.handleComponentRelativeSize(this);
+ }
+ }
+
+ private static int[] cloneArray(int[] toBeCloned) {
+ int[] clone = new int[toBeCloned.length];
+ for (int i = 0; i < clone.length; i++) {
+ clone[i] = toBeCloned[i] * 1;
+ }
+ return clone;
+ }
+
+ private void expandRows() {
+ if (!"".equals(height)) {
+ int usedSpace = minRowHeights[0];
+ for (int i = 1; i < minRowHeights.length; i++) {
+ usedSpace += spacingPixelsVertical + minRowHeights[i];
+ }
+ int availableSpace = getOffsetHeight() - marginTopAndBottom;
+ int excessSpace = availableSpace - usedSpace;
+ int distributed = 0;
+ if (excessSpace > 0) {
+ for (int i = 0; i < rowHeights.length; i++) {
+ int ew = excessSpace * rowExpandRatioArray[i] / 1000;
+ rowHeights[i] = minRowHeights[i] + ew;
+ distributed += ew;
+ }
+ excessSpace -= distributed;
+ int c = 0;
+ while (excessSpace > 0) {
+ rowHeights[c % rowHeights.length]++;
+ excessSpace--;
+ c++;
+ }
+ }
+ }
+ }
+
+ @Override
+ public void setHeight(String height) {
+ super.setHeight(height);
+ if (!height.equals(this.height)) {
+ this.height = height;
+ if (rendering) {
+ sizeChangedDuringRendering = true;
+ } else {
+ expandRows();
+ layoutCells();
+ for (Paintable c : paintableToCell.keySet()) {
+ client.handleComponentRelativeSize((Widget) c);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void setWidth(String width) {
+ super.setWidth(width);
+ if (!width.equals(this.width)) {
+ this.width = width;
+ if (rendering) {
+ sizeChangedDuringRendering = true;
+ } else {
+ int[] oldWidths = cloneArray(columnWidths);
+ expandColumns();
+ boolean heightChanged = false;
+ HashSet<Integer> dirtyRows = null;
+ for (int i = 0; i < oldWidths.length; i++) {
+ if (columnWidths[i] != oldWidths[i]) {
+ Cell[] column = cells[i];
+ for (int j = 0; j < column.length; j++) {
+ Cell c = column[j];
+ if (c != null && c.cc != null
+ && c.widthCanAffectHeight()) {
+ c.cc.setContainerSize(c.getAvailableWidth(), c
+ .getAvailableHeight());
+ client.handleComponentRelativeSize(c.cc
+ .getWidget());
+ c.cc.updateWidgetSize();
+ int newHeight = c.getHeight();
+ if (columnWidths[i] < oldWidths[i]
+ && newHeight > minRowHeights[j]) {
+ minRowHeights[j] = newHeight;
+ if (newHeight > rowHeights[j]) {
+ rowHeights[j] = newHeight;
+ heightChanged = true;
+ }
+ } else if (newHeight < minRowHeights[j]) {
+ // need to recalculate new minimum height
+ // for this row
+ if (dirtyRows == null) {
+ dirtyRows = new HashSet<Integer>();
+ }
+ dirtyRows.add(j);
+ }
+ }
+ }
+ }
+ }
+ if (dirtyRows != null) {
+ /* flag indicating that there is a potential row shrinking */
+ boolean rowMayShrink = false;
+ for (Integer rowIndex : dirtyRows) {
+ int oldMinimum = minRowHeights[rowIndex];
+ int newMinimum = 0;
+ for (int colIndex = 0; colIndex < columnWidths.length; colIndex++) {
+ Cell cell = cells[colIndex][rowIndex];
+ if (cell != null && !cell.hasRelativeHeight()
+ && cell.getHeight() > newMinimum) {
+ newMinimum = cell.getHeight();
+ }
+ }
+ if (newMinimum < oldMinimum) {
+ minRowHeights[rowIndex] = rowHeights[rowIndex] = newMinimum;
+ rowMayShrink = true;
+ }
+ }
+ if (rowMayShrink) {
+ distributeRowSpanHeights();
+ minRowHeights = cloneArray(rowHeights);
+ heightChanged = true;
+ }
+
+ }
+ layoutCells();
+ for (Paintable c : paintableToCell.keySet()) {
+ client.handleComponentRelativeSize((Widget) c);
+ }
+ if (heightChanged && "".equals(height)) {
+ Util.notifyParentOfSizeChange(this, false);
+ }
+ }
+ }
+ }
+
+ private void expandColumns() {
+ if (!"".equals(width)) {
+ int usedSpace = minColumnWidths[0];
+ for (int i = 1; i < minColumnWidths.length; i++) {
+ usedSpace += spacingPixelsHorizontal + minColumnWidths[i];
+ }
+ canvas.setWidth("");
+ int availableSpace = canvas.getOffsetWidth();
+ int excessSpace = availableSpace - usedSpace;
+ int distributed = 0;
+ if (excessSpace > 0) {
+ for (int i = 0; i < columnWidths.length; i++) {
+ int ew = excessSpace * colExpandRatioArray[i] / 1000;
+ columnWidths[i] = minColumnWidths[i] + ew;
+ distributed += ew;
+ }
+ excessSpace -= distributed;
+ int c = 0;
+ while (excessSpace > 0) {
+ columnWidths[c % columnWidths.length]++;
+ excessSpace--;
+ c++;
+ }
+ }
+ }
+ }
+
+ private void layoutCells() {
+ int x = 0;
+ int y = 0;
+ for (int i = 0; i < cells.length; i++) {
+ y = 0;
+ for (int j = 0; j < cells[i].length; j++) {
+ Cell cell = cells[i][j];
+ if (cell != null) {
+ cell.layout(x, y);
+ }
+ y += rowHeights[j] + spacingPixelsVertical;
+ }
+ x += columnWidths[i] + spacingPixelsHorizontal;
+ }
+
+ if ("".equals(width)) {
+ canvas.setWidth((x - spacingPixelsHorizontal) + "px");
+ } else {
+ // main element defines width
+ canvas.setWidth("");
+ }
+ int canvasHeight;
+ if ("".equals(height)) {
+ canvasHeight = y - spacingPixelsVertical;
+ } else {
+ canvasHeight = getOffsetHeight() - marginTopAndBottom;
+ }
+ canvas.setHeight(canvasHeight + "px");
+ }
+
+ private void renderRemainingComponents(LinkedList<Cell> pendingCells) {
+ for (Cell cell : pendingCells) {
+ cell.render();
+ }
+ }
+
+ private void detectRowHeights() {
+
+ // collect min rowheight from non-rowspanned cells
+ for (int i = 0; i < cells.length; i++) {
+ for (int j = 0; j < cells[i].length; j++) {
+ Cell cell = cells[i][j];
+ if (cell != null) {
+ /*
+ * Setting fixing container width may in some situations
+ * affect height. Example: Label with wrapping text without
+ * or with relative width.
+ */
+ if (cell.cc != null && cell.widthCanAffectHeight()) {
+ cell.cc.setWidth(cell.getAvailableWidth() + "px");
+ cell.cc.updateWidgetSize();
+ }
+ if (cell.rowspan == 1) {
+ if (!cell.hasRelativeHeight()
+ && rowHeights[j] < cell.getHeight()) {
+ rowHeights[j] = cell.getHeight();
+ }
+ } else {
+ storeRowSpannedCell(cell);
+ }
+ }
+ }
+ }
+
+ distributeRowSpanHeights();
+
+ minRowHeights = cloneArray(rowHeights);
+ }
+
+ private void storeRowSpannedCell(Cell cell) {
+ SpanList l = null;
+ for (SpanList list : rowSpans) {
+ if (list.span < cell.rowspan) {
+ continue;
+ } else {
+ // insert before this
+ l = list;
+ break;
+ }
+ }
+ if (l == null) {
+ l = new SpanList(cell.rowspan);
+ rowSpans.add(l);
+ } else if (l.span != cell.rowspan) {
+ SpanList newL = new SpanList(cell.rowspan);
+ rowSpans.add(rowSpans.indexOf(l), newL);
+ l = newL;
+ }
+ l.cells.add(cell);
+ }
+
+ private void renderRemainingComponentsWithNoRelativeHeight(
+ LinkedList<Cell> pendingCells) {
+
+ for (Iterator iterator = pendingCells.iterator(); iterator.hasNext();) {
+ Cell cell = (Cell) iterator.next();
+ if (!cell.hasRelativeHeight()) {
+ cell.render();
+ iterator.remove();
+ }
+ }
+
+ }
+
+ /**
+ * Iterates colspanned cells, ensures cols have enough space to accommodate
+ * them
+ */
+ private void distributeColSpanWidths() {
+ for (SpanList list : colSpans) {
+ for (Cell cell : list.cells) {
+ int width = cell.getWidth();
+ int allocated = columnWidths[cell.col];
+ for (int i = 1; i < cell.colspan; i++) {
+ allocated += spacingPixelsHorizontal
+ + columnWidths[cell.col + i];
+ }
+ if (allocated < width) {
+ // columnWidths needs to be expanded due colspanned cell
+ int neededExtraSpace = width - allocated;
+ int spaceForColunms = neededExtraSpace / cell.colspan;
+ for (int i = 0; i < cell.colspan; i++) {
+ int col = cell.col + i;
+ columnWidths[col] += spaceForColunms;
+ neededExtraSpace -= spaceForColunms;
+ }
+ if (neededExtraSpace > 0) {
+ for (int i = 0; i < cell.colspan; i++) {
+ int col = cell.col + i;
+ columnWidths[col] += 1;
+ neededExtraSpace -= 1;
+ if (neededExtraSpace == 0) {
+ break;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Iterates rowspanned cells, ensures rows have enough space to accommodate
+ * them
+ */
+ private void distributeRowSpanHeights() {
+ for (SpanList list : rowSpans) {
+ for (Cell cell : list.cells) {
+ int height = cell.getHeight();
+ int allocated = rowHeights[cell.row];
+ for (int i = 1; i < cell.rowspan; i++) {
+ allocated += spacingPixelsVertical
+ + rowHeights[cell.row + i];
+ }
+ if (allocated < height) {
+ // columnWidths needs to be expanded due colspanned cell
+ int neededExtraSpace = height - allocated;
+ int spaceForColunms = neededExtraSpace / cell.rowspan;
+ for (int i = 0; i < cell.rowspan; i++) {
+ int row = cell.row + i;
+ rowHeights[row] += spaceForColunms;
+ neededExtraSpace -= spaceForColunms;
+ }
+ if (neededExtraSpace > 0) {
+ for (int i = 0; i < cell.rowspan; i++) {
+ int row = cell.row + i;
+ rowHeights[row] += 1;
+ neededExtraSpace -= 1;
+ if (neededExtraSpace == 0) {
+ break;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private LinkedList<SpanList> colSpans = new LinkedList<SpanList>();
+ private LinkedList<SpanList> rowSpans = new LinkedList<SpanList>();
+
+ private int marginTopAndBottom;
+
+ private class SpanList {
+ final int span;
+ List<Cell> cells = new LinkedList<Cell>();
+
+ public SpanList(int span) {
+ this.span = span;
+ }
+ }
+
+ private void storeColSpannedCell(Cell cell) {
+ SpanList l = null;
+ for (SpanList list : colSpans) {
+ if (list.span < cell.colspan) {
+ continue;
+ } else {
+ // insert before this
+ l = list;
+ break;
+ }
+ }
+ if (l == null) {
+ l = new SpanList(cell.colspan);
+ colSpans.add(l);
+ } else if (l.span != cell.colspan) {
+
+ SpanList newL = new SpanList(cell.colspan);
+ colSpans.add(colSpans.indexOf(l), newL);
+ l = newL;
+ }
+ l.cells.add(cell);
+ }
+
+ private void detectSpacing(UIDL uidl) {
+ DivElement spacingmeter = Document.get().createDivElement();
+ spacingmeter.setClassName(CLASSNAME + "-" + "spacing-"
+ + (uidl.getBooleanAttribute("spacing") ? "on" : "off"));
+ spacingmeter.getStyle().setProperty("width", "0");
+ spacingmeter.getStyle().setProperty("height", "0");
+ canvas.getElement().appendChild(spacingmeter);
+ spacingPixelsHorizontal = spacingmeter.getOffsetWidth();
+ spacingPixelsVertical = spacingmeter.getOffsetHeight();
+ canvas.getElement().removeChild(spacingmeter);
+ }
+
+ private void handleMargins(UIDL uidl) {
+ final IMarginInfo margins = new IMarginInfo(uidl
+ .getIntAttribute("margins"));
+
+ String styles = CLASSNAME + "-margin";
+ if (margins.hasTop()) {
+ styles += " " + CLASSNAME + "-" + StyleConstants.MARGIN_TOP;
+ }
+ if (margins.hasRight()) {
+ styles += " " + CLASSNAME + "-" + StyleConstants.MARGIN_RIGHT;
+ }
+ if (margins.hasBottom()) {
+ styles += " " + CLASSNAME + "-" + StyleConstants.MARGIN_BOTTOM;
+ }
+ if (margins.hasLeft()) {
+ styles += " " + CLASSNAME + "-" + StyleConstants.MARGIN_LEFT;
+ }
+ margin.setClassName(styles);
+
+ marginTopAndBottom = margin.getOffsetHeight()
+ - canvas.getOffsetHeight();
+ }
+
+ public boolean hasChildComponent(Widget component) {
+ return paintableToCell.containsKey(component);
+ }
+
+ public void replaceChildComponent(Widget oldComponent, Widget newComponent) {
+ ChildComponentContainer componentContainer = widgetToComponentContainer
+ .remove(oldComponent);
+ if (componentContainer == null) {
+ return;
+ }
+
+ componentContainer.setWidget(newComponent);
+ widgetToComponentContainer.put(newComponent, componentContainer);
+
+ paintableToCell.put((Paintable) newComponent, paintableToCell
+ .get(oldComponent));
+ }
+
+ public void updateCaption(Paintable component, UIDL uidl) {
+ ChildComponentContainer cc = widgetToComponentContainer.get(component);
+ if (cc != null) {
+ cc.updateCaption(uidl, client);
+ }
+ if (!rendering) {
+ // ensure rel size details are updated
+ paintableToCell.get(component).updateRelSizeStatus(uidl);
+ }
+ }
+
+ public boolean requestLayout(final Set<Paintable> changedChildren) {
+ boolean needsLayout = false;
+ boolean reDistributeColSpanWidths = false;
+ boolean reDistributeRowSpanHeights = false;
+ int offsetHeight = canvas.getOffsetHeight();
+ int offsetWidth = canvas.getOffsetWidth();
+ if ("".equals(width) || "".equals(height)) {
+ needsLayout = true;
+ }
+ ArrayList<Integer> dirtyColumns = new ArrayList<Integer>();
+ ArrayList<Integer> dirtyRows = new ArrayList<Integer>();
+ for (Paintable paintable : changedChildren) {
+
+ Cell cell = paintableToCell.get(paintable);
+ if (!cell.hasRelativeHeight() || !cell.hasRelativeWidth()) {
+ // cell sizes will only stay still if only relatively
+ // sized
+ // components
+ // check if changed child affects min col widths
+ cell.cc.setWidth("");
+ cell.cc.setHeight("");
+
+ cell.cc.updateWidgetSize();
+ int width = cell.getWidth();
+ int allocated = columnWidths[cell.col];
+ for (int i = 1; i < cell.colspan; i++) {
+ allocated += spacingPixelsHorizontal
+ + columnWidths[cell.col + i];
+ }
+ if (allocated < width) {
+ needsLayout = true;
+ if (cell.colspan == 1) {
+ // do simple column width expansion
+ columnWidths[cell.col] = minColumnWidths[cell.col] = width;
+ } else {
+ // mark that col span expansion is needed
+ reDistributeColSpanWidths = true;
+ }
+ } else if (allocated != width) {
+ // size is smaller thant allocated, column might
+ // shrink
+ dirtyColumns.add(cell.col);
+ }
+
+ int height = cell.getHeight();
+
+ allocated = rowHeights[cell.row];
+ for (int i = 1; i < cell.rowspan; i++) {
+ allocated += spacingPixelsVertical
+ + rowHeights[cell.row + i];
+ }
+ if (allocated < height) {
+ needsLayout = true;
+ if (cell.rowspan == 1) {
+ // do simple row expansion
+ rowHeights[cell.row] = minRowHeights[cell.row] = height;
+ } else {
+ // mark that row span expansion is needed
+ reDistributeRowSpanHeights = true;
+ }
+ } else if (allocated != height) {
+ // size is smaller than allocated, row might shrink
+ dirtyRows.add(cell.row);
+ }
+ }
+ }
+
+ if (dirtyColumns.size() > 0) {
+ for (Integer colIndex : dirtyColumns) {
+ int colW = 0;
+ for (int i = 0; i < rowHeights.length; i++) {
+ Cell cell = cells[colIndex][i];
+ if (cell != null && cell.getChildUIDL() != null
+ && !cell.hasRelativeWidth() && cell.colspan == 1) {
+ int width = cell.getWidth();
+ if (width > colW) {
+ colW = width;
+ }
+ }
+ }
+ minColumnWidths[colIndex] = colW;
+ }
+ needsLayout = true;
+ // ensure colspanned columns have enough space
+ columnWidths = cloneArray(minColumnWidths);
+ distributeColSpanWidths();
+ reDistributeColSpanWidths = false;
+ }
+
+ if (reDistributeColSpanWidths) {
+ distributeColSpanWidths();
+ }
+
+ if (dirtyRows.size() > 0) {
+ needsLayout = true;
+ for (Integer rowIndex : dirtyRows) {
+ // recalculate min row height
+ int rowH = minRowHeights[rowIndex] = 0;
+ // loop all columns on row rowIndex
+ for (int i = 0; i < columnWidths.length; i++) {
+ Cell cell = cells[i][rowIndex];
+ if (cell != null && cell.getChildUIDL() != null
+ && !cell.hasRelativeHeight() && cell.rowspan == 1) {
+ int h = cell.getHeight();
+ if (h > rowH) {
+ rowH = h;
+ }
+ }
+ }
+ minRowHeights[rowIndex] = rowH;
+ }
+ // TODO could check only some row spans
+ rowHeights = cloneArray(minRowHeights);
+ distributeRowSpanHeights();
+ reDistributeRowSpanHeights = false;
+ }
+
+ if (reDistributeRowSpanHeights) {
+ distributeRowSpanHeights();
+ }
+
+ if (needsLayout) {
+ expandColumns();
+ expandRows();
+ layoutCells();
+ // loop all relative sized components and update their size
+ for (int i = 0; i < cells.length; i++) {
+ for (int j = 0; j < cells[i].length; j++) {
+ Cell cell = cells[i][j];
+ if (cell != null
+ && cell.cc != null
+ && (cell.hasRelativeHeight() || cell
+ .hasRelativeWidth())) {
+ client.handleComponentRelativeSize(cell.cc.getWidget());
+ }
+ }
+ }
+ }
+ if (canvas.getOffsetHeight() != offsetHeight
+ || canvas.getOffsetWidth() != offsetWidth) {
+ return false;
+ } else {
+ return true;
+ }
+ }
+
+ public RenderSpace getAllocatedSpace(Widget child) {
+ Cell cell = paintableToCell.get(child);
+ assert cell != null;
+ return cell.getAllocatedSpace();
+ }
+
+ private Cell[][] cells;
+
+ /**
+ * Private helper class.
+ */
+ private class Cell {
+ private boolean relHeight = false;
+ private boolean relWidth = false;
+ private boolean widthCanAffectHeight = false;
+
+ public Cell(UIDL c) {
+ row = c.getIntAttribute("y");
+ col = c.getIntAttribute("x");
+ setUidl(c);
+ }
+
+ public boolean widthCanAffectHeight() {
+ return widthCanAffectHeight;
+ }
+
+ public boolean hasRelativeHeight() {
+ return relHeight;
+ }
+
+ public RenderSpace getAllocatedSpace() {
+ return new RenderSpace(getAvailableWidth()
+ - cc.getCaptionWidthAfterComponent(), getAvailableHeight()
+ - cc.getCaptionHeightAboveComponent());
+ }
+
+ public boolean hasContent() {
+ return childUidl != null;
+ }
+
+ /**
+ * @return total of spanned cols
+ */
+ private int getAvailableWidth() {
+ int width = columnWidths[col];
+ for (int i = 1; i < colspan; i++) {
+ width += spacingPixelsHorizontal + columnWidths[col + i];
+ }
+ return width;
+ }
+
+ /**
+ * @return total of spanned rows
+ */
+ private int getAvailableHeight() {
+ int height = rowHeights[row];
+ for (int i = 1; i < rowspan; i++) {
+ height += spacingPixelsVertical + rowHeights[row + i];
+ }
+ return height;
+ }
+
+ public void layout(int x, int y) {
+ if (cc != null && cc.isAttached()) {
+ canvas.setWidgetPosition(cc, x, y);
+ cc.setContainerSize(getAvailableWidth(), getAvailableHeight());
+ cc.setAlignment(new AlignmentInfo(alignment));
+ cc.updateAlignments(getAvailableWidth(), getAvailableHeight());
+ }
+ }
+
+ public int getWidth() {
+ if (cc != null) {
+ int w = cc.getWidgetSize().getWidth()
+ + cc.getCaptionWidthAfterComponent();
+ return w;
+ } else {
+ return 0;
+ }
+ }
+
+ public int getHeight() {
+ if (cc != null) {
+ return cc.getWidgetSize().getHeight()
+ + cc.getCaptionHeightAboveComponent();
+ } else {
+ return 0;
+ }
+ }
+
+ public boolean renderIfNoRelativeWidth() {
+ if (childUidl == null) {
+ return false;
+ }
+ if (!hasRelativeWidth()) {
+ render();
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ protected boolean hasRelativeWidth() {
+ return relWidth;
+ }
+
+ protected void render() {
+ assert childUidl != null;
+
+ Paintable paintable = client.getPaintable(childUidl);
+ assert paintable != null;
+ if (cc == null || cc.getWidget() != paintable) {
+ if (widgetToComponentContainer.containsKey(paintable)) {
+ cc = widgetToComponentContainer.get(paintable);
+ cc.setWidth("");
+ cc.setHeight("");
+ } else {
+ cc = new ChildComponentContainer((Widget) paintable,
+ CellBasedLayout.ORIENTATION_VERTICAL);
+ widgetToComponentContainer.put((Widget) paintable, cc);
+ paintableToCell.put(paintable, this);
+ cc.setWidth("");
+ canvas.add(cc, 0, 0);
+ }
+ }
+ cc.renderChild(childUidl, client, -1);
+ if (sizeChangedDuringRendering && Util.isCached(childUidl)) {
+ client.handleComponentRelativeSize(cc.getWidget());
+ }
+ cc.updateWidgetSize();
+ nonRenderedWidgets.remove(paintable);
+ }
+
+ public UIDL getChildUIDL() {
+ return childUidl;
+ }
+
+ final int row;
+ final int col;
+ int colspan = 1;
+ int rowspan = 1;
+ UIDL childUidl;
+ int alignment;
+ ChildComponentContainer cc;
+
+ public void setUidl(UIDL c) {
+ // Set cell width
+ colspan = c.hasAttribute("w") ? c.getIntAttribute("w") : 1;
+ // Set cell height
+ rowspan = c.hasAttribute("h") ? c.getIntAttribute("h") : 1;
+ // ensure we will lose reference to old cells, now overlapped by
+ // this cell
+ for (int i = 0; i < colspan; i++) {
+ for (int j = 0; j < rowspan; j++) {
+ if (i > 0 || j > 0) {
+ cells[col + i][row + j] = null;
+ }
+ }
+ }
+
+ c = c.getChildUIDL(0); // we are interested about childUidl
+ if (childUidl != null) {
+ if (c == null) {
+ // content has vanished, old content will be removed from
+ // canvas
+ // later durin render phase
+ cc = null;
+ } else if (cc != null
+ && cc.getWidget() != client.getPaintable(c)) {
+ // content has changed
+ cc = null;
+ if (widgetToComponentContainer.containsKey(client
+ .getPaintable(c))) {
+ // cc exist for this component (moved) use that for this
+ // cell
+ cc = widgetToComponentContainer.get(client
+ .getPaintable(c));
+ cc.setWidth("");
+ cc.setHeight("");
+ }
+ }
+ }
+ childUidl = c;
+ updateRelSizeStatus(c);
+ }
+
+ protected void updateRelSizeStatus(UIDL uidl) {
+ if (uidl != null && !uidl.getBooleanAttribute("cached")) {
+ if (uidl.hasAttribute("height")
+ && uidl.getStringAttribute("height").contains("%")) {
+ relHeight = true;
+ } else {
+ relHeight = false;
+ }
+ if (uidl.hasAttribute("width")) {
+ widthCanAffectHeight = relWidth = uidl.getStringAttribute(
+ "width").contains("%");
+ if (uidl.hasAttribute("height")) {
+ widthCanAffectHeight = false;
+ }
+ } else {
+ widthCanAffectHeight = !uidl.hasAttribute("height");
+ relWidth = false;
+ }
+ }
+ }
+ }
+
+ private Cell getCell(UIDL c) {
+ int row = c.getIntAttribute("y");
+ int col = c.getIntAttribute("x");
+ Cell cell = cells[col][row];
+ if (cell == null) {
+ cell = new Cell(c);
+ cells[col][row] = cell;
+ } else {
+ cell.setUidl(c);
+ }
+ return cell;
+ }
+}
diff --git a/src/com/vaadin/terminal/gwt/client/ui/IHorizontalLayout.java b/src/com/vaadin/terminal/gwt/client/ui/IHorizontalLayout.java
new file mode 100644
index 0000000000..eeb2c1d1df
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/IHorizontalLayout.java
@@ -0,0 +1,11 @@
+package com.vaadin.terminal.gwt.client.ui;
+
+public class IHorizontalLayout extends IOrderedLayout {
+
+ public static final String CLASSNAME = "i-horizontallayout";
+
+ public IHorizontalLayout() {
+ super(CLASSNAME, ORIENTATION_HORIZONTAL);
+ }
+
+}
diff --git a/src/com/vaadin/terminal/gwt/client/ui/ILabel.java b/src/com/vaadin/terminal/gwt/client/ui/ILabel.java
new file mode 100644
index 0000000000..c1ef09d8bf
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/ILabel.java
@@ -0,0 +1,123 @@
+/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui;
+
+import com.google.gwt.dom.client.Document;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.dom.client.NodeList;
+import com.google.gwt.dom.client.PreElement;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.Event;
+import com.google.gwt.user.client.ui.HTML;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.ITooltip;
+import com.vaadin.terminal.gwt.client.Paintable;
+import com.vaadin.terminal.gwt.client.UIDL;
+import com.vaadin.terminal.gwt.client.Util;
+
+public class ILabel extends HTML implements Paintable {
+
+ public static final String CLASSNAME = "i-label";
+ private static final String CLASSNAME_UNDEFINED_WIDTH = "i-label-undef-w";
+
+ private ApplicationConnection client;
+ private int verticalPaddingBorder = 0;
+ private int horizontalPaddingBorder = 0;
+
+ public ILabel() {
+ super();
+ setStyleName(CLASSNAME);
+ sinkEvents(ITooltip.TOOLTIP_EVENTS);
+ }
+
+ public ILabel(String text) {
+ super(text);
+ setStyleName(CLASSNAME);
+ sinkEvents(ITooltip.TOOLTIP_EVENTS);
+ }
+
+ @Override
+ public void onBrowserEvent(Event event) {
+ super.onBrowserEvent(event);
+ if (event.getTypeInt() == Event.ONLOAD) {
+ Util.notifyParentOfSizeChange(this, true);
+ event.cancelBubble(true);
+ return;
+ }
+ if (client != null) {
+ client.handleTooltipEvent(event, this);
+ }
+ }
+
+ public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+
+ if (client.updateComponent(this, uidl, true)) {
+ return;
+ }
+
+ this.client = client;
+
+ boolean sinkOnloads = false;
+
+ final String mode = uidl.getStringAttribute("mode");
+ if (mode == null || "text".equals(mode)) {
+ setText(uidl.getChildString(0));
+ } else if ("pre".equals(mode)) {
+ PreElement preElement = Document.get().createPreElement();
+ preElement.setInnerText(uidl.getChildUIDL(0).getChildString(0));
+ // clear existing content
+ setHTML("");
+ // add preformatted text to dom
+ getElement().appendChild(preElement);
+ } else if ("uidl".equals(mode)) {
+ setHTML(uidl.getChildrenAsXML());
+ } else if ("xhtml".equals(mode)) {
+ UIDL content = uidl.getChildUIDL(0).getChildUIDL(0);
+ if (content.getChildCount() > 0) {
+ setHTML(content.getChildString(0));
+ } else {
+ setHTML("");
+ }
+ sinkOnloads = true;
+ } else if ("xml".equals(mode)) {
+ setHTML(uidl.getChildUIDL(0).getChildString(0));
+ } else if ("raw".equals(mode)) {
+ setHTML(uidl.getChildUIDL(0).getChildString(0));
+ sinkOnloads = true;
+ } else {
+ setText("");
+ }
+ if (sinkOnloads) {
+ sinkOnloadsForContainedImgs();
+ }
+ }
+
+ private void sinkOnloadsForContainedImgs() {
+ NodeList<Element> images = getElement().getElementsByTagName("img");
+ for (int i = 0; i < images.getLength(); i++) {
+ Element img = images.getItem(i);
+ DOM.sinkEvents((com.google.gwt.user.client.Element) img,
+ Event.ONLOAD);
+ }
+
+ }
+
+ @Override
+ public void setHeight(String height) {
+ verticalPaddingBorder = Util.setHeightExcludingPaddingAndBorder(this,
+ height, verticalPaddingBorder);
+ }
+
+ @Override
+ public void setWidth(String width) {
+ horizontalPaddingBorder = Util.setWidthExcludingPaddingAndBorder(this,
+ width, horizontalPaddingBorder);
+ if (width == null || width.equals("")) {
+ setStyleName(getElement(), CLASSNAME_UNDEFINED_WIDTH, true);
+ } else {
+ setStyleName(getElement(), CLASSNAME_UNDEFINED_WIDTH, false);
+ }
+ }
+}
diff --git a/src/com/vaadin/terminal/gwt/client/ui/ILink.java b/src/com/vaadin/terminal/gwt/client/ui/ILink.java
new file mode 100644
index 0000000000..2d09c2186e
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/ILink.java
@@ -0,0 +1,182 @@
+/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.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.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.ITooltip;
+import com.vaadin.terminal.gwt.client.Paintable;
+import com.vaadin.terminal.gwt.client.UIDL;
+import com.vaadin.terminal.gwt.client.Util;
+
+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 anchor = DOM.createAnchor();
+
+ private final Element captionElement = DOM.createSpan();
+
+ private Icon icon;
+
+ private ApplicationConnection client;
+
+ public ILink() {
+ super();
+ getElement().appendChild(anchor);
+ anchor.appendChild(captionElement);
+ addClickListener(this);
+ sinkEvents(ITooltip.TOOLTIP_EVENTS);
+ 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;
+ }
+
+ this.client = client;
+
+ enabled = uidl.hasAttribute("disabled") ? false : true;
+ readonly = uidl.hasAttribute("readonly") ? true : false;
+
+ if (uidl.hasAttribute("name")) {
+ target = uidl.getStringAttribute("name");
+ anchor.setAttribute("target", target);
+ }
+ if (uidl.hasAttribute("src")) {
+ src = client.translateToolkitUri(uidl.getStringAttribute("src"));
+ anchor.setAttribute("href", 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("targetHeight") ? uidl
+ .getIntAttribute("targetHeight") : -1;
+ targetWidth = uidl.hasAttribute("targetWidth") ? uidl
+ .getIntAttribute("targetWidth") : -1;
+
+ // Set link caption
+ captionElement.setInnerText(uidl.getStringAttribute("caption"));
+
+ // handle error
+ if (uidl.hasAttribute("error")) {
+ if (errorIndicatorElement == null) {
+ errorIndicatorElement = DOM.createDiv();
+ DOM.setElementProperty(errorIndicatorElement, "className",
+ "i-errorindicator");
+ }
+ DOM.insertChild(getElement(), errorIndicatorElement, 0);
+ } else if (errorIndicatorElement != null) {
+ DOM.setStyleAttribute(errorIndicatorElement, "display", "none");
+ }
+
+ if (uidl.hasAttribute("icon")) {
+ if (icon == null) {
+ icon = new Icon(client);
+ anchor.insertBefore(icon.getElement(), captionElement);
+ }
+ icon.setUri(uidl.getStringAttribute("icon"));
+ }
+
+ }
+
+ 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;
+ }
+
+ if (features.length() > 0) {
+ // if 'special features' are set, use window.open(), unless
+ // a modifier key is held (ctrl to open in new tab etc)
+ Event e = DOM.eventGetCurrentEvent();
+ if (!e.getCtrlKey() && !e.getAltKey() && !e.getShiftKey()
+ && !e.getMetaKey()) {
+ Window.open(src, target, features);
+ e.preventDefault();
+ }
+ }
+ }
+ }
+
+ @Override
+ public void onBrowserEvent(Event event) {
+ final Element target = DOM.eventGetTarget(event);
+ if (event.getTypeInt() == Event.ONLOAD) {
+ Util.notifyParentOfSizeChange(this, true);
+ }
+ if (client != null) {
+ client.handleTooltipEvent(event, this);
+ }
+ if (target == captionElement || target == anchor
+ || (icon != null && target == icon.getElement())) {
+ super.onBrowserEvent(event);
+ }
+ if (!enabled) {
+ event.preventDefault();
+ }
+
+ }
+
+}
diff --git a/src/com/vaadin/terminal/gwt/client/ui/IListSelect.java b/src/com/vaadin/terminal/gwt/client/ui/IListSelect.java
new file mode 100644
index 0000000000..301c7aa65e
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/IListSelect.java
@@ -0,0 +1,141 @@
+/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.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.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.ITooltip;
+import com.vaadin.terminal.gwt.client.Paintable;
+import com.vaadin.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);
+ }
+
+ @Override
+ 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());
+ }
+ }
+
+ @Override
+ 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();
+ }
+
+ @Override
+ 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());
+ }
+ }
+ }
+
+ @Override
+ public void setHeight(String height) {
+ select.setHeight(height);
+ super.setHeight(height);
+ }
+
+ @Override
+ public void setWidth(String width) {
+ select.setWidth(width);
+ super.setWidth(width);
+ }
+
+ @Override
+ protected void setTabIndex(int tabIndex) {
+ ((TooltipListBox) optionsContainer).setTabIndex(tabIndex);
+ }
+
+ public void focus() {
+ select.setFocus(true);
+ }
+
+}
+
+/**
+ * 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(ITooltip.TOOLTIP_EVENTS);
+ }
+
+ public void setClient(ApplicationConnection client) {
+ this.client = client;
+ }
+
+ public void setSelect(Paintable s) {
+ pntbl = s;
+ }
+
+ @Override
+ 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/vaadin/terminal/gwt/client/ui/IMarginInfo.java b/src/com/vaadin/terminal/gwt/client/ui/IMarginInfo.java
new file mode 100644
index 0000000000..936639356c
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/IMarginInfo.java
@@ -0,0 +1,76 @@
+/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui;
+
+import java.io.Serializable;
+
+@SuppressWarnings("serial")
+public class IMarginInfo implements Serializable {
+
+ 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 IMarginInfo(int bitMask) {
+ this.bitMask = bitMask;
+ }
+
+ public IMarginInfo(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 void setMargins(IMarginInfo marginInfo) {
+ bitMask = marginInfo.bitMask;
+ }
+
+ 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;
+ }
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof IMarginInfo)) {
+ return false;
+ }
+
+ return ((IMarginInfo) obj).bitMask == bitMask;
+ }
+
+}
diff --git a/src/com/vaadin/terminal/gwt/client/ui/IMenuBar.java b/src/com/vaadin/terminal/gwt/client/ui/IMenuBar.java
new file mode 100644
index 0000000000..bb9a3d53e6
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/IMenuBar.java
@@ -0,0 +1,644 @@
+package com.vaadin.terminal.gwt.client.ui;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Stack;
+
+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.HasHTML;
+import com.google.gwt.user.client.ui.PopupListener;
+import com.google.gwt.user.client.ui.PopupPanel;
+import com.google.gwt.user.client.ui.UIObject;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.Paintable;
+import com.vaadin.terminal.gwt.client.UIDL;
+
+public class IMenuBar extends Widget implements Paintable, PopupListener {
+
+ /** 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 String submenuIcon = null;
+ protected boolean collapseItems = true;
+ protected CustomMenuItem moreItem = null;
+
+ // Construct an empty command to be used when the item has no command
+ // associated
+ protected static final Command emptyCommand = null;
+
+ /** Widget fields **/
+ protected boolean subMenu;
+ protected ArrayList<CustomMenuItem> items;
+ protected Element containerElement;
+ protected IToolkitOverlay popup;
+ protected IMenuBar visibleChildMenu;
+ protected IMenuBar parentMenu;
+ protected CustomMenuItem selected;
+
+ public IMenuBar() {
+ // Create an empty horizontal menubar
+ this(false);
+ }
+
+ public IMenuBar(boolean subMenu) {
+ super();
+ setElement(DOM.createDiv());
+
+ items = new ArrayList<CustomMenuItem>();
+ popup = null;
+ visibleChildMenu = null;
+
+ Element table = DOM.createTable();
+ Element tbody = DOM.createTBody();
+ DOM.appendChild(getElement(), table);
+ DOM.appendChild(table, tbody);
+
+ if (!subMenu) {
+ setStyleName(CLASSNAME);
+ Element tr = DOM.createTR();
+ DOM.appendChild(tbody, tr);
+ containerElement = tr;
+ } else {
+ setStyleName(CLASSNAME + "-submenu");
+ containerElement = tbody;
+ }
+ this.subMenu = subMenu;
+
+ sinkEvents(Event.ONCLICK | Event.ONMOUSEOVER | Event.ONMOUSEOUT);
+ }
+
+ /**
+ * 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 (!getItems().isEmpty()) {
+ clearItems();
+ }
+
+ UIDL options = uidl.getChildUIDL(0);
+
+ 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();
+
+ if (moreItemUIDL.hasAttribute("icon")) {
+ itemHTML.append("<img src=\""
+ + client.translateToolkitUri(moreItemUIDL
+ .getStringAttribute("icon"))
+ + "\" align=\"left\" />");
+ }
+ itemHTML.append(moreItemUIDL.getStringAttribute("text"));
+
+ moreItem = new CustomMenuItem(itemHTML.toString(), emptyCommand);
+ }
+
+ UIDL uidlItems = uidl.getChildUIDL(1);
+ Iterator<UIDL> itr = uidlItems.getChildIterator();
+ Stack<Iterator<UIDL>> iteratorStack = new Stack<Iterator<UIDL>>();
+ Stack<IMenuBar> menuStack = new Stack<IMenuBar>();
+ IMenuBar currentMenu = this;
+
+ while (itr.hasNext()) {
+ UIDL item = (UIDL) itr.next();
+ CustomMenuItem currentItem = null;
+
+ 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();
+
+ 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\" />");
+ }
+
+ Command cmd = null;
+
+ 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(), cmd);
+
+ if (item.getChildCount() > 0) {
+ menuStack.push(currentMenu);
+ iteratorStack.push(itr);
+ itr = item.getChildIterator();
+ currentMenu = new IMenuBar(true);
+ currentItem.setSubMenu(currentMenu);
+ }
+
+ while (!itr.hasNext() && !iteratorStack.empty()) {
+ itr = iteratorStack.pop();
+ currentMenu = (IMenuBar) menuStack.pop();
+ }
+ }// while
+
+ // we might need to collapse the top-level menu
+ if (collapseItems) {
+ int topLevelWidth = 0;
+
+ int ourWidth = getOffsetWidth();
+
+ int i = 0;
+ for (; i < getItems().size() && topLevelWidth < ourWidth; i++) {
+ CustomMenuItem item = (CustomMenuItem) getItems().get(i);
+ topLevelWidth += item.getOffsetWidth();
+ }
+
+ if (topLevelWidth > getOffsetWidth()) {
+ ArrayList<CustomMenuItem> toBeCollapsed = new ArrayList<CustomMenuItem>();
+ IMenuBar collapsed = new IMenuBar(true);
+ for (int j = i - 2; j < getItems().size(); j++) {
+ toBeCollapsed.add(getItems().get(j));
+ }
+
+ for (int j = 0; j < toBeCollapsed.size(); j++) {
+ CustomMenuItem item = (CustomMenuItem) toBeCollapsed.get(j);
+ removeItem(item);
+
+ // it's ugly, but we have to insert the submenu icon
+ if (item.getSubMenu() != null && submenuIcon != null) {
+ StringBuffer itemText = new StringBuffer(item.getHTML());
+ itemText.append("<img src=\"");
+ itemText.append(submenuIcon);
+ itemText.append("\" align=\"right\" />");
+ 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);
+ }
+ }
+
+ /** Widget methods **/
+
+ /**
+ * Returns a list of items in this menu
+ */
+ public List<CustomMenuItem> getItems() {
+ return items;
+ }
+
+ /**
+ * Remove all the items in this menu
+ */
+ public void clearItems() {
+ Element e = getContainingElement();
+ while (DOM.getChildCount(e) > 0) {
+ DOM.removeChild(e, DOM.getChild(e, 0));
+ }
+ items.clear();
+ }
+
+ /**
+ * Returns the containing element of the menu
+ *
+ * @return
+ */
+ public Element getContainingElement() {
+ return containerElement;
+ }
+
+ /**
+ * Returns a new child element to add an item to
+ *
+ * @return
+ */
+ public Element getNewChildElement() {
+ if (subMenu) {
+ Element tr = DOM.createTR();
+ DOM.appendChild(getContainingElement(), tr);
+ return tr;
+ } else {
+ return getContainingElement();
+ }
+
+ }
+
+ /**
+ * Add a new item to this menu
+ *
+ * @param html
+ * items text
+ * @param cmd
+ * items command
+ * @return the item created
+ */
+ public CustomMenuItem addItem(String html, Command cmd) {
+ CustomMenuItem item = new CustomMenuItem(html, cmd);
+ addItem(item);
+ return item;
+ }
+
+ /**
+ * Add a new item to this menu
+ *
+ * @param item
+ */
+ public void addItem(CustomMenuItem item) {
+ DOM.appendChild(getNewChildElement(), item.getElement());
+ item.setParentMenu(this);
+ item.setSelected(false);
+ items.add(item);
+ }
+
+ /**
+ * Remove the given item from this menu
+ *
+ * @param item
+ */
+ public void removeItem(CustomMenuItem item) {
+ if (items.contains(item)) {
+ int index = items.indexOf(item);
+ Element container = getContainingElement();
+
+ DOM.removeChild(container, DOM.getChild(container, index));
+ items.remove(index);
+ }
+ }
+
+ /*
+ * @see
+ * com.google.gwt.user.client.ui.Widget#onBrowserEvent(com.google.gwt.user
+ * .client.Event)
+ */
+ @Override
+ public void onBrowserEvent(Event e) {
+ super.onBrowserEvent(e);
+
+ Element targetElement = DOM.eventGetTarget(e);
+ CustomMenuItem targetItem = null;
+ for (int i = 0; i < items.size(); i++) {
+ CustomMenuItem item = (CustomMenuItem) items.get(i);
+ if (DOM.isOrHasChild(item.getElement(), targetElement)) {
+ targetItem = item;
+ }
+ }
+
+ if (targetItem != null) {
+ switch (DOM.eventGetType(e)) {
+
+ case Event.ONCLICK:
+ itemClick(targetItem);
+ break;
+
+ case Event.ONMOUSEOVER:
+ itemOver(targetItem);
+ break;
+
+ case Event.ONMOUSEOUT:
+ itemOut(targetItem);
+ break;
+ }
+ }
+ }
+
+ /**
+ * When an item is clicked
+ *
+ * @param item
+ */
+ public void itemClick(CustomMenuItem item) {
+ if (item.getCommand() != null) {
+ setSelected(null);
+
+ if (visibleChildMenu != null) {
+ visibleChildMenu.hideChildren();
+ }
+
+ hideParents();
+ DeferredCommand.addCommand(item.getCommand());
+
+ } else {
+ if (item.getSubMenu() != null
+ && item.getSubMenu() != visibleChildMenu) {
+ setSelected(item);
+ showChildMenu(item);
+ }
+ }
+ }
+
+ /**
+ * When the user hovers the mouse over the item
+ *
+ * @param item
+ */
+ public void itemOver(CustomMenuItem item) {
+ setSelected(item);
+
+ boolean menuWasVisible = visibleChildMenu != null;
+
+ if (menuWasVisible && visibleChildMenu != item.getSubMenu()) {
+ popup.hide();
+ visibleChildMenu = null;
+ }
+
+ if (item.getSubMenu() != null && (parentMenu != null || menuWasVisible)
+ && visibleChildMenu != item.getSubMenu()) {
+ showChildMenu(item);
+ }
+ }
+
+ /**
+ * When the mouse is moved away from an item
+ *
+ * @param item
+ */
+ public void itemOut(CustomMenuItem item) {
+ if (visibleChildMenu != item.getSubMenu() || visibleChildMenu == null) {
+ hideChildMenu(item);
+ setSelected(null);
+ }
+ }
+
+ /**
+ * Shows the child menu of an item. The caller must ensure that the item has
+ * a submenu.
+ *
+ * @param item
+ */
+ public void showChildMenu(CustomMenuItem item) {
+ popup = new IToolkitOverlay(true, false, true);
+ popup.setWidget(item.getSubMenu());
+ popup.addPopupListener(this);
+
+ if (subMenu) {
+ popup.setPopupPosition(item.getParentMenu().getAbsoluteLeft()
+ + item.getParentMenu().getOffsetWidth(), item
+ .getAbsoluteTop());
+ } else {
+ popup.setPopupPosition(item.getAbsoluteLeft(), item.getParentMenu()
+ .getAbsoluteTop()
+ + item.getParentMenu().getOffsetHeight());
+ }
+
+ item.getSubMenu().onShow();
+ visibleChildMenu = item.getSubMenu();
+ item.getSubMenu().setParentMenu(this);
+
+ popup.show();
+ }
+
+ /**
+ * Hides the submenu of an item
+ *
+ * @param item
+ */
+ public void hideChildMenu(CustomMenuItem item) {
+ if (visibleChildMenu != null
+ && !(visibleChildMenu == item.getSubMenu())) {
+ popup.hide();
+
+ }
+ }
+
+ /**
+ * When the menu is shown.
+ */
+ public void onShow() {
+ if (!items.isEmpty()) {
+ ((CustomMenuItem) items.get(0)).setSelected(true);
+ }
+ }
+
+ /**
+ * Recursively hide all child menus
+ */
+ public void hideChildren() {
+ if (visibleChildMenu != null) {
+ visibleChildMenu.hideChildren();
+ popup.hide();
+ }
+ }
+
+ /**
+ * Recursively hide all parent menus
+ */
+ public void hideParents() {
+
+ if (visibleChildMenu != null) {
+ popup.hide();
+ setSelected(null);
+ }
+
+ if (getParentMenu() != null) {
+ getParentMenu().hideParents();
+ }
+ }
+
+ /**
+ * Returns the parent menu of this menu, or null if this is the top-level
+ * menu
+ *
+ * @return
+ */
+ public IMenuBar getParentMenu() {
+ return parentMenu;
+ }
+
+ /**
+ * Set the parent menu of this menu
+ *
+ * @param parent
+ */
+ public void setParentMenu(IMenuBar parent) {
+ parentMenu = parent;
+ }
+
+ /**
+ * Returns the currently selected item of this menu, or null if nothing is
+ * selected
+ *
+ * @return
+ */
+ public CustomMenuItem getSelected() {
+ return selected;
+ }
+
+ /**
+ * Set the currently selected item of this menu
+ *
+ * @param item
+ */
+ public void setSelected(CustomMenuItem item) {
+ // If we had something selected, unselect
+ if (item != selected && selected != null) {
+ selected.setSelected(false);
+ }
+ // If we have a valid selection, select it
+ if (item != null) {
+ item.setSelected(true);
+ }
+
+ selected = item;
+ }
+
+ /**
+ * Listener method, fired when this menu is closed
+ */
+ public void onPopupClosed(PopupPanel sender, boolean autoClosed) {
+ hideChildren();
+ if (autoClosed) {
+ hideParents();
+ }
+ // setSelected(null);
+ visibleChildMenu = null;
+ popup = null;
+
+ }
+
+ /**
+ *
+ * A class to hold information on menu items
+ *
+ */
+ private class CustomMenuItem extends UIObject implements HasHTML {
+
+ protected String html = null;
+ protected Command command = null;
+ protected IMenuBar subMenu = null;
+ protected IMenuBar parentMenu = null;
+
+ public CustomMenuItem(String html, Command cmd) {
+ setElement(DOM.createTD());
+
+ setHTML(html);
+ setCommand(cmd);
+ setSelected(false);
+
+ addStyleName("menuitem");
+ }
+
+ public void setSelected(boolean selected) {
+ if (selected) {
+ addStyleDependentName("selected");
+ } else {
+ removeStyleDependentName("selected");
+ }
+ }
+
+ /*
+ * setters and getters for the fields
+ */
+
+ public void setSubMenu(IMenuBar subMenu) {
+ this.subMenu = subMenu;
+ }
+
+ public IMenuBar getSubMenu() {
+ return subMenu;
+ }
+
+ public void setParentMenu(IMenuBar parentMenu) {
+ this.parentMenu = parentMenu;
+ }
+
+ public IMenuBar getParentMenu() {
+ return parentMenu;
+ }
+
+ public void setCommand(Command command) {
+ this.command = command;
+ }
+
+ public Command getCommand() {
+ return command;
+ }
+
+ public String getHTML() {
+ return html;
+ }
+
+ public void setHTML(String html) {
+ this.html = html;
+ DOM.setInnerHTML(getElement(), html);
+ }
+
+ public String getText() {
+ return html;
+ }
+
+ public void setText(String text) {
+ setHTML(text);
+
+ }
+ }
+
+}// class IMenuBar
diff --git a/src/com/vaadin/terminal/gwt/client/ui/INativeSelect.java b/src/com/vaadin/terminal/gwt/client/ui/INativeSelect.java
new file mode 100644
index 0000000000..3b063bb5bf
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/INativeSelect.java
@@ -0,0 +1,111 @@
+/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui;
+
+import java.util.Iterator;
+import java.util.Vector;
+
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.terminal.gwt.client.BrowserInfo;
+import com.vaadin.terminal.gwt.client.UIDL;
+import com.vaadin.terminal.gwt.client.Util;
+
+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");
+
+ }
+
+ @Override
+ 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);
+ }
+ boolean selected = false;
+ 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);
+ selected = true;
+ }
+ }
+ if (!selected && !isNullSelectionAllowed()) {
+ // null-select not allowed, but value not selected yet; add null and
+ // remove when something is selected
+ select.insertItem("", null, 0);
+ select.setItemSelected(0, true);
+ }
+ if (BrowserInfo.get().isIE6()) {
+ // lazy size change - IE6 uses naive dropdown that does not have a
+ // proper size yet
+ Util.notifyParentOfSizeChange(this, true);
+ }
+ }
+
+ @Override
+ 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();
+ }
+
+ @Override
+ public void onChange(Widget sender) {
+
+ if (select.isMultipleSelect()) {
+ client.updateVariable(id, "selected", getSelectedItems(),
+ isImmediate());
+ } else {
+ client.updateVariable(id, "selected", new String[] { ""
+ + getSelectedItem() }, isImmediate());
+ }
+ if (!isNullSelectionAllowed() && "null".equals(select.getValue(0))) {
+ // remove temporary empty item
+ select.removeItem(0);
+ }
+ }
+
+ @Override
+ public void setHeight(String height) {
+ select.setHeight(height);
+ super.setHeight(height);
+ }
+
+ @Override
+ public void setWidth(String width) {
+ select.setWidth(width);
+ super.setWidth(width);
+ }
+
+ @Override
+ protected void setTabIndex(int tabIndex) {
+ ((TooltipListBox) optionsContainer).setTabIndex(tabIndex);
+ }
+
+ public void focus() {
+ select.setFocus(true);
+ }
+
+}
diff --git a/src/com/vaadin/terminal/gwt/client/ui/INotification.java b/src/com/vaadin/terminal/gwt/client/ui/INotification.java
new file mode 100644
index 0000000000..5ebb6c710e
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/INotification.java
@@ -0,0 +1,323 @@
+/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.EventObject;
+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.ui.HTML;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.terminal.gwt.client.BrowserInfo;
+
+public class INotification extends IToolkitOverlay {
+
+ public static final int CENTERED = 1;
+ public static final int CENTERED_TOP = 2;
+ public static final int CENTERED_BOTTOM = 3;
+ public static final int TOP_LEFT = 4;
+ public static final int TOP_RIGHT = 5;
+ public static final int BOTTOM_LEFT = 6;
+ public static final int BOTTOM_RIGHT = 7;
+
+ public static final int DELAY_FOREVER = -1;
+ public static final int DELAY_NONE = 0;
+
+ private static final String STYLENAME = "i-Notification";
+ private static final int mouseMoveThreshold = 7;
+ private static final int Z_INDEX_BASE = 20000;
+ public static final String STYLE_SYSTEM = "system";
+ private static final int FADE_ANIMATION_INTERVAL = 50; // == 20 fps
+
+ private int startOpacity = 90;
+ private int fadeMsec = 400;
+ private int delayMsec = 1000;
+
+ private Timer fader;
+ private Timer delay;
+
+ private int x = -1;
+ private int y = -1;
+
+ private String temporaryStyle;
+
+ private ArrayList<EventListener> listeners;
+
+ public INotification() {
+ setStylePrimaryName(STYLENAME);
+ sinkEvents(Event.ONCLICK);
+ DOM.setStyleAttribute(getElement(), "zIndex", "" + Z_INDEX_BASE);
+ }
+
+ public INotification(int delayMsec) {
+ this();
+ this.delayMsec = delayMsec;
+ }
+
+ public INotification(int delayMsec, int fadeMsec, int startOpacity) {
+ this(delayMsec);
+ this.fadeMsec = fadeMsec;
+ this.startOpacity = startOpacity;
+ }
+
+ public void startDelay() {
+ DOM.removeEventPreview(this);
+ if (delayMsec > 0) {
+ if (delay == null) {
+ delay = new Timer() {
+ @Override
+ public void run() {
+ fade();
+ }
+ };
+ delay.schedule(delayMsec);
+ }
+ } else if (delayMsec == 0) {
+ fade();
+ }
+ }
+
+ @Override
+ public void show() {
+ show(CENTERED);
+ }
+
+ public void show(String style) {
+ show(CENTERED, style);
+ }
+
+ public void show(int position) {
+ show(position, null);
+ }
+
+ public void show(Widget widget, int position, String style) {
+ setWidget(widget);
+ show(position, style);
+ }
+
+ public void show(String html, int position, String style) {
+ setWidget(new HTML(html));
+ show(position, style);
+ }
+
+ public void show(int position, String style) {
+ setOpacity(getElement(), startOpacity);
+ if (style != null) {
+ temporaryStyle = style;
+ addStyleName(style);
+ }
+ super.show();
+ setPosition(position);
+ }
+
+ @Override
+ public void hide() {
+ DOM.removeEventPreview(this);
+ cancelDelay();
+ cancelFade();
+ if (temporaryStyle != null) {
+ removeStyleName(temporaryStyle);
+ temporaryStyle = null;
+ }
+ super.hide();
+ fireEvent(new HideEvent(this));
+ }
+
+ public void fade() {
+ DOM.removeEventPreview(this);
+ cancelDelay();
+ fader = new Timer() {
+ private final long start = new Date().getTime();
+
+ @Override
+ public void run() {
+ /*
+ * To make animation smooth, don't count that event happens on
+ * time. Reduce opacity according to the actual time spent
+ * instead of fixed decrement.
+ */
+ long now = new Date().getTime();
+ long timeEplaced = now - start;
+ float remainingFraction = 1 - timeEplaced / (float) fadeMsec;
+ int opacity = (int) (startOpacity * remainingFraction);
+ if (opacity <= 0) {
+ cancel();
+ hide();
+ if (BrowserInfo.get().isOpera()) {
+ // tray notification on opera needs to explicitly define
+ // size, reset it
+ DOM.setStyleAttribute(getElement(), "width", "");
+ DOM.setStyleAttribute(getElement(), "height", "");
+ }
+ } else {
+ setOpacity(getElement(), opacity);
+ }
+ }
+ };
+ fader.scheduleRepeating(FADE_ANIMATION_INTERVAL);
+ }
+
+ public void setPosition(int position) {
+ final Element el = getElement();
+ DOM.setStyleAttribute(el, "top", "");
+ DOM.setStyleAttribute(el, "left", "");
+ DOM.setStyleAttribute(el, "bottom", "");
+ DOM.setStyleAttribute(el, "right", "");
+ switch (position) {
+ case TOP_LEFT:
+ DOM.setStyleAttribute(el, "top", "0px");
+ DOM.setStyleAttribute(el, "left", "0px");
+ break;
+ case TOP_RIGHT:
+ DOM.setStyleAttribute(el, "top", "0px");
+ DOM.setStyleAttribute(el, "right", "0px");
+ break;
+ case BOTTOM_RIGHT:
+ DOM.setStyleAttribute(el, "position", "absolute");
+ if (BrowserInfo.get().isOpera()) {
+ // tray notification on opera needs explicitly defined size
+ DOM.setStyleAttribute(el, "width", getOffsetWidth() + "px");
+ DOM.setStyleAttribute(el, "height", getOffsetHeight() + "px");
+ }
+ DOM.setStyleAttribute(el, "bottom", "0px");
+ DOM.setStyleAttribute(el, "right", "0px");
+ break;
+ case BOTTOM_LEFT:
+ DOM.setStyleAttribute(el, "bottom", "0px");
+ DOM.setStyleAttribute(el, "left", "0px");
+ break;
+ case CENTERED_TOP:
+ center();
+ DOM.setStyleAttribute(el, "top", "0px");
+ break;
+ case CENTERED_BOTTOM:
+ center();
+ DOM.setStyleAttribute(el, "top", "");
+ DOM.setStyleAttribute(el, "bottom", "0px");
+ break;
+ default:
+ case CENTERED:
+ center();
+ break;
+ }
+ }
+
+ private void cancelFade() {
+ if (fader != null) {
+ fader.cancel();
+ fader = null;
+ }
+ }
+
+ private void cancelDelay() {
+ if (delay != null) {
+ delay.cancel();
+ delay = null;
+ }
+ }
+
+ private void setOpacity(Element el, int opacity) {
+ DOM.setStyleAttribute(el, "opacity", "" + (opacity / 100.0));
+ if (BrowserInfo.get().isIE()) {
+ DOM.setStyleAttribute(el, "filter", "Alpha(opacity=" + opacity
+ + ")");
+ }
+ }
+
+ @Override
+ public void onBrowserEvent(Event event) {
+ DOM.removeEventPreview(this);
+ if (fader == null) {
+ fade();
+ }
+ }
+
+ @Override
+ public boolean onEventPreview(Event event) {
+ int type = DOM.eventGetType(event);
+ // "modal"
+ if (delayMsec == -1 || temporaryStyle == STYLE_SYSTEM) {
+ if (type == Event.ONCLICK) {
+ if (DOM.isOrHasChild(getElement(), DOM.eventGetTarget(event))) {
+ fade();
+ return false;
+ }
+ }
+ if (temporaryStyle == STYLE_SYSTEM) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+ // default
+ switch (type) {
+ case Event.ONMOUSEMOVE:
+
+ if (x < 0) {
+ x = DOM.eventGetClientX(event);
+ y = DOM.eventGetClientY(event);
+ } else if (Math.abs(DOM.eventGetClientX(event) - x) > mouseMoveThreshold
+ || Math.abs(DOM.eventGetClientY(event) - y) > mouseMoveThreshold) {
+ startDelay();
+ }
+ break;
+ case Event.ONMOUSEDOWN:
+ case Event.ONMOUSEWHEEL:
+ case Event.ONSCROLL:
+ startDelay();
+ break;
+ case Event.ONKEYDOWN:
+ if (event.getRepeat()) {
+ return true;
+ }
+ startDelay();
+ break;
+ default:
+ break;
+ }
+ return true;
+ }
+
+ public void addEventListener(EventListener listener) {
+ if (listeners == null) {
+ listeners = new ArrayList<EventListener>();
+ }
+ listeners.add(listener);
+ }
+
+ public void removeEventListener(EventListener listener) {
+ if (listeners == null) {
+ return;
+ }
+ listeners.remove(listener);
+ }
+
+ private void fireEvent(HideEvent event) {
+ if (listeners != null) {
+ for (Iterator<EventListener> it = listeners.iterator(); it
+ .hasNext();) {
+ EventListener l = it.next();
+ l.notificationHidden(event);
+ }
+ }
+ }
+
+ public class HideEvent extends EventObject {
+ private static final long serialVersionUID = 4428671753988459560L;
+
+ public HideEvent(Object source) {
+ super(source);
+ }
+ }
+
+ public interface EventListener extends java.util.EventListener {
+ public void notificationHidden(HideEvent event);
+ }
+}
diff --git a/src/com/vaadin/terminal/gwt/client/ui/IOptionGroup.java b/src/com/vaadin/terminal/gwt/client/ui/IOptionGroup.java
new file mode 100644
index 0000000000..cec3d07be8
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/IOptionGroup.java
@@ -0,0 +1,102 @@
+/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+import com.google.gwt.user.client.ui.CheckBox;
+import com.google.gwt.user.client.ui.HasFocus;
+import com.google.gwt.user.client.ui.Panel;
+import com.google.gwt.user.client.ui.RadioButton;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.terminal.gwt.client.UIDL;
+
+public class IOptionGroup extends IOptionGroupBase {
+
+ public static final String CLASSNAME = "i-select-optiongroup";
+
+ private final Panel panel;
+
+ private final Map optionsToKeys;
+
+ public IOptionGroup() {
+ super(CLASSNAME);
+ panel = (Panel) optionsContainer;
+ optionsToKeys = new HashMap();
+ }
+
+ /*
+ * Return true if no elements were changed, false otherwise.
+ */
+ @Override
+ protected void buildOptions(UIDL uidl) {
+ panel.clear();
+ for (final Iterator it = uidl.getChildIterator(); it.hasNext();) {
+ final UIDL opUidl = (UIDL) it.next();
+ CheckBox op;
+ if (isMultiselect()) {
+ op = new ICheckBox();
+ op.setText(opUidl.getStringAttribute("caption"));
+ } else {
+ op = new RadioButton(id, opUidl.getStringAttribute("caption"));
+ op.setStyleName("i-radiobutton");
+ }
+ op.addStyleName(CLASSNAME_OPTION);
+ op.setChecked(opUidl.getBooleanAttribute("selected"));
+ op.setEnabled(!opUidl.getBooleanAttribute("disabled")
+ && !isReadonly() && !isDisabled());
+ op.addClickListener(this);
+ optionsToKeys.put(op, opUidl.getStringAttribute("key"));
+ panel.add(op);
+ }
+ }
+
+ @Override
+ protected Object[] getSelectedItems() {
+ return selectedKeys.toArray();
+ }
+
+ @Override
+ public void onClick(Widget sender) {
+ super.onClick(sender);
+ if (sender instanceof CheckBox) {
+ final boolean selected = ((CheckBox) sender).isChecked();
+ final String key = (String) optionsToKeys.get(sender);
+ if (!isMultiselect()) {
+ selectedKeys.clear();
+ }
+ if (selected) {
+ selectedKeys.add(key);
+ } else {
+ selectedKeys.remove(key);
+ }
+ client.updateVariable(id, "selected", getSelectedItems(),
+ isImmediate());
+ }
+ }
+
+ @Override
+ protected void setTabIndex(int tabIndex) {
+ for (Iterator iterator = panel.iterator(); iterator.hasNext();) {
+ if (isMultiselect()) {
+ ICheckBox cb = (ICheckBox) iterator.next();
+ cb.setTabIndex(tabIndex);
+ } else {
+ RadioButton rb = (RadioButton) iterator.next();
+ rb.setTabIndex(tabIndex);
+ }
+ }
+ }
+
+ public void focus() {
+ Iterator<Widget> iterator = panel.iterator();
+ if (iterator.hasNext()) {
+ ((HasFocus) iterator.next()).setFocus(true);
+ }
+ }
+
+}
diff --git a/src/com/vaadin/terminal/gwt/client/ui/IOptionGroupBase.java b/src/com/vaadin/terminal/gwt/client/ui/IOptionGroupBase.java
new file mode 100644
index 0000000000..d91ade2639
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/IOptionGroupBase.java
@@ -0,0 +1,229 @@
+/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui;
+
+import java.util.Set;
+
+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.FlowPanel;
+import com.google.gwt.user.client.ui.KeyboardListener;
+import com.google.gwt.user.client.ui.Panel;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.Focusable;
+import com.vaadin.terminal.gwt.client.Paintable;
+import com.vaadin.terminal.gwt.client.UIDL;
+
+abstract class IOptionGroupBase extends Composite implements Paintable, Field,
+ ClickListener, ChangeListener, KeyboardListener, Focusable {
+
+ public static final String CLASSNAME_OPTION = "i-select-option";
+
+ protected ApplicationConnection client;
+
+ protected String id;
+
+ protected Set selectedKeys;
+
+ private boolean immediate;
+
+ private boolean multiselect;
+
+ private boolean disabled;
+
+ private boolean readonly;
+
+ private int cols = 0;
+
+ private int rows = 0;
+
+ private boolean nullSelectionAllowed = true;
+
+ private boolean nullSelectionItemAvailable = false;
+
+ /**
+ * Widget holding the different options (e.g. ListBox or Panel for radio
+ * buttons) (optional, fallbacks to container Panel)
+ */
+ protected Widget optionsContainer;
+
+ /**
+ * Panel containing the component
+ */
+ private final Panel container;
+
+ private ITextField newItemField;
+
+ private IButton newItemButton;
+
+ public IOptionGroupBase(String classname) {
+ container = new FlowPanel();
+ initWidget(container);
+ optionsContainer = container;
+ container.setStyleName(classname);
+ immediate = false;
+ multiselect = false;
+ }
+
+ /*
+ * Call this if you wish to specify your own container for the option
+ * elements (e.g. SELECT)
+ */
+ public IOptionGroupBase(Widget w, String classname) {
+ this(classname);
+ optionsContainer = w;
+ container.add(optionsContainer);
+ }
+
+ protected boolean isImmediate() {
+ return immediate;
+ }
+
+ protected boolean isMultiselect() {
+ return multiselect;
+ }
+
+ protected boolean isDisabled() {
+ return disabled;
+ }
+
+ protected boolean isReadonly() {
+ return readonly;
+ }
+
+ protected boolean isNullSelectionAllowed() {
+ return nullSelectionAllowed;
+ }
+
+ protected boolean isNullSelectionItemAvailable() {
+ return nullSelectionItemAvailable;
+ }
+
+ /**
+ * @return "cols" specified in uidl, 0 if not specified
+ */
+ protected int getColumns() {
+ return cols;
+ }
+
+ /**
+ * @return "rows" specified in uidl, 0 if not specified
+ */
+
+ protected int getRows() {
+ return rows;
+ }
+
+ public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+ this.client = client;
+ id = uidl.getId();
+
+ if (client.updateComponent(this, uidl, true)) {
+ return;
+ }
+
+ selectedKeys = uidl.getStringArrayVariableAsSet("selected");
+
+ readonly = uidl.getBooleanAttribute("readonly");
+ disabled = uidl.getBooleanAttribute("disabled");
+ multiselect = "multi".equals(uidl.getStringAttribute("selectmode"));
+ immediate = uidl.getBooleanAttribute("immediate");
+ nullSelectionAllowed = uidl.getBooleanAttribute("nullselect");
+ nullSelectionItemAvailable = uidl.getBooleanAttribute("nullselectitem");
+
+ cols = uidl.getIntAttribute("cols");
+ rows = uidl.getIntAttribute("rows");
+
+ final UIDL ops = uidl.getChildUIDL(0);
+
+ if (getColumns() > 0) {
+ container.setWidth(getColumns() + "em");
+ if (container != optionsContainer) {
+ optionsContainer.setWidth("100%");
+ }
+ }
+
+ buildOptions(ops);
+
+ if (uidl.getBooleanAttribute("allownewitem")) {
+ if (newItemField == null) {
+ newItemButton = new IButton();
+ newItemButton.setText("+");
+ newItemButton.setWidth("1.5em");
+ newItemButton.addClickListener(this);
+ newItemField = new ITextField();
+ newItemField.addKeyboardListener(this);
+ // newItemField.setColumns(16);
+ if (getColumns() > 0) {
+ newItemField.setWidth((getColumns() - 2) + "em");
+ }
+ }
+ newItemField.setEnabled(!disabled && !readonly);
+ newItemButton.setEnabled(!disabled && !readonly);
+
+ if (newItemField == null || newItemField.getParent() != container) {
+ container.add(newItemField);
+ container.add(newItemButton);
+ }
+ } else if (newItemField != null) {
+ container.remove(newItemField);
+ container.remove(newItemButton);
+ }
+
+ setTabIndex(uidl.hasAttribute("tabindex") ? uidl
+ .getIntAttribute("tabindex") : 0);
+
+ }
+
+ abstract protected void setTabIndex(int tabIndex);
+
+ public void onClick(Widget sender) {
+ if (sender == newItemButton && !newItemField.getText().equals("")) {
+ client.updateVariable(id, "newitem", newItemField.getText(), true);
+ newItemField.setText("");
+ }
+ }
+
+ public void onChange(Widget sender) {
+ if (multiselect) {
+ client
+ .updateVariable(id, "selected", getSelectedItems(),
+ immediate);
+ } else {
+ client.updateVariable(id, "selected", new String[] { ""
+ + getSelectedItem() }, immediate);
+ }
+ }
+
+ public void onKeyPress(Widget sender, char keyCode, int modifiers) {
+ if (sender == newItemField && keyCode == KeyboardListener.KEY_ENTER) {
+ newItemButton.click();
+ }
+ }
+
+ public void onKeyUp(Widget sender, char keyCode, int modifiers) {
+ // Ignore, subclasses may override
+ }
+
+ public void onKeyDown(Widget sender, char keyCode, int modifiers) {
+ // Ignore, subclasses may override
+ }
+
+ protected abstract void buildOptions(UIDL uidl);
+
+ protected abstract Object[] getSelectedItems();
+
+ protected Object getSelectedItem() {
+ final Object[] sel = getSelectedItems();
+ if (sel.length > 0) {
+ return sel[0];
+ } else {
+ return null;
+ }
+ }
+
+}
diff --git a/src/com/vaadin/terminal/gwt/client/ui/IOrderedLayout.java b/src/com/vaadin/terminal/gwt/client/ui/IOrderedLayout.java
new file mode 100644
index 0000000000..305b930351
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/IOrderedLayout.java
@@ -0,0 +1,876 @@
+package com.vaadin.terminal.gwt.client.ui;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.Set;
+
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.BrowserInfo;
+import com.vaadin.terminal.gwt.client.Paintable;
+import com.vaadin.terminal.gwt.client.RenderSpace;
+import com.vaadin.terminal.gwt.client.UIDL;
+import com.vaadin.terminal.gwt.client.Util;
+import com.vaadin.terminal.gwt.client.RenderInformation.FloatSize;
+import com.vaadin.terminal.gwt.client.RenderInformation.Size;
+import com.vaadin.terminal.gwt.client.ui.layout.CellBasedLayout;
+import com.vaadin.terminal.gwt.client.ui.layout.ChildComponentContainer;
+
+public class IOrderedLayout extends CellBasedLayout {
+
+ public static final String CLASSNAME = "i-orderedlayout";
+
+ private int orientation;
+
+ // Can be removed once OrderedLayout is removed
+ private boolean allowOrientationUpdate = false;
+
+ /**
+ * Size of the layout excluding any margins.
+ */
+ private Size activeLayoutSize = new Size(0, 0);
+
+ private boolean isRendering = false;
+
+ private String width = "";
+
+ private boolean sizeHasChangedDuringRendering = false;
+
+ public IOrderedLayout() {
+ this(CLASSNAME, ORIENTATION_VERTICAL);
+ allowOrientationUpdate = true;
+ }
+
+ protected IOrderedLayout(String className, int orientation) {
+ setStyleName(className);
+ this.orientation = orientation;
+
+ STYLENAME_SPACING = className + "-spacing";
+ STYLENAME_MARGIN_TOP = className + "-margin-top";
+ STYLENAME_MARGIN_RIGHT = className + "-margin-right";
+ STYLENAME_MARGIN_BOTTOM = className + "-margin-bottom";
+ STYLENAME_MARGIN_LEFT = className + "-margin-left";
+
+ }
+
+ @Override
+ public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+ isRendering = true;
+ super.updateFromUIDL(uidl, client);
+
+ // Only non-cached, visible UIDL:s can introduce changes
+ if (uidl.getBooleanAttribute("cached")
+ || uidl.getBooleanAttribute("invisible")) {
+ isRendering = false;
+ return;
+ }
+
+ if (allowOrientationUpdate) {
+ handleOrientationUpdate(uidl);
+ }
+
+ // IStopWatch w = new IStopWatch("OrderedLayout.updateFromUIDL");
+
+ ArrayList<Widget> uidlWidgets = new ArrayList<Widget>(uidl
+ .getChildCount());
+ ArrayList<ChildComponentContainer> relativeSizeComponents = new ArrayList<ChildComponentContainer>();
+ ArrayList<UIDL> relativeSizeComponentUIDL = new ArrayList<UIDL>();
+
+ int pos = 0;
+ for (final Iterator<UIDL> it = uidl.getChildIterator(); it.hasNext();) {
+ final UIDL childUIDL = it.next();
+ final Paintable child = client.getPaintable(childUIDL);
+ Widget widget = (Widget) child;
+
+ // Create container for component
+ ChildComponentContainer childComponentContainer = getComponentContainer(widget);
+
+ if (childComponentContainer == null) {
+ // This is a new component
+ childComponentContainer = createChildContainer(widget);
+ }
+
+ addOrMoveChild(childComponentContainer, pos++);
+
+ /*
+ * Components which are to be expanded in the same orientation as
+ * the layout are rendered later when it is clear how much space
+ * they can use
+ */
+ if (!Util.isCached(childUIDL)) {
+ FloatSize relativeSize = Util.parseRelativeSize(childUIDL);
+ childComponentContainer.setRelativeSize(relativeSize);
+ }
+
+ if (childComponentContainer.isComponentRelativeSized(orientation)) {
+ relativeSizeComponents.add(childComponentContainer);
+ relativeSizeComponentUIDL.add(childUIDL);
+ } else {
+ if (isDynamicWidth()) {
+ childComponentContainer.renderChild(childUIDL, client, 0);
+ } else {
+ childComponentContainer.renderChild(childUIDL, client,
+ activeLayoutSize.getWidth());
+ }
+ if (sizeHasChangedDuringRendering && Util.isCached(childUIDL)) {
+ // notify cached relative sized component about size
+ // chance
+ client.handleComponentRelativeSize(childComponentContainer
+ .getWidget());
+ }
+ }
+
+ uidlWidgets.add(widget);
+
+ }
+
+ // w.mark("Rendering of "
+ // + (uidlWidgets.size() - relativeSizeComponents.size())
+ // + " absolute size components done");
+
+ /*
+ * Remove any children after pos. These are the ones that previously
+ * were in the layout but have now been removed
+ */
+ removeChildrenAfter(pos);
+
+ // w.mark("Old children removed");
+
+ /* Fetch alignments and expand ratio from UIDL */
+ updateAlignmentsAndExpandRatios(uidl, uidlWidgets);
+ // w.mark("Alignments and expand ratios updated");
+
+ /* Fetch widget sizes from rendered components */
+ updateWidgetSizes();
+ // w.mark("Widget sizes updated");
+
+ recalculateLayout();
+ // w.mark("Layout size calculated (" + activeLayoutSize +
+ // ") offsetSize: "
+ // + getOffsetWidth() + "," + getOffsetHeight());
+
+ /* Render relative size components */
+ for (int i = 0; i < relativeSizeComponents.size(); i++) {
+ ChildComponentContainer childComponentContainer = relativeSizeComponents
+ .get(i);
+ UIDL childUIDL = relativeSizeComponentUIDL.get(i);
+
+ if (isDynamicWidth()) {
+ childComponentContainer.renderChild(childUIDL, client, 0);
+ } else {
+ childComponentContainer.renderChild(childUIDL, client,
+ activeLayoutSize.getWidth());
+ }
+
+ if (Util.isCached(childUIDL)) {
+ /*
+ * We must update the size of the relative sized component if
+ * the expand ratio or something else in the layout changes
+ * which affects the size of a relative sized component
+ */
+ client.handleComponentRelativeSize(childComponentContainer
+ .getWidget());
+ }
+
+ // childComponentContainer.updateWidgetSize();
+ }
+
+ // w.mark("Rendering of " + (relativeSizeComponents.size())
+ // + " relative size components done");
+
+ /* Fetch widget sizes for relative size components */
+ for (ChildComponentContainer childComponentContainer : widgetToComponentContainer
+ .values()) {
+
+ /* Update widget size from DOM */
+ childComponentContainer.updateWidgetSize();
+ }
+
+ // w.mark("Widget sizes updated");
+
+ /*
+ * Components with relative size in main direction may affect the layout
+ * size in the other direction
+ */
+ if ((isHorizontal() && isDynamicHeight())
+ || (isVertical() && isDynamicWidth())) {
+ layoutSizeMightHaveChanged();
+ }
+ // w.mark("Layout dimensions updated");
+
+ /* Update component spacing */
+ updateContainerMargins();
+
+ /*
+ * Update component sizes for components with relative size in non-main
+ * direction
+ */
+ if (updateRelativeSizesInNonMainDirection()) {
+ // Sizes updated - might affect the other dimension so we need to
+ // recheck the widget sizes and recalculate layout dimensions
+ updateWidgetSizes();
+ layoutSizeMightHaveChanged();
+ }
+ calculateAlignments();
+ // w.mark("recalculateComponentSizesAndAlignments done");
+
+ setRootSize();
+
+ if (BrowserInfo.get().isIE()) {
+ /*
+ * This should fix the issue with padding not always taken into
+ * account for the containers leading to no spacing between
+ * elements.
+ */
+ root.getStyle().setProperty("zoom", "1");
+ }
+
+ // w.mark("runDescendentsLayout done");
+ isRendering = false;
+ sizeHasChangedDuringRendering = false;
+ }
+
+ private void layoutSizeMightHaveChanged() {
+ Size oldSize = new Size(activeLayoutSize.getWidth(), activeLayoutSize
+ .getHeight());
+ calculateLayoutDimensions();
+
+ /*
+ * If layout dimension changes we must also update container sizes
+ */
+ if (!oldSize.equals(activeLayoutSize)) {
+ calculateContainerSize();
+ }
+ }
+
+ private void updateWidgetSizes() {
+ for (ChildComponentContainer childComponentContainer : widgetToComponentContainer
+ .values()) {
+
+ /*
+ * Update widget size from DOM
+ */
+ childComponentContainer.updateWidgetSize();
+ }
+ }
+
+ private void recalculateLayout() {
+
+ /* Calculate space for relative size components */
+ int spaceForExpansion = calculateLayoutDimensions();
+
+ if (!widgetToComponentContainer.isEmpty()) {
+ /* Divide expansion space between component containers */
+ expandComponentContainers(spaceForExpansion);
+
+ /* Update container sizes */
+ calculateContainerSize();
+ }
+
+ }
+
+ private void expandComponentContainers(int spaceForExpansion) {
+ int remaining = spaceForExpansion;
+ for (ChildComponentContainer childComponentContainer : widgetToComponentContainer
+ .values()) {
+ remaining -= childComponentContainer.expand(orientation,
+ spaceForExpansion);
+ }
+
+ if (remaining > 0) {
+ // Some left-over pixels due to rounding errors
+
+ // Add one pixel to each container until there are no pixels left
+
+ Iterator<Widget> widgetIterator = iterator();
+ while (widgetIterator.hasNext() && remaining-- > 0) {
+ ChildComponentContainer childComponentContainer = (ChildComponentContainer) widgetIterator
+ .next();
+ childComponentContainer.expandExtra(orientation, 1);
+ }
+ }
+
+ }
+
+ private void handleOrientationUpdate(UIDL uidl) {
+ int newOrientation = ORIENTATION_VERTICAL;
+ if ("horizontal".equals(uidl.getStringAttribute("orientation"))) {
+ newOrientation = ORIENTATION_HORIZONTAL;
+ }
+
+ if (orientation != newOrientation) {
+ orientation = newOrientation;
+
+ for (ChildComponentContainer childComponentContainer : widgetToComponentContainer
+ .values()) {
+ childComponentContainer.setOrientation(orientation);
+ }
+ }
+
+ }
+
+ /**
+ * Updated components with relative height in horizontal layouts and
+ * components with relative width in vertical layouts. This is only needed
+ * if the height (horizontal layout) or width (vertical layout) has not been
+ * specified.
+ */
+ private boolean updateRelativeSizesInNonMainDirection() {
+ int updateDirection = 1 - orientation;
+ if ((updateDirection == ORIENTATION_HORIZONTAL && !isDynamicWidth())
+ || (updateDirection == ORIENTATION_VERTICAL && !isDynamicHeight())) {
+ return false;
+ }
+
+ boolean updated = false;
+ for (ChildComponentContainer componentContainer : widgetToComponentContainer
+ .values()) {
+ if (componentContainer.isComponentRelativeSized(updateDirection)) {
+ client.handleComponentRelativeSize(componentContainer
+ .getWidget());
+ }
+
+ updated = true;
+ }
+
+ return updated;
+ }
+
+ private int calculateLayoutDimensions() {
+ int summedWidgetWidth = 0;
+ int summedWidgetHeight = 0;
+
+ int maxWidgetWidth = 0;
+ int maxWidgetHeight = 0;
+
+ // Calculate layout dimensions from component dimensions
+ for (ChildComponentContainer childComponentContainer : widgetToComponentContainer
+ .values()) {
+
+ int widgetHeight = 0;
+ int widgetWidth = 0;
+ if (childComponentContainer.isComponentRelativeSized(orientation)) {
+ if (orientation == ORIENTATION_HORIZONTAL) {
+ widgetHeight = getWidgetHeight(childComponentContainer);
+ } else {
+ widgetWidth = getWidgetWidth(childComponentContainer);
+ }
+ } else {
+ widgetWidth = getWidgetWidth(childComponentContainer);
+ widgetHeight = getWidgetHeight(childComponentContainer);
+ }
+
+ summedWidgetWidth += widgetWidth;
+ summedWidgetHeight += widgetHeight;
+
+ maxWidgetHeight = Math.max(maxWidgetHeight, widgetHeight);
+ maxWidgetWidth = Math.max(maxWidgetWidth, widgetWidth);
+ }
+
+ if (isHorizontal()) {
+ summedWidgetWidth += activeSpacing.hSpacing
+ * (widgetToComponentContainer.size() - 1);
+ } else {
+ summedWidgetHeight += activeSpacing.vSpacing
+ * (widgetToComponentContainer.size() - 1);
+ }
+
+ Size layoutSize = updateLayoutDimensions(summedWidgetWidth,
+ summedWidgetHeight, maxWidgetWidth, maxWidgetHeight);
+
+ int remainingSpace;
+ if (isHorizontal()) {
+ remainingSpace = layoutSize.getWidth() - summedWidgetWidth;
+ } else {
+ remainingSpace = layoutSize.getHeight() - summedWidgetHeight;
+ }
+ if (remainingSpace < 0) {
+ remainingSpace = 0;
+ }
+
+ // ApplicationConnection.getConsole().log(
+ // "Layout size: " + activeLayoutSize);
+ return remainingSpace;
+ }
+
+ private int getWidgetHeight(ChildComponentContainer childComponentContainer) {
+ Size s = childComponentContainer.getWidgetSize();
+ return s.getHeight()
+ + childComponentContainer.getCaptionHeightAboveComponent();
+ }
+
+ private int getWidgetWidth(ChildComponentContainer childComponentContainer) {
+ Size s = childComponentContainer.getWidgetSize();
+ int widgetWidth = s.getWidth()
+ + childComponentContainer.getCaptionWidthAfterComponent();
+
+ /*
+ * If the component does not have a specified size in the main direction
+ * the caption may determine the space used by the component
+ */
+ if (!childComponentContainer.widgetHasSizeSpecified(orientation)) {
+ int captionWidth = childComponentContainer
+ .getCaptionRequiredWidth();
+
+ if (captionWidth > widgetWidth) {
+ widgetWidth = captionWidth;
+ }
+ }
+
+ return widgetWidth;
+ }
+
+ private void calculateAlignments() {
+ int w = 0;
+ int h = 0;
+
+ if (isHorizontal()) {
+ // HORIZONTAL
+ h = activeLayoutSize.getHeight();
+ if (!isDynamicWidth()) {
+ w = -1;
+ }
+
+ } else {
+ // VERTICAL
+ w = activeLayoutSize.getWidth();
+ if (!isDynamicHeight()) {
+ h = -1;
+ }
+ }
+
+ for (ChildComponentContainer childComponentContainer : widgetToComponentContainer
+ .values()) {
+ childComponentContainer.updateAlignments(w, h);
+ }
+
+ }
+
+ private void calculateContainerSize() {
+
+ /*
+ * Container size here means the size the container gets from the
+ * component. The expansion size is not include in this but taken
+ * separately into account.
+ */
+ int height = 0, width = 0;
+ Iterator<Widget> widgetIterator = iterator();
+ if (isHorizontal()) {
+ height = activeLayoutSize.getHeight();
+ int availableWidth = activeLayoutSize.getWidth();
+ boolean first = true;
+ while (widgetIterator.hasNext()) {
+ ChildComponentContainer childComponentContainer = (ChildComponentContainer) widgetIterator
+ .next();
+ if (!childComponentContainer
+ .isComponentRelativeSized(ORIENTATION_HORIZONTAL)) {
+ /*
+ * Only components with non-relative size in the main
+ * direction has a container size
+ */
+ width = childComponentContainer.getWidgetSize().getWidth()
+ + childComponentContainer
+ .getCaptionWidthAfterComponent();
+
+ /*
+ * If the component does not have a specified size in the
+ * main direction the caption may determine the space used
+ * by the component
+ */
+ if (!childComponentContainer
+ .widgetHasSizeSpecified(orientation)) {
+ int captionWidth = childComponentContainer
+ .getCaptionRequiredWidth();
+ // ApplicationConnection.getConsole().log(
+ // "Component width: " + width
+ // + ", caption width: " + captionWidth);
+ if (captionWidth > width) {
+ width = captionWidth;
+ }
+ }
+ } else {
+ width = 0;
+ }
+
+ if (!isDynamicWidth()) {
+ if (availableWidth == 0) {
+ /*
+ * Let the overflowing components overflow. IE has
+ * problems with zero sizes.
+ */
+ // width = 0;
+ // height = 0;
+ } else if (width > availableWidth) {
+ width = availableWidth;
+
+ if (!first) {
+ width -= activeSpacing.hSpacing;
+ }
+ availableWidth = 0;
+ } else {
+ availableWidth -= width;
+ if (!first) {
+ availableWidth -= activeSpacing.hSpacing;
+ }
+ }
+
+ first = false;
+ }
+
+ childComponentContainer.setContainerSize(width, height);
+ }
+ } else {
+ width = activeLayoutSize.getWidth();
+ while (widgetIterator.hasNext()) {
+ ChildComponentContainer childComponentContainer = (ChildComponentContainer) widgetIterator
+ .next();
+
+ if (!childComponentContainer
+ .isComponentRelativeSized(ORIENTATION_VERTICAL)) {
+ /*
+ * Only components with non-relative size in the main
+ * direction has a container size
+ */
+ height = childComponentContainer.getWidgetSize()
+ .getHeight()
+ + childComponentContainer
+ .getCaptionHeightAboveComponent();
+ } else {
+ height = 0;
+ }
+
+ childComponentContainer.setContainerSize(width, height);
+ }
+
+ }
+
+ }
+
+ private Size updateLayoutDimensions(int totalComponentWidth,
+ int totalComponentHeight, int maxComponentWidth,
+ int maxComponentHeight) {
+
+ /* Only need to calculate dynamic dimensions */
+ if (!isDynamicHeight() && !isDynamicWidth()) {
+ return activeLayoutSize;
+ }
+
+ int activeLayoutWidth = 0;
+ int activeLayoutHeight = 0;
+
+ // Update layout dimensions
+ if (isHorizontal()) {
+ // Horizontal
+ if (isDynamicWidth()) {
+ activeLayoutWidth = totalComponentWidth;
+ }
+
+ if (isDynamicHeight()) {
+ activeLayoutHeight = maxComponentHeight;
+ }
+
+ } else {
+ // Vertical
+ if (isDynamicWidth()) {
+ activeLayoutWidth = maxComponentWidth;
+ }
+
+ if (isDynamicHeight()) {
+ activeLayoutHeight = totalComponentHeight;
+ }
+ }
+
+ if (isDynamicWidth()) {
+ setActiveLayoutWidth(activeLayoutWidth);
+ setOuterLayoutWidth(activeLayoutSize.getWidth());
+ }
+
+ if (isDynamicHeight()) {
+ setActiveLayoutHeight(activeLayoutHeight);
+ setOuterLayoutHeight(activeLayoutSize.getHeight());
+ }
+
+ return activeLayoutSize;
+ }
+
+ private void setActiveLayoutWidth(int activeLayoutWidth) {
+ if (activeLayoutWidth < 0) {
+ activeLayoutWidth = 0;
+ }
+ activeLayoutSize.setWidth(activeLayoutWidth);
+ }
+
+ private void setActiveLayoutHeight(int activeLayoutHeight) {
+ if (activeLayoutHeight < 0) {
+ activeLayoutHeight = 0;
+ }
+ activeLayoutSize.setHeight(activeLayoutHeight);
+
+ }
+
+ private void setOuterLayoutWidth(int activeLayoutWidth) {
+ super.setWidth((activeLayoutWidth + activeMargins.getHorizontal())
+ + "px");
+
+ }
+
+ private void setOuterLayoutHeight(int activeLayoutHeight) {
+ super.setHeight((activeLayoutHeight + activeMargins.getVertical())
+ + "px");
+
+ }
+
+ /**
+ * Updates the spacing between components. Needs to be done only when
+ * components are added/removed.
+ */
+ private void updateContainerMargins() {
+ ChildComponentContainer firstChildComponent = getFirstChildComponentContainer();
+ if (firstChildComponent != null) {
+ firstChildComponent.setMarginLeft(0);
+ firstChildComponent.setMarginTop(0);
+
+ for (ChildComponentContainer childComponent : widgetToComponentContainer
+ .values()) {
+ if (childComponent == firstChildComponent) {
+ continue;
+ }
+
+ if (isHorizontal()) {
+ childComponent.setMarginLeft(activeSpacing.hSpacing);
+ } else {
+ childComponent.setMarginTop(activeSpacing.vSpacing);
+ }
+ }
+ }
+ }
+
+ private boolean isHorizontal() {
+ return orientation == ORIENTATION_HORIZONTAL;
+ }
+
+ private boolean isVertical() {
+ return orientation == ORIENTATION_VERTICAL;
+ }
+
+ private ChildComponentContainer createChildContainer(Widget child) {
+
+ // Create a container DIV for the child
+ ChildComponentContainer childComponent = new ChildComponentContainer(
+ child, orientation);
+
+ return childComponent;
+
+ }
+
+ public RenderSpace getAllocatedSpace(Widget child) {
+ int width = 0;
+ int height = 0;
+ ChildComponentContainer childComponentContainer = getComponentContainer(child);
+ // WIDTH CALCULATION
+ if (isVertical()) {
+ width = activeLayoutSize.getWidth();
+ width -= childComponentContainer.getCaptionWidthAfterComponent();
+ } else if (!isDynamicWidth()) {
+ // HORIZONTAL
+ width = childComponentContainer.getContSize().getWidth();
+ width -= childComponentContainer.getCaptionWidthAfterComponent();
+ }
+
+ // HEIGHT CALCULATION
+ if (isHorizontal()) {
+ height = activeLayoutSize.getHeight();
+ height -= childComponentContainer.getCaptionHeightAboveComponent();
+ } else if (!isDynamicHeight()) {
+ // VERTICAL
+ height = childComponentContainer.getContSize().getHeight();
+ height -= childComponentContainer.getCaptionHeightAboveComponent();
+ }
+
+ // ApplicationConnection.getConsole().log(
+ // "allocatedSpace for " + Util.getSimpleName(child) + ": "
+ // + width + "," + height);
+ RenderSpace space = new RenderSpace(width, height);
+ return space;
+ }
+
+ private void recalculateLayoutAndComponentSizes() {
+ recalculateLayout();
+
+ if (!(isDynamicHeight() && isDynamicWidth())) {
+ /* First update relative sized components */
+ for (ChildComponentContainer componentContainer : widgetToComponentContainer
+ .values()) {
+ client.handleComponentRelativeSize(componentContainer
+ .getWidget());
+
+ // Update widget size from DOM
+ componentContainer.updateWidgetSize();
+ }
+ }
+
+ if (isDynamicHeight()) {
+ /*
+ * Height is not necessarily correct anymore as the height of
+ * components might have changed if the width has changed.
+ */
+
+ /*
+ * Get the new widget sizes from DOM and calculate new container
+ * sizes
+ */
+ updateWidgetSizes();
+
+ /* Update layout dimensions based on widget sizes */
+ recalculateLayout();
+ }
+
+ updateRelativeSizesInNonMainDirection();
+ calculateAlignments();
+
+ setRootSize();
+ }
+
+ private void setRootSize() {
+ root.getStyle().setPropertyPx("width", activeLayoutSize.getWidth());
+ root.getStyle().setPropertyPx("height", activeLayoutSize.getHeight());
+ }
+
+ public boolean requestLayout(Set<Paintable> children) {
+ for (Paintable p : children) {
+ /* Update widget size from DOM */
+ ChildComponentContainer componentContainer = getComponentContainer((Widget) p);
+ // This should no longer be needed (after #2563)
+ // if (isDynamicWidth()) {
+ // componentContainer.setUnlimitedContainerWidth();
+ // } else {
+ // componentContainer.setLimitedContainerWidth(activeLayoutSize
+ // .getWidth());
+ // }
+
+ componentContainer.updateWidgetSize();
+
+ /*
+ * If this is the result of an caption icon onload event the caption
+ * size may have changed
+ */
+ componentContainer.updateCaptionSize();
+ }
+
+ Size sizeBefore = new Size(activeLayoutSize.getWidth(),
+ activeLayoutSize.getHeight());
+
+ recalculateLayoutAndComponentSizes();
+ boolean sameSize = (sizeBefore.equals(activeLayoutSize));
+ if (!sameSize) {
+ /* Must inform child components about possible size updates */
+ client.runDescendentsLayout(this);
+ }
+
+ /* Automatically propagated upwards if the size has changed */
+
+ return sameSize;
+ }
+
+ @Override
+ public void setHeight(String height) {
+ Size sizeBefore = new Size(activeLayoutSize.getWidth(),
+ activeLayoutSize.getHeight());
+
+ super.setHeight(height);
+
+ if (height != null && !height.equals("")) {
+ setActiveLayoutHeight(getOffsetHeight()
+ - activeMargins.getVertical());
+ }
+
+ if (isRendering) {
+ sizeHasChangedDuringRendering = true;
+ } else {
+ recalculateLayoutAndComponentSizes();
+ boolean sameSize = (sizeBefore.equals(activeLayoutSize));
+ if (!sameSize) {
+ /* Must inform child components about possible size updates */
+ client.runDescendentsLayout(this);
+ }
+ }
+ }
+
+ @Override
+ public void setWidth(String width) {
+ if (this.width.equals(width)) {
+ return;
+ }
+ Size sizeBefore = new Size(activeLayoutSize.getWidth(),
+ activeLayoutSize.getHeight());
+
+ super.setWidth(width);
+ this.width = width;
+ if (width != null && !width.equals("")) {
+ setActiveLayoutWidth(getOffsetWidth()
+ - activeMargins.getHorizontal());
+ }
+
+ if (isRendering) {
+ sizeHasChangedDuringRendering = true;
+ } else {
+ recalculateLayoutAndComponentSizes();
+ boolean sameSize = (sizeBefore.equals(activeLayoutSize));
+ if (!sameSize) {
+ /* Must inform child components about possible size updates */
+ client.runDescendentsLayout(this);
+ }
+ /*
+ * If the height changes as a consequence of this we must inform the
+ * parent also
+ */
+ if (isDynamicHeight()
+ && sizeBefore.getHeight() != activeLayoutSize.getHeight()) {
+ Util.notifyParentOfSizeChange(this, false);
+ }
+
+ }
+ }
+
+ protected void updateAlignmentsAndExpandRatios(UIDL uidl,
+ ArrayList<Widget> renderedWidgets) {
+
+ /*
+ * UIDL contains component alignments as a comma separated list.
+ *
+ * See com.vaadin.terminal.gwt.client.ui.AlignmentInfo.java for
+ * possible values.
+ */
+ final int[] alignments = uidl.getIntArrayAttribute("alignments");
+
+ /*
+ * UIDL contains normalized expand ratios as a comma separated list.
+ */
+ final int[] expandRatios = uidl.getIntArrayAttribute("expandRatios");
+
+ for (int i = 0; i < renderedWidgets.size(); i++) {
+ Widget widget = renderedWidgets.get(i);
+
+ ChildComponentContainer container = getComponentContainer(widget);
+
+ // Calculate alignment info
+ container.setAlignment(new AlignmentInfo(alignments[i]));
+
+ // Update expand ratio
+ container.setExpandRatio(expandRatios[i]);
+ }
+ }
+
+ public void updateCaption(Paintable component, UIDL uidl) {
+ ChildComponentContainer componentContainer = getComponentContainer((Widget) component);
+ componentContainer.updateCaption(uidl, client);
+ if (!isRendering) {
+ /*
+ * This was a component-only update and the possible size change
+ * must be propagated to the layout
+ */
+ client.captionSizeUpdated(component);
+ }
+ }
+
+}
diff --git a/src/com/vaadin/terminal/gwt/client/ui/IPanel.java b/src/com/vaadin/terminal/gwt/client/ui/IPanel.java
new file mode 100644
index 0000000000..7be9a112b3
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/IPanel.java
@@ -0,0 +1,516 @@
+/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui;
+
+import java.util.Set;
+
+import com.google.gwt.dom.client.DivElement;
+import com.google.gwt.dom.client.Document;
+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.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.BrowserInfo;
+import com.vaadin.terminal.gwt.client.Container;
+import com.vaadin.terminal.gwt.client.Paintable;
+import com.vaadin.terminal.gwt.client.RenderInformation;
+import com.vaadin.terminal.gwt.client.RenderSpace;
+import com.vaadin.terminal.gwt.client.UIDL;
+import com.vaadin.terminal.gwt.client.Util;
+
+public class IPanel extends SimplePanel implements Container {
+
+ 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 String height;
+
+ private Paintable layout;
+
+ ShortcutActionHandler shortcutHandler;
+
+ private String width = "";
+
+ private Element geckoCaptionMeter;
+
+ private int scrollTop;
+
+ private int scrollLeft;
+
+ private RenderInformation renderInformation = new RenderInformation();
+
+ private int borderPaddingHorizontal = -1;
+
+ private int borderPaddingVertical = -1;
+
+ private int captionPaddingHorizontal = -1;
+
+ private int captionMarginLeft = -1;
+
+ private boolean rendering;
+
+ private int contentMarginLeft = -1;
+
+ private String previousStyleName;
+
+ public IPanel() {
+ super();
+ DivElement captionWrap = Document.get().createDivElement();
+ captionWrap.appendChild(captionNode);
+ captionNode.appendChild(captionText);
+
+ captionWrap.setClassName(CLASSNAME + "-captionwrap");
+ captionNode.setClassName(CLASSNAME + "-caption");
+ contentNode.setClassName(CLASSNAME + "-content");
+ bottomDecoration.setClassName(CLASSNAME + "-deco");
+
+ getElement().appendChild(captionWrap);
+ getElement().appendChild(contentNode);
+ getElement().appendChild(bottomDecoration);
+ setStyleName(CLASSNAME);
+ DOM.sinkEvents(getElement(), Event.ONKEYDOWN);
+ DOM.sinkEvents(contentNode, Event.ONSCROLL);
+ contentNode.getStyle().setProperty("position", "relative");
+ getElement().getStyle().setProperty("overflow", "hidden");
+ }
+
+ @Override
+ protected Element getContainerElement() {
+ return contentNode;
+ }
+
+ private void setCaption(String text) {
+ DOM.setInnerHTML(captionText, text);
+ }
+
+ public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+ rendering = true;
+ if (!uidl.hasAttribute("cached")) {
+ // Handle caption displaying and style names, prior generics.
+ // Affects size
+ // calculations
+
+ // Restore default stylenames
+ contentNode.setClassName(CLASSNAME + "-content");
+ bottomDecoration.setClassName(CLASSNAME + "-deco");
+ captionNode.setClassName(CLASSNAME + "-caption");
+ boolean hasCaption = false;
+ if (uidl.hasAttribute("caption")
+ && !uidl.getStringAttribute("caption").equals("")) {
+ setCaption(uidl.getStringAttribute("caption"));
+ hasCaption = true;
+ } else {
+ setCaption("");
+ captionNode.setClassName(CLASSNAME + "-nocaption");
+ }
+
+ // 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];
+ }
+ captionNode.setClassName(captionClass);
+ contentNode.setClassName(contentClass);
+ bottomDecoration.setClassName(decoClass);
+
+ }
+ }
+ // Ensure correct implementation
+ if (client.updateComponent(this, uidl, false)) {
+ rendering = false;
+ return;
+ }
+
+ this.client = client;
+ id = uidl.getId();
+
+ setIconUri(uidl, client);
+
+ handleError(uidl);
+
+ // 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);
+
+ runHacks(false);
+ // 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);
+ }
+
+ rendering = false;
+
+ }
+
+ @Override
+ public void setStyleName(String style) {
+ if (!style.equals(previousStyleName)) {
+ super.setStyleName(style);
+ detectContainerBorders();
+ previousStyleName = style;
+ }
+ }
+
+ private void handleError(UIDL uidl) {
+ if (uidl.hasAttribute("error")) {
+ 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);
+ } else if (errorIndicatorElement != null) {
+ DOM.removeChild(captionNode, errorIndicatorElement);
+ errorIndicatorElement = null;
+ }
+ }
+
+ 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 runHacks(boolean runGeckoFix) {
+ if (BrowserInfo.get().isIE6() && width != null && !width.equals("")) {
+ /*
+ * IE6 requires overflow-hidden elements to have a width specified
+ * so we calculate the width of the content and caption nodes when
+ * no width has been specified.
+ */
+ /*
+ * Fixes #1923 IPanel: Horizontal scrollbar does not appear in IE6
+ * with wide content
+ */
+
+ /*
+ * Caption must be shrunk for parent measurements to return correct
+ * result in IE6
+ */
+ DOM.setStyleAttribute(captionNode, "width", "1px");
+
+ int parentPadding = Util.measureHorizontalPaddingAndBorder(
+ getElement(), 0);
+
+ int parentWidthExcludingPadding = getElement().getOffsetWidth()
+ - parentPadding;
+
+ Util.setWidthExcludingPaddingAndBorder(captionNode,
+ parentWidthExcludingPadding - getCaptionMarginLeft(), 26,
+ false);
+
+ int contentMarginLeft = getContentMarginLeft();
+
+ Util.setWidthExcludingPaddingAndBorder(contentNode,
+ parentWidthExcludingPadding - contentMarginLeft, 2, false);
+
+ }
+
+ if ((BrowserInfo.get().isIE() || BrowserInfo.get().isFF2())
+ && (width == null || width.equals(""))) {
+ /*
+ * IE and FF2 needs width to be specified for the root DIV so we
+ * calculate that from the sizes of the caption and layout
+ */
+ int captionWidth = captionText.getOffsetWidth()
+ + getCaptionMarginLeft() + getCaptionPaddingHorizontal();
+ int layoutWidth = ((Widget) layout).getOffsetWidth()
+ + getContainerBorderWidth();
+ int width = layoutWidth;
+ if (captionWidth > width) {
+ width = captionWidth;
+ }
+
+ if (BrowserInfo.get().isIE7()) {
+ Util.setWidthExcludingPaddingAndBorder(captionNode, width
+ - getCaptionMarginLeft(), 26, false);
+ }
+
+ super.setWidth(width + "px");
+ }
+
+ 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", "");
+ }
+ }
+ }
+
+ client.runDescendentsLayout(this);
+
+ Util.runWebkitOverflowAutoFix(contentNode);
+
+ }
+
+ @Override
+ 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)) {
+ scrollLeft = newscrollLeft;
+ scrollTop = newscrollTop;
+ client.updateVariable(id, "scrollTop", scrollTop, false);
+ client.updateVariable(id, "scrollLeft", scrollLeft, false);
+ }
+ } else if (captionNode.isOrHasChild(target)) {
+ if (client != null) {
+ client.handleTooltipEvent(event, this);
+ }
+ }
+ }
+
+ @Override
+ public void setHeight(String height) {
+ this.height = height;
+ super.setHeight(height);
+ if (height != null && height != "") {
+ final int targetHeight = getOffsetHeight();
+ int containerHeight = targetHeight - captionNode.getOffsetHeight()
+ - bottomDecoration.getOffsetHeight()
+ - getContainerBorderHeight();
+ if (containerHeight < 0) {
+ containerHeight = 0;
+ }
+ DOM
+ .setStyleAttribute(contentNode, "height", containerHeight
+ + "px");
+ } else {
+ DOM.setStyleAttribute(contentNode, "height", "");
+ }
+ if (!rendering) {
+ runHacks(true);
+ }
+ }
+
+ private int getCaptionMarginLeft() {
+ if (captionMarginLeft < 0) {
+ detectContainerBorders();
+ }
+ return captionMarginLeft;
+ }
+
+ private int getContentMarginLeft() {
+ if (contentMarginLeft < 0) {
+ detectContainerBorders();
+ }
+ return contentMarginLeft;
+ }
+
+ private int getCaptionPaddingHorizontal() {
+ if (captionPaddingHorizontal < 0) {
+ detectContainerBorders();
+ }
+ return captionPaddingHorizontal;
+ }
+
+ private int getContainerBorderHeight() {
+ if (borderPaddingVertical < 0) {
+ detectContainerBorders();
+ }
+ return borderPaddingVertical;
+ }
+
+ @Override
+ public void setWidth(String width) {
+ if (this.width.equals(width)) {
+ return;
+ }
+
+ this.width = width;
+ super.setWidth(width);
+ if (!rendering) {
+ runHacks(true);
+
+ if (height.equals("")) {
+ // Width change may affect height
+ Util.updateRelativeChildrenAndSendSizeUpdateEvent(client, this);
+ }
+
+ }
+ }
+
+ private int getContainerBorderWidth() {
+ if (borderPaddingHorizontal < 0) {
+ detectContainerBorders();
+ }
+ return borderPaddingHorizontal;
+ }
+
+ private void detectContainerBorders() {
+ DOM.setStyleAttribute(contentNode, "overflow", "hidden");
+
+ borderPaddingHorizontal = Util.measureHorizontalBorder(contentNode);
+ borderPaddingVertical = Util.measureVerticalBorder(contentNode);
+
+ DOM.setStyleAttribute(contentNode, "overflow", "auto");
+
+ captionPaddingHorizontal = Util.measureHorizontalPaddingAndBorder(
+ captionNode, 26);
+
+ captionMarginLeft = Util.measureMarginLeft(captionNode);
+ contentMarginLeft = Util.measureMarginLeft(contentNode);
+
+ }
+
+ public boolean hasChildComponent(Widget component) {
+ if (component != null && component == layout) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ public void replaceChildComponent(Widget oldComponent, Widget newComponent) {
+ // TODO This is untested as no layouts require this
+ if (oldComponent != layout) {
+ return;
+ }
+
+ setWidget(newComponent);
+ layout = (Paintable) newComponent;
+ }
+
+ public RenderSpace getAllocatedSpace(Widget child) {
+ int w = 0;
+ int h = 0;
+
+ if (width != null && !width.equals("")) {
+ w = getOffsetWidth() - getContainerBorderWidth();
+ if (w < 0) {
+ w = 0;
+ }
+ }
+
+ if (height != null && !height.equals("")) {
+ h = contentNode.getOffsetHeight() - getContainerBorderHeight();
+ if (h < 0) {
+ h = 0;
+ }
+ }
+
+ return new RenderSpace(w, h, true);
+ }
+
+ public boolean requestLayout(Set<Paintable> child) {
+ if (height != null && height != "" && width != null && width != "") {
+ /*
+ * If the height and width has been specified the child components
+ * cannot make the size of the layout change
+ */
+ return true;
+ }
+ runHacks(false);
+ return !renderInformation.updateSize(getElement());
+ }
+
+ public void updateCaption(Paintable component, UIDL uidl) {
+ // NOP: layouts caption, errors etc not rendered in Panel
+ }
+
+ @Override
+ protected void onAttach() {
+ super.onAttach();
+ detectContainerBorders();
+ }
+
+}
diff --git a/src/com/vaadin/terminal/gwt/client/ui/IPasswordField.java b/src/com/vaadin/terminal/gwt/client/ui/IPasswordField.java
new file mode 100644
index 0000000000..569076e353
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/IPasswordField.java
@@ -0,0 +1,21 @@
+/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui;
+
+import com.google.gwt.user.client.DOM;
+
+/**
+ * This class represents a password field.
+ *
+ * @author IT Mill Ltd.
+ *
+ */
+public class IPasswordField extends ITextField {
+
+ public IPasswordField() {
+ super(DOM.createInputPassword());
+ }
+
+}
diff --git a/src/com/vaadin/terminal/gwt/client/ui/IPopupCalendar.java b/src/com/vaadin/terminal/gwt/client/ui/IPopupCalendar.java
new file mode 100644
index 0000000000..0b876f1cb2
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/IPopupCalendar.java
@@ -0,0 +1,130 @@
+/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui;
+
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.Timer;
+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.PopupListener;
+import com.google.gwt.user.client.ui.PopupPanel;
+import com.google.gwt.user.client.ui.Widget;
+import com.google.gwt.user.client.ui.PopupPanel.PositionCallback;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.Paintable;
+import com.vaadin.terminal.gwt.client.UIDL;
+
+public class IPopupCalendar extends ITextualDate implements Paintable, Field,
+ ClickListener, PopupListener {
+
+ private final Button calendarToggle;
+
+ private final ICalendarPanel calendar;
+
+ private final IToolkitOverlay popup;
+ private boolean open = false;
+
+ public IPopupCalendar() {
+ super();
+
+ calendarToggle = new Button();
+ calendarToggle.setStyleName(CLASSNAME + "-button");
+ calendarToggle.setText("");
+ calendarToggle.addClickListener(this);
+ add(calendarToggle);
+
+ calendar = new ICalendarPanel(this);
+ popup = new IToolkitOverlay(true, true, true);
+ popup.setStyleName(IDateField.CLASSNAME + "-popup");
+ popup.setWidget(calendar);
+ popup.addPopupListener(this);
+
+ DOM.setElementProperty(calendar.getElement(), "id",
+ "PID_TOOLKIT_POPUPCAL");
+
+ }
+
+ @Override
+ public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+ super.updateFromUIDL(uidl, client);
+ if (date != null) {
+ calendar.updateCalendar();
+ }
+ calendarToggle.setEnabled(enabled);
+ }
+
+ public void onClick(Widget sender) {
+ if (sender == calendarToggle && !open) {
+ open = true;
+ calendar.updateCalendar();
+ // clear previous values
+ popup.setWidth("");
+ popup.setHeight("");
+ popup.setPopupPositionAndShow(new PositionCallback() {
+ public void setPosition(int offsetWidth, int offsetHeight) {
+ final int w = offsetWidth;
+ final int h = offsetHeight;
+ int t = calendarToggle.getAbsoluteTop();
+ int l = calendarToggle.getAbsoluteLeft();
+ if (l + w > Window.getClientWidth()
+ + Window.getScrollLeft()) {
+ l = Window.getClientWidth() + Window.getScrollLeft()
+ - w;
+ }
+ if (t + h + calendarToggle.getOffsetHeight() + 30 > Window
+ .getClientHeight()
+ + Window.getScrollTop()) {
+ t = Window.getClientHeight() + Window.getScrollTop()
+ - h - calendarToggle.getOffsetHeight() - 30;
+ l += calendarToggle.getOffsetWidth();
+ }
+
+ // fix size
+ popup.setWidth(w + "px");
+ popup.setHeight(h + "px");
+
+ popup.setPopupPosition(l, t
+ + calendarToggle.getOffsetHeight() + 2);
+
+ setFocus(true);
+ }
+ });
+ }
+ }
+
+ public void onPopupClosed(PopupPanel sender, boolean autoClosed) {
+ if (sender == popup) {
+ buildDate();
+ // Sigh.
+ Timer t = new Timer() {
+ @Override
+ public void run() {
+ open = false;
+ }
+ };
+ t.schedule(100);
+ }
+ }
+
+ /**
+ * Sets focus to Calendar panel.
+ *
+ * @param focus
+ */
+ public void setFocus(boolean focus) {
+ calendar.setFocus(focus);
+ }
+
+ @Override
+ protected int getFieldExtraWidth() {
+ if (fieldExtraWidth < 0) {
+ fieldExtraWidth = super.getFieldExtraWidth();
+ fieldExtraWidth += calendarToggle.getOffsetWidth();
+ }
+ return fieldExtraWidth;
+ }
+
+}
diff --git a/src/com/vaadin/terminal/gwt/client/ui/IPopupView.java b/src/com/vaadin/terminal/gwt/client/ui/IPopupView.java
new file mode 100644
index 0000000000..8e89b451c4
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/IPopupView.java
@@ -0,0 +1,417 @@
+package com.vaadin.terminal.gwt.client.ui;
+
+import java.util.HashSet;
+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.ui.ClickListener;
+import com.google.gwt.user.client.ui.HTML;
+import com.google.gwt.user.client.ui.HasFocus;
+import com.google.gwt.user.client.ui.Label;
+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.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.Container;
+import com.vaadin.terminal.gwt.client.ICaption;
+import com.vaadin.terminal.gwt.client.ICaptionWrapper;
+import com.vaadin.terminal.gwt.client.ITooltip;
+import com.vaadin.terminal.gwt.client.Paintable;
+import com.vaadin.terminal.gwt.client.RenderSpace;
+import com.vaadin.terminal.gwt.client.UIDL;
+import com.vaadin.terminal.gwt.client.Util;
+import com.vaadin.terminal.gwt.client.RenderInformation.Size;
+
+public class IPopupView extends HTML implements Container {
+
+ public static final String CLASSNAME = "i-popupview";
+
+ /** For server-client communication */
+ private String uidlId;
+ private ApplicationConnection client;
+
+ /** This variable helps to communicate popup visibility to the server */
+ private boolean hostPopupVisible;
+
+ private final CustomPopup popup;
+ private final Label loading = new Label("Loading...");
+
+ /**
+ * loading constructor
+ */
+ public IPopupView() {
+ super();
+ popup = new CustomPopup();
+
+ setStyleName(CLASSNAME);
+ popup.setStylePrimaryName(CLASSNAME + "-popup");
+
+ setHTML("(No HTML defined for PopupView)");
+ popup.setWidget(loading);
+
+ // When we click to open the popup...
+ addClickListener(new ClickListener() {
+ public void onClick(Widget sender) {
+ updateState(true);
+ }
+ });
+
+ // ..and when we close it
+ popup.addPopupListener(new PopupListener() {
+ public void onPopupClosed(PopupPanel sender, boolean autoClosed) {
+ updateState(false);
+ }
+ });
+
+ popup.setAnimationEnabled(true);
+ sinkEvents(ITooltip.TOOLTIP_EVENTS);
+ }
+
+ /**
+ *
+ *
+ * @see com.vaadin.terminal.gwt.client.Paintable#updateFromUIDL(com.vaadin.terminal.gwt.client.UIDL,
+ * com.vaadin.terminal.gwt.client.ApplicationConnection)
+ */
+ public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+ // This call should be made first. Ensure correct implementation,
+ // and don't let the containing layout manage caption.
+ if (client.updateComponent(this, uidl, false)) {
+ return;
+ }
+ // These are for future server connections
+ this.client = client;
+ uidlId = uidl.getId();
+
+ hostPopupVisible = uidl.getBooleanVariable("popupVisibility");
+
+ setHTML(uidl.getStringAttribute("html"));
+
+ if (uidl.hasAttribute("hideOnMouseOut")) {
+ popup.setHideOnMouseOut(uidl.getBooleanAttribute("hideOnMouseOut"));
+ }
+
+ // Render the popup if visible and show it.
+ if (hostPopupVisible) {
+ UIDL popupUIDL = uidl.getChildUIDL(0);
+
+ // showPopupOnTop(popup, hostReference);
+ preparePopup(popup);
+ popup.updateFromUIDL(popupUIDL, client);
+ if (uidl.hasAttribute("style")) {
+ final String[] styles = uidl.getStringAttribute("style").split(
+ " ");
+ final StringBuffer styleBuf = new StringBuffer();
+ final String primaryName = popup.getStylePrimaryName();
+ styleBuf.append(primaryName);
+ for (int i = 0; i < styles.length; i++) {
+ styleBuf.append(" ");
+ styleBuf.append(primaryName);
+ styleBuf.append("-");
+ styleBuf.append(styles[i]);
+ }
+ popup.setStyleName(styleBuf.toString());
+ } else {
+ popup.setStyleName(popup.getStylePrimaryName());
+ }
+ showPopup(popup);
+
+ // The popup shouldn't be visible, try to hide it.
+ } else {
+ popup.hide();
+ }
+ }// updateFromUIDL
+
+ /**
+ * Update popup visibility to server
+ *
+ * @param visibility
+ */
+ private void updateState(boolean visible) {
+ // If we know the server connection
+ // then update the current situation
+ if (uidlId != null && client != null && isAttached()) {
+ client.updateVariable(uidlId, "popupVisibility", visible, true);
+ }
+ }
+
+ private void preparePopup(final CustomPopup popup) {
+ popup.setVisible(false);
+ popup.show();
+ }
+
+ private void showPopup(final CustomPopup popup) {
+ int windowTop = RootPanel.get().getAbsoluteTop();
+ int windowLeft = RootPanel.get().getAbsoluteLeft();
+ int windowRight = windowLeft + RootPanel.get().getOffsetWidth();
+ int windowBottom = windowTop + RootPanel.get().getOffsetHeight();
+
+ int offsetWidth = popup.getOffsetWidth();
+ int offsetHeight = popup.getOffsetHeight();
+
+ int hostHorizontalCenter = IPopupView.this.getAbsoluteLeft()
+ + IPopupView.this.getOffsetWidth() / 2;
+ int hostVerticalCenter = IPopupView.this.getAbsoluteTop()
+ + IPopupView.this.getOffsetHeight() / 2;
+
+ int left = hostHorizontalCenter - offsetWidth / 2;
+ int top = hostVerticalCenter - offsetHeight / 2;
+
+ // Superclass takes care of top and left
+ if ((left + offsetWidth) > windowRight) {
+ left -= (left + offsetWidth) - windowRight;
+ }
+
+ if ((top + offsetHeight) > windowBottom) {
+ top -= (top + offsetHeight) - windowBottom;
+ }
+
+ popup.setPopupPosition(left, top);
+
+ popup.setVisible(true);
+ }
+
+ /**
+ * Make sure that we remove the popup when the main widget is removed.
+ *
+ * @see com.google.gwt.user.client.ui.Widget#onUnload()
+ */
+ @Override
+ protected void onDetach() {
+ popup.hide();
+ super.onDetach();
+ }
+
+ private static native void nativeBlur(Element e)
+ /*-{
+ if(e && e.blur) {
+ e.blur();
+ }
+ }-*/;
+
+ private class CustomPopup extends IToolkitOverlay {
+
+ private Paintable popupComponentPaintable = null;
+ private Widget popupComponentWidget = null;
+ private ICaptionWrapper captionWrapper = null;
+
+ private boolean hasHadMouseOver = false;
+ private boolean hideOnMouseOut = true;
+ private final Set<Element> activeChildren = new HashSet<Element>();
+ private boolean hiding = false;
+
+ public CustomPopup() {
+ super(true, false, true); // autoHide, not modal, dropshadow
+ }
+
+ // For some reason ONMOUSEOUT events are not always received, so we have
+ // to use ONMOUSEMOVE that doesn't target the popup
+ @Override
+ public boolean onEventPreview(Event event) {
+ Element target = DOM.eventGetTarget(event);
+ boolean eventTargetsPopup = DOM.isOrHasChild(getElement(), target);
+ int type = DOM.eventGetType(event);
+
+ // Catch children that use keyboard, so we can unfocus them when
+ // hiding
+ if (eventTargetsPopup && type == Event.ONKEYPRESS) {
+ activeChildren.add(target);
+ }
+
+ if (eventTargetsPopup && type == Event.ONMOUSEMOVE) {
+ hasHadMouseOver = true;
+ }
+
+ if (!eventTargetsPopup && type == Event.ONMOUSEMOVE) {
+
+ if (hasHadMouseOver && hideOnMouseOut) {
+ hide();
+ return true;
+ }
+ }
+
+ return super.onEventPreview(event);
+ }
+
+ @Override
+ public void hide(boolean autoClosed) {
+ hiding = true;
+ syncChildren();
+ unregisterPaintables();
+ if (popupComponentWidget != null && popupComponentWidget != loading) {
+ remove(popupComponentWidget);
+ }
+ hasHadMouseOver = false;
+ super.hide(autoClosed);
+ }
+
+ @Override
+ public void show() {
+ hiding = false;
+ super.show();
+ }
+
+ /**
+ * Try to sync all known active child widgets to server
+ */
+ public void syncChildren() {
+ // Notify children with focus
+ if ((popupComponentWidget instanceof HasFocus)) {
+ ((HasFocus) popupComponentWidget).setFocus(false);
+ }
+
+ // Notify children that have used the keyboard
+ for (Element e : activeChildren) {
+ try {
+ nativeBlur(e);
+ } catch (Exception ignored) {
+ }
+ }
+ activeChildren.clear();
+ }
+
+ @Override
+ public boolean remove(Widget w) {
+
+ popupComponentPaintable = null;
+ popupComponentWidget = null;
+ captionWrapper = null;
+
+ return super.remove(w);
+ }
+
+ public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+
+ Paintable newPopupComponent = client.getPaintable(uidl
+ .getChildUIDL(0));
+
+ if (newPopupComponent != popupComponentPaintable) {
+
+ setWidget((Widget) newPopupComponent);
+
+ popupComponentWidget = (Widget) newPopupComponent;
+
+ popupComponentPaintable = newPopupComponent;
+ }
+
+ popupComponentPaintable
+ .updateFromUIDL(uidl.getChildUIDL(0), client);
+
+ }
+
+ public void unregisterPaintables() {
+ if (popupComponentPaintable != null) {
+ client.unregisterPaintable(popupComponentPaintable);
+ }
+ }
+
+ public void setHideOnMouseOut(boolean hideOnMouseOut) {
+ this.hideOnMouseOut = hideOnMouseOut;
+ }
+
+ /*
+ *
+ * We need a hack make popup act as a child of IPopupView in toolkits
+ * component tree, but work in default GWT manner when closing or
+ * opening.
+ *
+ * (non-Javadoc)
+ *
+ * @see com.google.gwt.user.client.ui.Widget#getParent()
+ */
+ @Override
+ public Widget getParent() {
+ if (!isAttached() || hiding) {
+ return super.getParent();
+ } else {
+ return IPopupView.this;
+ }
+ }
+
+ @Override
+ protected void onDetach() {
+ super.onDetach();
+ hiding = false;
+ }
+
+ @Override
+ public Element getContainerElement() {
+ return super.getContainerElement();
+ }
+
+ }// class CustomPopup
+
+ // Container methods
+
+ public RenderSpace getAllocatedSpace(Widget child) {
+ Size popupExtra = calculatePopupExtra();
+
+ return new RenderSpace(RootPanel.get().getOffsetWidth()
+ - popupExtra.getWidth(), RootPanel.get().getOffsetHeight()
+ - popupExtra.getHeight());
+ }
+
+ /**
+ * Calculate extra space taken by the popup decorations
+ *
+ * @return
+ */
+ protected Size calculatePopupExtra() {
+ Element pe = popup.getElement();
+ Element ipe = popup.getContainerElement();
+
+ // border + padding
+ int width = Util.getRequiredWidth(pe) - Util.getRequiredWidth(ipe);
+ int height = Util.getRequiredHeight(pe) - Util.getRequiredHeight(ipe);
+
+ return new Size(width, height);
+ }
+
+ public boolean hasChildComponent(Widget component) {
+ if (popup.popupComponentWidget != null) {
+ return popup.popupComponentWidget == component;
+ } else {
+ return false;
+ }
+ }
+
+ public void replaceChildComponent(Widget oldComponent, Widget newComponent) {
+ popup.setWidget(newComponent);
+ popup.popupComponentWidget = newComponent;
+ }
+
+ public boolean requestLayout(Set<Paintable> child) {
+ return true;
+ }
+
+ public void updateCaption(Paintable component, UIDL uidl) {
+ if (ICaption.isNeeded(uidl)) {
+ if (popup.captionWrapper != null) {
+ popup.captionWrapper.updateCaption(uidl);
+ } else {
+ popup.captionWrapper = new ICaptionWrapper(component, client);
+ popup.setWidget(popup.captionWrapper);
+ popup.captionWrapper.updateCaption(uidl);
+ }
+ } else {
+ if (popup.captionWrapper != null) {
+ popup.setWidget(popup.popupComponentWidget);
+ }
+ }
+
+ popup.popupComponentWidget = (Widget) component;
+ popup.popupComponentPaintable = component;
+ }
+
+ @Override
+ public void onBrowserEvent(Event event) {
+ super.onBrowserEvent(event);
+ if (client != null) {
+ client.handleTooltipEvent(event, this);
+ }
+ }
+
+}// class IPopupView
diff --git a/src/com/vaadin/terminal/gwt/client/ui/IProgressIndicator.java b/src/com/vaadin/terminal/gwt/client/ui/IProgressIndicator.java
new file mode 100644
index 0000000000..0490dd048f
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/IProgressIndicator.java
@@ -0,0 +1,100 @@
+/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.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.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.Paintable;
+import com.vaadin.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(DOM.createDiv());
+ getElement().appendChild(wrapper);
+ setStyleName(CLASSNAME);
+ wrapper.appendChild(indicator);
+ indicator.setClassName(CLASSNAME + "-indicator");
+ wrapper.setClassName(CLASSNAME + "-wrapper");
+ 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");
+
+ if (indeterminate) {
+ String basename = CLASSNAME + "-indeterminate";
+ IProgressIndicator.setStyleName(getElement(), basename, true);
+ IProgressIndicator.setStyleName(getElement(), basename
+ + "-disabled", uidl.getBooleanAttribute("disabled"));
+ } else {
+ 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"));
+ }
+ }
+
+ @Override
+ protected void onAttach() {
+ super.onAttach();
+ if (pollerSuspendedDueDetach) {
+ poller.run();
+ }
+ }
+
+ @Override
+ protected void onDetach() {
+ super.onDetach();
+ poller.cancel();
+ pollerSuspendedDueDetach = true;
+ }
+
+ @Override
+ public void setVisible(boolean visible) {
+ super.setVisible(visible);
+ if (!visible) {
+ poller.cancel();
+ }
+ }
+
+ class Poller extends Timer {
+
+ @Override
+ public void run() {
+ client.sendPendingVariableChanges();
+ }
+
+ }
+
+}
diff --git a/src/com/vaadin/terminal/gwt/client/ui/IScrollTable.java b/src/com/vaadin/terminal/gwt/client/ui/IScrollTable.java
new file mode 100644
index 0000000000..2b085ebd87
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/IScrollTable.java
@@ -0,0 +1,2841 @@
+/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
+import java.util.Vector;
+
+import com.google.gwt.dom.client.Document;
+import com.google.gwt.dom.client.NodeList;
+import com.google.gwt.dom.client.TableCellElement;
+import com.google.gwt.dom.client.TableRowElement;
+import com.google.gwt.dom.client.TableSectionElement;
+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.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.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.BrowserInfo;
+import com.vaadin.terminal.gwt.client.Container;
+import com.vaadin.terminal.gwt.client.MouseEventDetails;
+import com.vaadin.terminal.gwt.client.Paintable;
+import com.vaadin.terminal.gwt.client.RenderSpace;
+import com.vaadin.terminal.gwt.client.UIDL;
+import com.vaadin.terminal.gwt.client.Util;
+import com.vaadin.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 FlowPanel implements Table, ScrollListener {
+
+ public static final String CLASSNAME = "i-table";
+ /**
+ * multiple of pagelength 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 int lastRequestedFirstvisible = 0; // to detect "serverside scroll"
+
+ 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<String> selectedRowKeys = new HashSet<String>();
+
+ private boolean initializedAndAttached = false;
+
+ /**
+ * Flag to indicate if a column width recalculation is needed due update.
+ */
+ private boolean headerChangedDuringUpdate = false;
+
+ private final TableHead tHead = new TableHead();
+
+ private final ScrollPanel bodyContainer = new ScrollPanel();
+
+ private int totalRows;
+
+ private Set<String> collapsedColumns;
+
+ private final RowRequestHandler rowRequestHandler;
+ private IScrollTableBody tBody;
+ 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<Object, String> actionMap = new HashMap<Object, String>();
+ private String[] visibleColOrder;
+ private boolean initialContentReceived = false;
+ private Element scrollPositionElement;
+ private boolean enabled;
+ private boolean showColHeaders;
+
+ /** flag to indicate that table body has changed */
+ private boolean isNewBody = true;
+
+ private boolean emitClickEvents;
+
+ /*
+ * Read from the "recalcWidths" -attribute. When it is true, the table will
+ * recalculate the widths for columns - desirable in some cases. For #1983,
+ * marked experimental.
+ */
+ boolean recalcWidths = false;
+
+ private final ArrayList<Panel> lazyUnregistryBag = new ArrayList<Panel>();
+ private String height;
+ private String width = "";
+ private boolean rendering = false;
+
+ public IScrollTable() {
+ bodyContainer.addScrollListener(this);
+ bodyContainer.setStyleName(CLASSNAME + "-body");
+
+ setStyleName(CLASSNAME);
+ add(tHead);
+ add(bodyContainer);
+
+ rowRequestHandler = new RowRequestHandler();
+
+ }
+
+ @SuppressWarnings("unchecked")
+ public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+ rendering = true;
+ if (client.updateComponent(this, uidl, true)) {
+ rendering = false;
+ return;
+ }
+
+ // we may have pending cache row fetch, cancel it. See #2136
+ rowRequestHandler.cancel();
+
+ enabled = !uidl.hasAttribute("disabled");
+
+ this.client = client;
+ paintableId = uidl.getStringAttribute("id");
+ immediate = uidl.getBooleanAttribute("immediate");
+ emitClickEvents = uidl.getBooleanAttribute("listenClicks");
+ final int newTotalRows = uidl.getIntAttribute("totalrows");
+ if (newTotalRows != totalRows) {
+ if (tBody != null) {
+ if (totalRows == 0) {
+ tHead.clear();
+ }
+ initializedAndAttached = false;
+ initialContentReceived = false;
+ isNewBody = true;
+ }
+ totalRows = newTotalRows;
+ }
+
+ recalcWidths = uidl.hasAttribute("recalcWidths");
+
+ pageLength = uidl.getIntAttribute("pagelength");
+ if (pageLength == 0) {
+ pageLength = totalRows;
+ }
+ firstvisible = uidl.hasVariable("firstvisible") ? uidl
+ .getIntVariable("firstvisible") : 0;
+ if (firstvisible != lastRequestedFirstvisible && tBody != null) {
+ // received 'surprising' firstvisible from server: scroll there
+ firstRowInViewPort = firstvisible;
+ bodyContainer
+ .setScrollPosition(firstvisible * tBody.getRowHeight());
+ }
+
+ showRowHeaders = uidl.getBooleanAttribute("rowheaders");
+ showColHeaders = uidl.getBooleanAttribute("colheaders");
+
+ if (uidl.hasVariable("sortascending")) {
+ sortAscending = uidl.getBooleanVariable("sortascending");
+ sortColumn = uidl.getStringVariable("sortcolumn");
+ }
+
+ if (uidl.hasVariable("selected")) {
+ final Set<String> selectedKeys = uidl
+ .getStringArrayVariableAsSet("selected");
+ selectedRowKeys.clear();
+ for (String string : selectedKeys) {
+ selectedRowKeys.add(string);
+ }
+ }
+
+ 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 (!recalcWidths && initializedAndAttached) {
+ updateBody(rowData, uidl.getIntAttribute("firstrow"), uidl
+ .getIntAttribute("rows"));
+ if (headerChangedDuringUpdate) {
+ lazyAdjustColumnWidths.schedule(1);
+ }
+ } else {
+ if (tBody != null) {
+ tBody.removeFromParent();
+ lazyUnregistryBag.add(tBody);
+ }
+ tBody = new IScrollTableBody();
+
+ tBody.renderInitialRows(rowData, uidl.getIntAttribute("firstrow"),
+ uidl.getIntAttribute("rows"));
+ bodyContainer.add(tBody);
+ initialContentReceived = true;
+ if (isAttached()) {
+ sizeInit();
+ }
+ }
+ hideScrollPositionAnnotation();
+ purgeUnregistryBag();
+ rendering = false;
+ headerChangedDuringUpdate = false;
+ }
+
+ /**
+ * Unregisters Paintables in "trashed" HasWidgets (IScrollTableBodys or
+ * IScrollTableRows). This is done lazily as Table must survive from
+ * "subtreecaching" logic.
+ */
+ private void purgeUnregistryBag() {
+ for (Iterator<Panel> iterator = lazyUnregistryBag.iterator(); iterator
+ .hasNext();) {
+ client.unregisterChildPaintables(iterator.next());
+ }
+ lazyUnregistryBag.clear();
+ }
+
+ 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 actionMap.get(actionKey + "_c");
+ }
+
+ public String getActionIcon(String actionKey) {
+ return 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, boolean isDefinedWidth) {
+ final HeaderCell cell = tHead.getHeaderCell(colIndex);
+ cell.setWidth(w, isDefinedWidth);
+ tBody.setColWidth(colIndex, w);
+ }
+
+ private int getColWidth(String colKey) {
+ return tHead.getHeaderCell(colKey).getWidth();
+ }
+
+ private IScrollTableRow getRenderedRowByKey(String key) {
+ final Iterator<Widget> 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);
+ }
+
+ @Override
+ protected void onAttach() {
+ super.onAttach();
+ if (initialContentReceived) {
+ sizeInit();
+ }
+ }
+
+ @Override
+ 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<Widget> headCells = tHead.iterator();
+ int i = 0;
+ int totalExplicitColumnsWidths = 0;
+ int total = 0;
+ float expandRatioDivider = 0;
+
+ final int[] widths = new int[tHead.visibleCells.size()];
+
+ tHead.enableBrowserIntelligence();
+ // first loop: collect natural widths
+ while (headCells.hasNext()) {
+ final HeaderCell hCell = (HeaderCell) headCells.next();
+ int w = hCell.getWidth();
+ if (hCell.isDefinedWidth()) {
+ // server has defined column width explicitly
+ totalExplicitColumnsWidths += w;
+ } else {
+ if (hCell.getExpandRatio() > 0) {
+ expandRatioDivider += hCell.getExpandRatio();
+ w = 0;
+ } else {
+ // get and store greater of header width and column width,
+ // and
+ // store it as a minimumn natural col width
+ w = hCell.getNaturalColumnWidth(i);
+ }
+ hCell.setNaturalMinimumColumnWidth(w);
+ }
+ widths[i] = w;
+ total += w;
+ i++;
+ }
+
+ tHead.disableBrowserIntelligence();
+
+ boolean willHaveScrollbarz = willHaveScrollbars();
+
+ // fix "natural" width if width not set
+ if (width == null || "".equals(width)) {
+ int w = total;
+ w += tBody.getCellExtraWidth() * visibleColOrder.length;
+ if (willHaveScrollbarz) {
+ w += Util.getNativeScrollbarSize();
+ }
+ setContentWidth(w);
+ }
+
+ int availW = tBody.getAvailableWidth();
+ if (BrowserInfo.get().isIE()) {
+ // Hey IE, are you really sure about this?
+ availW = tBody.getAvailableWidth();
+ }
+ availW -= tBody.getCellExtraWidth() * visibleColOrder.length;
+
+ if (willHaveScrollbarz) {
+ availW -= Util.getNativeScrollbarSize();
+ }
+
+ boolean needsReLayout = false;
+
+ if (availW > total) {
+ // natural size is smaller than available space
+ final int extraSpace = availW - total;
+ final int totalWidthR = total - totalExplicitColumnsWidths;
+ if (totalWidthR > 0) {
+ needsReLayout = true;
+
+ if (expandRatioDivider > 0) {
+ // visible columns have some active expand ratios, excess
+ // space is divided according to them
+ headCells = tHead.iterator();
+ i = 0;
+ while (headCells.hasNext()) {
+ HeaderCell hCell = (HeaderCell) headCells.next();
+ if (hCell.getExpandRatio() > 0) {
+ int w = widths[i];
+ final int newSpace = (int) (extraSpace * (hCell
+ .getExpandRatio() / expandRatioDivider));
+ w += newSpace;
+ widths[i] = w;
+ }
+ i++;
+ }
+ } else {
+ // now we will share this sum relatively to those without
+ // explicit width
+ headCells = tHead.iterator();
+ i = 0;
+ while (headCells.hasNext()) {
+ HeaderCell hCell = (HeaderCell) headCells.next();
+ if (!hCell.isDefinedWidth()) {
+ 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, false);
+ }
+ i++;
+ }
+ if (needsReLayout) {
+ tBody.reLayoutComponents();
+ }
+
+ /*
+ * Fix "natural" height if height is not set. This must be after width
+ * fixing so the components' widths have been adjusted.
+ */
+ if (height == null || "".equals(height)) {
+ /*
+ * We must force an update of the row height as this point as it
+ * might have been (incorrectly) calculated earlier
+ */
+ if (pageLength == totalRows) {
+ /*
+ * A hack to support variable height rows when paging is off.
+ * Generally this is not supported by scrolltable. We want to
+ * show all rows so the bodyHeight should be equal to the table
+ * height.
+ */
+ int bodyHeight = tBody.getOffsetHeight();
+ bodyContainer.setHeight(bodyHeight + "px");
+ Util.runWebkitOverflowAutoFix(bodyContainer.getElement());
+ } else {
+ int bodyHeight = (tBody.getRowHeight(true) * pageLength);
+ bodyContainer.setHeight(bodyHeight + "px");
+ }
+ }
+
+ 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) {
+ if (totalRows - 1 > tBody.getLastRendered()) {
+ // fetch cache rows
+ rowRequestHandler
+ .setReqFirstRow(tBody.getLastRendered() + 1);
+ rowRequestHandler
+ .setReqRows((int) (pageLength * CACHE_RATE));
+ rowRequestHandler.deferRowFetch(1);
+ }
+ }
+ }
+ initializedAndAttached = true;
+ }
+
+ private boolean willHaveScrollbars() {
+ if (!(height != null && !height.equals(""))) {
+ if (pageLength < totalRows) {
+ return true;
+ }
+ } else {
+ int fakeheight = tBody.getRowHeight() * totalRows;
+ int availableHeight = bodyContainer.getElement().getPropertyInt(
+ "clientHeight");
+ if (fakeheight > availableHeight) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * 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());
+
+ 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) {
+ // remember which firstvisible we requested, in case the server has
+ // a differing opinion
+ lastRequestedFirstvisible = firstRowInViewPort;
+ 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
+ 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
+ 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
+ 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;
+ }
+
+ @Override
+ public void run() {
+ if (client.hasActiveRequest()) {
+ // if client connection is busy, don't bother loading it more
+ schedule(250);
+
+ } else {
+
+ 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;
+ }
+ // due Safari 3.1 bug (see #2607), verify reqrows, original
+ // problem unknown, but this should catch the issue
+ if (reqFirstRow + reqRows - 1 > lastToBeRendered) {
+ reqRows = lastToBeRendered - reqFirstRow;
+ }
+ }
+
+ client.updateVariable(paintableId, "firstToBeRendered",
+ firstToBeRendered, false);
+
+ client.updateVariable(paintableId, "lastToBeRendered",
+ lastToBeRendered, false);
+ // remember which firstvisible we requested, in case the server
+ // has
+ // a differing opinion
+ lastRequestedFirstvisible = firstRowInViewPort;
+ 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 {
+
+ 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 int naturalWidth = -1;
+
+ private char align = ALIGN_LEFT;
+
+ boolean definedWidth = false;
+
+ private float expandRatio = 0;
+
+ public void setSortable(boolean b) {
+ sortable = b;
+ }
+
+ public void setNaturalMinimumColumnWidth(int w) {
+ naturalWidth = w;
+ }
+
+ public HeaderCell(String colId, String headerText) {
+ cid = colId;
+
+ DOM.setElementProperty(colResizeWidget, "className", CLASSNAME
+ + "-resizer");
+ 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, boolean ensureDefinedWidth) {
+ if (ensureDefinedWidth) {
+ definedWidth = true;
+ // on column resize expand ratio becomes zero
+ expandRatio = 0;
+ }
+ if (width == w) {
+ return;
+ }
+ if (width == -1) {
+ // go to default mode, clip content if necessary
+ DOM.setStyleAttribute(captionContainer, "overflow", "");
+ }
+ width = w;
+ if (w == -1) {
+ DOM.setStyleAttribute(captionContainer, "width", "");
+ setWidth("");
+ } else {
+
+ captionContainer.getStyle().setPropertyPx("width", w);
+
+ /*
+ * if we already have tBody, set the header width properly, if
+ * not defer it. IE will fail with complex float in table header
+ * unless TD width is not explicitly set.
+ */
+ if (tBody != null) {
+ int tdWidth = width + tBody.getCellExtraWidth();
+ setWidth(tdWidth + "px");
+ } else {
+ DeferredCommand.addCommand(new Command() {
+ public void execute() {
+ int tdWidth = width + tBody.getCellExtraWidth();
+ setWidth(tdWidth + "px");
+ }
+ });
+ }
+ }
+ }
+
+ public void setUndefinedWidth() {
+ definedWidth = false;
+ setWidth(-1, false);
+ }
+
+ /**
+ * Detects if width is fixed by developer on server side or resized to
+ * current width by user.
+ *
+ * @return true if defined, false if "natural" width
+ */
+ public boolean isDefinedWidth() {
+ return definedWidth;
+ }
+
+ 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.
+ */
+ @Override
+ public void onBrowserEvent(Event event) {
+ if (enabled && event != null) {
+ if (isResizing || event.getTarget() == 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:
+ if (columnReordering) {
+ dragging = true;
+ moved = false;
+ colIndex = getColIndexByKey(cid);
+ DOM.setCapture(getElement());
+ headerX = tHead.getAbsoluteLeft();
+ DOM.eventPreventDefault(event); // prevent selecting text
+ }
+ break;
+ case Event.ONMOUSEUP:
+ if (columnReordering) {
+ dragging = false;
+ DOM.releaseCapture(getElement());
+ 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) {
+ 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);
+ }
+ 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());
+ // readjust undefined width columns
+ lazyAdjustColumnWidths.cancel();
+ lazyAdjustColumnWidths.schedule(1);
+ break;
+ case Event.ONMOUSEMOVE:
+ if (isResizing) {
+ final int deltaX = DOM.eventGetClientX(event) - dragStartX;
+ if (deltaX == 0) {
+ return;
+ }
+
+ int newWidth = originalWidth + deltaX;
+ if (newWidth < tBody.getCellExtraWidth()) {
+ newWidth = tBody.getCellExtraWidth();
+ }
+ setColWidth(colIndex, newWidth, true);
+ }
+ 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;
+ }
+
+ /**
+ * Detects the natural minimum width for the column of this header cell.
+ * If column is resized by user or the width is defined by server the
+ * actual width is returned. Else the natural min width is returned.
+ *
+ * @param columnIndex
+ * column index hint, if -1 (unknown) it will be detected
+ *
+ * @return
+ */
+ public int getNaturalColumnWidth(int columnIndex) {
+ if (isDefinedWidth()) {
+ return width;
+ } else {
+ if (naturalWidth < 0) {
+ // This is recently revealed column. Try to detect a proper
+ // value (greater of header and data
+ // cols)
+
+ final int hw = ((Element) getElement().getLastChild())
+ .getOffsetWidth()
+ + tBody.getCellExtraWidth();
+ if (columnIndex < 0) {
+ columnIndex = 0;
+ for (Iterator<Widget> it = tHead.iterator(); it
+ .hasNext(); columnIndex++) {
+ if (it.next() == this) {
+ break;
+ }
+ }
+ }
+ final int cw = tBody.getColWidth(columnIndex);
+ naturalWidth = (hw > cw ? hw : cw);
+ }
+ return naturalWidth;
+ }
+ }
+
+ public void setExpandRatio(float floatAttribute) {
+ expandRatio = floatAttribute;
+ }
+
+ public float getExpandRatio() {
+ return expandRatio;
+ }
+
+ }
+
+ /**
+ * HeaderCell that is header cell for row headers.
+ *
+ * Reordering disabled and clicking on it resets sorting.
+ */
+ public class RowHeadersHeaderCell extends HeaderCell {
+
+ RowHeadersHeaderCell() {
+ super("0", "");
+ }
+
+ @Override
+ 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<Widget> visibleCells = new Vector<Widget>();
+
+ HashMap<String, HeaderCell> availableCells = new HashMap<String, HeaderCell>();
+
+ 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() {
+ if (BrowserInfo.get().isIE()) {
+ table.setPropertyInt("cellSpacing", 0);
+ }
+
+ 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());
+ }
+
+ @Override
+ public void clear() {
+ for (String cid : availableCells.keySet()) {
+ removeCell(cid);
+ }
+ availableCells.clear();
+ availableCells.put("0", new RowHeadersHeaderCell());
+ }
+
+ public void updateCellsFromUIDL(UIDL uidl) {
+ Iterator<?> it = uidl.getChildIterator();
+ HashSet<String> updated = new HashSet<String>();
+ updated.add("0");
+ while (it.hasNext()) {
+ final UIDL col = (UIDL) it.next();
+ final String cid = col.getStringAttribute("cid");
+ updated.add(cid);
+
+ String caption = buildCaptionHtmlSnippet(col);
+ HeaderCell c = getHeaderCell(cid);
+ if (c == null) {
+ c = new HeaderCell(cid, caption);
+ availableCells.put(cid, c);
+ if (initializedAndAttached) {
+ // we will need a column width recalculation
+ initializedAndAttached = false;
+ initialContentReceived = false;
+ isNewBody = true;
+ }
+ } else {
+ c.setText(caption);
+ }
+
+ if (col.hasAttribute("sortable")) {
+ c.setSortable(true);
+ if (cid.equals(sortColumn)) {
+ c.setSorted(true);
+ } else {
+ c.setSorted(false);
+ }
+ } else {
+ c.setSortable(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), true);
+ } else if (recalcWidths) {
+ c.setUndefinedWidth();
+ }
+ if (col.hasAttribute("er")) {
+ c.setExpandRatio(col.getFloatAttribute("er"));
+ }
+ }
+ // check for orphaned header cells
+ for (String cid : availableCells.keySet()) {
+ 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 (initializedAndAttached) {
+ headerChangedDuringUpdate = 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 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<Widget> iterator() {
+ return visibleCells.iterator();
+ }
+
+ @Override
+ 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;
+ }
+
+ @Override
+ public void onBrowserEvent(Event event) {
+ if (enabled) {
+ if (event.getTarget() == 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();
+ }
+
+ @Override
+ public void execute() {
+ client.getContextMenu().hide();
+ // toggle selected column
+ if (collapsedColumns.contains(colKey)) {
+ collapsedColumns.remove(colKey);
+ } else {
+ tHead.removeCell(colKey);
+ collapsedColumns.add(colKey);
+ lazyAdjustColumnWidths.schedule(1);
+ }
+
+ // 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
+ */
+ @Override
+ 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<String> 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<Widget> 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 DEFAULT_ROW_HEIGHT = 24;
+
+ private int rowHeight = -1;
+
+ private final List<Widget> renderedRows = new Vector<Widget>();
+
+ /**
+ * Due some optimizations row height measuring is deferred and initial
+ * set of rows is rendered detached. Flag set on when table body has
+ * been attached in dom and rowheight has been measured.
+ */
+ private boolean tBodyMeasurementsDone = false;
+
+ Element preSpacer = DOM.createDiv();
+ Element postSpacer = DOM.createDiv();
+
+ Element container = DOM.createDiv();
+
+ TableSectionElement tBodyElement = Document.get().createTBodyElement();
+ 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");
+ if (BrowserInfo.get().isIE()) {
+ table.setPropertyInt("cellSpacing", 0);
+ }
+ DOM.setElementProperty(preSpacer, "className", CLASSNAME
+ + "-row-spacer");
+ DOM.setElementProperty(postSpacer, "className", CLASSNAME
+ + "-row-spacer");
+
+ table.appendChild(tBodyElement);
+ DOM.appendChild(container, preSpacer);
+ DOM.appendChild(container, table);
+ DOM.appendChild(container, postSpacer);
+
+ }
+
+ public int getAvailableWidth() {
+ int availW = bodyContainer.getOffsetWidth() - getBorderWidth();
+ return availW;
+ }
+
+ 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();
+ }
+ // 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 (lastRendered < reactLastRow) {
+ // get some cache rows below visible area
+ rowRequestHandler.setReqFirstRow(lastRendered + 1);
+ rowRequestHandler.setReqRows(reactLastRow - lastRendered - 1);
+ rowRequestHandler.deferRowFetch(1);
+ } else if (tBody.getFirstRendered() > reactFirstRow) {
+ /*
+ * Branch for fetching cache above visible area.
+ *
+ * If cache needed for both before and after visible area, this
+ * will be rendered after-cache is reveived and rendered. So in
+ * some rare situations table may take two cache visits to
+ * server.
+ */
+ rowRequestHandler.setReqFirstRow(reactFirstRow);
+ rowRequestHandler.setReqRows(firstRendered - reactFirstRow);
+ 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);
+ int w = IScrollTable.this.getColWidth(getColKeyByIndex(i));
+ if (w < 0) {
+ w = 0;
+ }
+ cell.getFirstChildElement().getStyle()
+ .setPropertyPx("width", w);
+ cell.getStyle().setPropertyPx("width", w);
+ }
+ 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.addStyleName(CLASSNAME + "-row-odd");
+ } else {
+ row.addStyleName(CLASSNAME + "-row");
+ }
+ if (row.isSelected()) {
+ row.addStyleName("i-selected");
+ }
+ tBodyElement.insertBefore(row.getElement(), tBodyElement
+ .getFirstChild());
+ 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.addStyleName(CLASSNAME + "-row-odd");
+ } else {
+ row.addStyleName(CLASSNAME + "-row");
+ }
+ if (row.isSelected()) {
+ row.addStyleName("i-selected");
+ }
+ tBodyElement.appendChild(row.getElement());
+ adopt(row);
+ renderedRows.add(row);
+ }
+
+ public Iterator<Widget> 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--;
+ }
+ if (index >= 0) {
+ final IScrollTableRow toBeRemoved = (IScrollTableRow) renderedRows
+ .get(index);
+ lazyUnregistryBag.add(toBeRemoved);
+ tBodyElement.removeChild(toBeRemoved.getElement());
+ orphan(toBeRemoved);
+ renderedRows.remove(index);
+ fixSpacers();
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ public boolean remove(Widget w) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ 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() {
+ return getRowHeight(false);
+ }
+
+ public int getRowHeight(boolean forceUpdate) {
+ if (tBodyMeasurementsDone && !forceUpdate) {
+ return rowHeight;
+ } else {
+
+ if (tBodyElement.getRows().getLength() > 0) {
+ rowHeight = getTableHeight()
+ / tBodyElement.getRows().getLength();
+ } else {
+ if (isAttached()) {
+ // measure row height by adding a dummy row
+ IScrollTableRow scrollTableRow = new IScrollTableRow();
+ tBodyElement.appendChild(scrollTableRow.getElement());
+ getRowHeight(forceUpdate);
+ tBodyElement.removeChild(scrollTableRow.getElement());
+ } else {
+ // TODO investigate if this can never happen anymore
+ return DEFAULT_ROW_HEIGHT;
+ }
+ }
+ tBodyMeasurementsDone = true;
+ return rowHeight;
+ }
+ }
+
+ public int getTableHeight() {
+ return table.getOffsetHeight();
+ }
+
+ /**
+ * Returns the width available for column content.
+ *
+ * @param columnIndex
+ * @return
+ */
+ public int getColWidth(int columnIndex) {
+ if (tBodyMeasurementsDone) {
+ NodeList<TableRowElement> rows = tBodyElement.getRows();
+ if (rows.getLength() == 0) {
+ // no rows yet rendered
+ return 0;
+ } else {
+ com.google.gwt.dom.client.Element wrapperdiv = rows
+ .getItem(0).getCells().getItem(columnIndex)
+ .getFirstChildElement();
+ return wrapperdiv.getOffsetWidth();
+ }
+ } else {
+ return 0;
+ }
+ }
+
+ /**
+ * Sets the content width of a column.
+ *
+ * Due IE limitation, we must set the width to a wrapper elements inside
+ * table cells (with overflow hidden, which does not work on td
+ * elements).
+ *
+ * To get this work properly crossplatform, we will also set the width
+ * of td.
+ *
+ * @param colIndex
+ * @param w
+ */
+ public void setColWidth(int colIndex, int w) {
+ NodeList<TableRowElement> rows2 = tBodyElement.getRows();
+ final int rows = rows2.getLength();
+ for (int i = 0; i < rows; i++) {
+ TableRowElement row = rows2.getItem(i);
+ TableCellElement cell = row.getCells().getItem(colIndex);
+ cell.getFirstChildElement().getStyle()
+ .setPropertyPx("width", w);
+ cell.getStyle().setPropertyPx("width", w);
+ }
+ }
+
+ private int cellExtraWidth = -1;
+
+ /**
+ * Method to return the space used for cell paddings + border.
+ */
+ private int getCellExtraWidth() {
+ if (cellExtraWidth < 0) {
+ detectExtrawidth();
+ }
+ return cellExtraWidth;
+ }
+
+ private void detectExtrawidth() {
+ NodeList<TableRowElement> rows = tBodyElement.getRows();
+ if (rows.getLength() == 0) {
+ /* need to temporary add empty row and detect */
+ IScrollTableRow scrollTableRow = new IScrollTableRow();
+ tBodyElement.appendChild(scrollTableRow.getElement());
+ detectExtrawidth();
+ tBodyElement.removeChild(scrollTableRow.getElement());
+ } else {
+ TableRowElement item = rows.getItem(0);
+ TableCellElement firstTD = item.getCells().getItem(0);
+ com.google.gwt.dom.client.Element wrapper = firstTD
+ .getFirstChildElement();
+ cellExtraWidth = firstTD.getOffsetWidth()
+ - wrapper.getOffsetWidth();
+ }
+ }
+
+ private void reLayoutComponents() {
+ for (Widget w : this) {
+ IScrollTableRow r = (IScrollTableRow) w;
+ for (Widget widget : r) {
+ client.handleComponentRelativeSize(widget);
+ }
+ }
+ }
+
+ 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,
+ Container {
+
+ Vector<Widget> childWidgets = new Vector<Widget>();
+ private boolean selected = false;
+ private final int rowKey;
+ private List<UIDL> pendingComponentPaints;
+
+ private String[] actionKeys = null;
+ private TableRowElement rowElement;
+
+ private IScrollTableRow(int rowKey) {
+ this.rowKey = rowKey;
+ rowElement = Document.get().createTRElement();
+ setElement(rowElement);
+ DOM.sinkEvents(getElement(), Event.ONCLICK | Event.ONDBLCLICK
+ | Event.ONCONTEXTMENU);
+ }
+
+ private void paintComponent(Paintable p, UIDL uidl) {
+ if (isAttached()) {
+ p.updateFromUIDL(uidl, client);
+ } else {
+ if (pendingComponentPaints == null) {
+ pendingComponentPaints = new LinkedList<UIDL>();
+ }
+ pendingComponentPaints.add(uidl);
+ }
+ }
+
+ @Override
+ protected void onAttach() {
+ super.onAttach();
+ if (pendingComponentPaints != null) {
+ for (UIDL uidl : pendingComponentPaints) {
+ Paintable paintable = client.getPaintable(uidl);
+ paintable.updateFromUIDL(uidl, client);
+ }
+ }
+ }
+
+ public String getKey() {
+ return String.valueOf(rowKey);
+ }
+
+ public IScrollTableRow(UIDL uidl, char[] aligns) {
+ this(uidl.getIntAttribute("key"));
+
+ String rowStyle = uidl.getStringAttribute("rowstyle");
+ if (rowStyle != null) {
+ addStyleName(CLASSNAME + "-row-" + rowStyle);
+ }
+
+ tHead.getColumnAlignments();
+ int col = 0;
+ int visibleColumnIndex = -1;
+
+ // row header
+ if (showRowHeaders) {
+ addCell(buildCaptionHtmlSnippet(uidl), aligns[col++], "",
+ true);
+ }
+
+ if (uidl.hasAttribute("al")) {
+ actionKeys = uidl.getStringArrayAttribute("al");
+ }
+
+ final Iterator<?> cells = uidl.getChildIterator();
+ while (cells.hasNext()) {
+ final Object cell = cells.next();
+ visibleColumnIndex++;
+
+ String columnId = visibleColOrder[visibleColumnIndex];
+
+ String style = "";
+ if (uidl.hasAttribute("style-" + columnId)) {
+ style = uidl.getStringAttribute("style-" + columnId);
+ }
+
+ if (cell instanceof String) {
+ addCell(cell.toString(), aligns[col++], style, false);
+ } else {
+ final Paintable cellContent = client
+ .getPaintable((UIDL) cell);
+
+ addCell((Widget) cellContent, aligns[col++], style);
+ paintComponent(cellContent, (UIDL) cell);
+ }
+ }
+ if (uidl.hasAttribute("selected") && !isSelected()) {
+ toggleSelection();
+ }
+ }
+
+ /**
+ * Add a dummy row, used for measurements if Table is empty.
+ */
+ public IScrollTableRow() {
+ this(0);
+ addStyleName(CLASSNAME + "-row");
+ addCell("_", 'b', "", true);
+ }
+
+ public void addCell(String text, char align, String style,
+ boolean textIsHTML) {
+ // 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;
+ }
+ td.setClassName(className);
+ container.setClassName(CLASSNAME + "-cell-wrapper");
+ if (textIsHTML) {
+ container.setInnerHTML(text);
+ } else {
+ container.setInnerText(text);
+ }
+ if (align != ALIGN_LEFT) {
+ switch (align) {
+ case ALIGN_CENTER:
+ container.getStyle().setProperty("textAlign", "center");
+ break;
+ case ALIGN_RIGHT:
+ default:
+ container.getStyle().setProperty("textAlign", "right");
+ break;
+ }
+ }
+ td.appendChild(container);
+ getElement().appendChild(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;
+ }
+ td.setClassName(className);
+ container.setClassName(CLASSNAME + "-cell-wrapper");
+ // 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:
+ container.getStyle().setProperty("textAlign", "center");
+ break;
+ case ALIGN_RIGHT:
+ default:
+ container.getStyle().setProperty("textAlign", "right");
+ break;
+ }
+ }
+ td.appendChild(container);
+ getElement().appendChild(td);
+ // ensure widget not attached to another element (possible tBody
+ // change)
+ w.removeFromParent();
+ container.appendChild(w.getElement());
+ adopt(w);
+ childWidgets.add(w);
+ }
+
+ public Iterator<Widget> iterator() {
+ return childWidgets.iterator();
+ }
+
+ @Override
+ public boolean remove(Widget w) {
+ if (childWidgets.contains(w)) {
+ orphan(w);
+ DOM.removeChild(DOM.getParent(w.getElement()), w
+ .getElement());
+ childWidgets.remove(w);
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ private void handleClickEvent(Event event, Element targetTdOrTr) {
+ if (emitClickEvents) {
+ boolean doubleClick = (DOM.eventGetType(event) == Event.ONDBLCLICK);
+
+ /* This row was clicked */
+ client.updateVariable(paintableId, "clickedKey", ""
+ + rowKey, false);
+
+ if (getElement() == targetTdOrTr.getParentElement()) {
+ /* A specific column was clicked */
+ int childIndex = DOM.getChildIndex(getElement(),
+ targetTdOrTr);
+ String colKey = null;
+ colKey = tHead.getHeaderCell(childIndex).getColKey();
+ client.updateVariable(paintableId, "clickedColKey",
+ colKey, false);
+ }
+
+ MouseEventDetails details = new MouseEventDetails(event);
+ // Note: the 'immediate' logic would need to be more
+ // involved (see #2104), but iscrolltable always sends
+ // select event, even though nullselectionallowed wont let
+ // the change trough. Will need to be updated if that is
+ // changed.
+ client
+ .updateVariable(
+ paintableId,
+ "clickEvent",
+ details.toString(),
+ !(!doubleClick
+ && selectMode > Table.SELECT_MODE_NONE && immediate));
+ }
+ }
+
+ /*
+ * React on click that occur on content cells only
+ */
+ @Override
+ public void onBrowserEvent(Event event) {
+ if (enabled) {
+ Element targetTdOrTr = getEventTargetTdOrTr(event);
+ if (targetTdOrTr != null) {
+ switch (DOM.eventGetType(event)) {
+ case Event.ONCLICK:
+ handleClickEvent(event, targetTdOrTr);
+ if (selectMode > Table.SELECT_MODE_NONE) {
+ toggleSelection();
+ // Note: changing the immediateness of this
+ // might
+ // require changes to "clickEvent" immediateness
+ // also.
+ client.updateVariable(paintableId, "selected",
+ selectedRowKeys.toArray(), immediate);
+ }
+ break;
+ case Event.ONDBLCLICK:
+ handleClickEvent(event, targetTdOrTr);
+ break;
+ case Event.ONCONTEXTMENU:
+ showContextMenu(event);
+ break;
+ default:
+ break;
+ }
+ }
+ }
+ super.onBrowserEvent(event);
+ }
+
+ /**
+ * Finds the TD that the event interacts with. Returns null if the
+ * target of the event should not be handled. If the event target is
+ * the row directly this method returns the TR element instead of
+ * the TD.
+ *
+ * @param event
+ * @return TD or TR element that the event targets (the actual event
+ * target is this element or a child of it)
+ */
+ private Element getEventTargetTdOrTr(Event event) {
+ Element targetTdOrTr = null;
+
+ final Element eventTarget = DOM.eventGetTarget(event);
+ final Element eventTargetParent = DOM.getParent(eventTarget);
+ final Element eventTargetGrandParent = DOM
+ .getParent(eventTargetParent);
+
+ final Element thisTrElement = getElement();
+
+ if (eventTarget == thisTrElement) {
+ // This was a click on the TR element
+ targetTdOrTr = eventTarget;
+ // rowTarget = true;
+ } else if (thisTrElement == eventTargetParent) {
+ // Target parent is the TR, so the actual target is the TD
+ targetTdOrTr = eventTarget;
+ } else if (thisTrElement == eventTargetGrandParent) {
+ // Target grand parent is the TR, so the parent is the TD
+ targetTdOrTr = eventTargetParent;
+ } else {
+ /*
+ * This is a workaround to make Labels and Embedded in a
+ * Table clickable (see #2688). It is really not a fix as it
+ * does not work for a custom component (not extending
+ * ILabel/IEmbedded) or for read only textfields etc.
+ */
+ Element tdElement = eventTargetParent;
+ while (DOM.getParent(tdElement) != thisTrElement) {
+ tdElement = DOM.getParent(tdElement);
+ }
+
+ Element componentElement = tdElement.getFirstChildElement()
+ .getFirstChildElement().cast();
+ Widget widget = (Widget) client
+ .getPaintable(componentElement);
+ if (widget instanceof ILabel || widget instanceof IEmbedded) {
+ targetTdOrTr = tdElement;
+ }
+ }
+
+ return targetTdOrTr;
+ }
+
+ public void showContextMenu(Event event) {
+ if (enabled && actionKeys != null) {
+ int left = event.getClientX();
+ int top = event.getClientY();
+ top += Window.getScrollTop();
+ left += Window.getScrollLeft();
+ client.getContextMenu().showAt(this, left, top);
+ }
+ event.cancelBubble(true);
+ event.preventDefault();
+ }
+
+ 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.vaadin.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 RenderSpace getAllocatedSpace(Widget child) {
+ int w = 0;
+ int i = getColIndexOf(child);
+ HeaderCell headerCell = tHead.getHeaderCell(i);
+ if (headerCell != null) {
+ if (initializedAndAttached) {
+ w = headerCell.getWidth();
+ } else {
+ // header offset width is not absolutely correct value,
+ // but a best guess (expecting similar content in all
+ // columns ->
+ // if one component is relative width so are others)
+ w = headerCell.getOffsetWidth() - getCellExtraWidth();
+ }
+ }
+ return new RenderSpace(w, getRowHeight());
+ }
+
+ private int getColIndexOf(Widget child) {
+ com.google.gwt.dom.client.Element widgetCell = child
+ .getElement().getParentElement().getParentElement();
+ NodeList<TableCellElement> cells = rowElement.getCells();
+ for (int i = 0; i < cells.getLength(); i++) {
+ if (cells.getItem(i) == widgetCell) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ public boolean hasChildComponent(Widget component) {
+ return childWidgets.contains(component);
+ }
+
+ public void replaceChildComponent(Widget oldComponent,
+ Widget newComponent) {
+ com.google.gwt.dom.client.Element parentElement = oldComponent
+ .getElement().getParentElement();
+ int index = childWidgets.indexOf(oldComponent);
+ oldComponent.removeFromParent();
+
+ parentElement.appendChild(newComponent.getElement());
+ childWidgets.insertElementAt(newComponent, index);
+ adopt(newComponent);
+
+ }
+
+ public boolean requestLayout(Set<Paintable> children) {
+ // row size should never change and system wouldn't event
+ // survive as this is a kind of fake paitable
+ return true;
+ }
+
+ public void updateCaption(Paintable component, UIDL uidl) {
+ // NOP, not rendered
+ }
+
+ public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+ // Should never be called,
+ // Component container interface faked here to get layouts
+ // render properly
+ }
+ }
+ }
+
+ 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();
+
+ }
+
+ @Override
+ public void setWidth(String width) {
+ if (this.width.equals(width)) {
+ return;
+ }
+
+ this.width = width;
+ if (width != null && !"".equals(width)) {
+ super.setWidth(width);
+ int innerPixels = getOffsetWidth() - getBorderWidth();
+ if (innerPixels < 0) {
+ innerPixels = 0;
+ }
+ setContentWidth(innerPixels);
+
+ if (!rendering) {
+ // readjust undefined width columns
+ lazyAdjustColumnWidths.cancel();
+ lazyAdjustColumnWidths.schedule(LAZY_COLUMN_ADJUST_TIMEOUT);
+ }
+
+ } else {
+ super.setWidth("");
+ }
+
+ }
+
+ private static final int LAZY_COLUMN_ADJUST_TIMEOUT = 300;
+
+ private final Timer lazyAdjustColumnWidths = new Timer() {
+ /**
+ * Check for column widths, and available width, to see if we can fix
+ * column widths "optimally". Doing this lazily to avoid expensive
+ * calculation when resizing is not yet finished.
+ */
+ @Override
+ public void run() {
+
+ Iterator<Widget> headCells = tHead.iterator();
+ int usedMinimumWidth = 0;
+ int totalExplicitColumnsWidths = 0;
+ float expandRatioDivider = 0;
+ int colIndex = 0;
+ while (headCells.hasNext()) {
+ final HeaderCell hCell = (HeaderCell) headCells.next();
+ if (hCell.isDefinedWidth()) {
+ totalExplicitColumnsWidths += hCell.getWidth();
+ usedMinimumWidth += hCell.getWidth();
+ } else {
+ usedMinimumWidth += hCell.getNaturalColumnWidth(colIndex);
+ expandRatioDivider += hCell.getExpandRatio();
+ }
+ colIndex++;
+ }
+
+ int availW = tBody.getAvailableWidth();
+ // Hey IE, are you really sure about this?
+ availW = tBody.getAvailableWidth();
+ availW -= tBody.getCellExtraWidth() * visibleColOrder.length;
+ if (willHaveScrollbars()) {
+ availW -= Util.getNativeScrollbarSize();
+ }
+
+ int extraSpace = availW - usedMinimumWidth;
+ if (extraSpace < 0) {
+ extraSpace = 0;
+ }
+
+ int totalUndefinedNaturaWidths = usedMinimumWidth
+ - totalExplicitColumnsWidths;
+
+ // we have some space that can be divided optimally
+ HeaderCell hCell;
+ colIndex = 0;
+ headCells = tHead.iterator();
+ while (headCells.hasNext()) {
+ hCell = (HeaderCell) headCells.next();
+ if (!hCell.isDefinedWidth()) {
+ int w = hCell.getNaturalColumnWidth(colIndex);
+ int newSpace;
+ if (expandRatioDivider > 0) {
+ // divide excess space by expand ratios
+ newSpace = (int) (w + extraSpace
+ * hCell.getExpandRatio() / expandRatioDivider);
+ } else {
+ if (totalUndefinedNaturaWidths != 0) {
+ // divide relatively to natural column widths
+ newSpace = w + extraSpace * w
+ / totalUndefinedNaturaWidths;
+ } else {
+ newSpace = w;
+ }
+ }
+ setColWidth(colIndex, newSpace, false);
+ }
+ colIndex++;
+ }
+ Util.runWebkitOverflowAutoFix(bodyContainer.getElement());
+ tBody.reLayoutComponents();
+ }
+ };
+
+ /**
+ * helper to set pixel size of head and body part
+ *
+ * @param pixels
+ */
+ private void setContentWidth(int pixels) {
+ tHead.setWidth(pixels + "px");
+ bodyContainer.setWidth(pixels + "px");
+ }
+
+ private int borderWidth = -1;
+
+ /**
+ * @return border left + border right
+ */
+ private int getBorderWidth() {
+ if (borderWidth < 0) {
+ borderWidth = Util.measureHorizontalPaddingAndBorder(bodyContainer
+ .getElement(), 2);
+ if (borderWidth < 0) {
+ borderWidth = 0;
+ }
+ }
+ return borderWidth;
+ }
+
+ /**
+ * Ensures scrollable area is properly sized.
+ */
+ private void setContainerHeight() {
+ if (height != null && !"".equals(height)) {
+ int contentH = getOffsetHeight() - tHead.getOffsetHeight();
+ contentH -= getContentAreaBorderHeight();
+ if (contentH < 0) {
+ contentH = 0;
+ }
+ bodyContainer.setHeight(contentH + "px");
+ }
+ }
+
+ private int contentAreaBorderHeight = -1;
+
+ /**
+ * @return border top + border bottom of the scrollable area of table
+ */
+ private int getContentAreaBorderHeight() {
+ if (contentAreaBorderHeight < 0) {
+ DOM.setStyleAttribute(bodyContainer.getElement(), "overflow",
+ "hidden");
+ contentAreaBorderHeight = bodyContainer.getOffsetHeight()
+ - bodyContainer.getElement().getPropertyInt("clientHeight");
+ DOM.setStyleAttribute(bodyContainer.getElement(), "overflow",
+ "auto");
+ }
+ return contentAreaBorderHeight;
+ }
+
+ @Override
+ public void setHeight(String height) {
+ this.height = height;
+ super.setHeight(height);
+ setContainerHeight();
+ }
+
+ /*
+ * Overridden due Table might not survive of visibility change (scroll pos
+ * lost). Example ITabPanel just set contained components invisible and back
+ * when changing tabs.
+ */
+ @Override
+ 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());
+ }
+ });
+ }
+ }
+ }
+ }
+
+ /**
+ * Helper function to build html snippet for column or row headers
+ *
+ * @param uidl
+ * possibly with values caption and icon
+ * @return html snippet containing possibly an icon + caption text
+ */
+ private String buildCaptionHtmlSnippet(UIDL uidl) {
+ String s = uidl.getStringAttribute("caption");
+ if (uidl.hasAttribute("icon")) {
+ s = "<img src=\""
+ + client.translateToolkitUri(uidl
+ .getStringAttribute("icon"))
+ + "\" alt=\"icon\" class=\"i-icon\">" + s;
+ }
+ return s;
+ }
+
+}
diff --git a/src/com/vaadin/terminal/gwt/client/ui/ISlider.java b/src/com/vaadin/terminal/gwt/client/ui/ISlider.java
new file mode 100644
index 0000000000..0cd04f6e89
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/ISlider.java
@@ -0,0 +1,436 @@
+/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+//
+package com.vaadin.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.Timer;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.BrowserInfo;
+import com.vaadin.terminal.gwt.client.ContainerResizedListener;
+import com.vaadin.terminal.gwt.client.Paintable;
+import com.vaadin.terminal.gwt.client.UIDL;
+
+public class ISlider extends Widget implements Paintable, Field,
+ ContainerResizedListener {
+
+ public static final String CLASSNAME = "i-slider";
+
+ /**
+ * Minimum size (width or height, depending on orientation) of the slider
+ * base.
+ */
+ private static final int MIN_SIZE = 50;
+
+ ApplicationConnection client;
+
+ String id;
+
+ private boolean immediate;
+ private boolean disabled;
+ private boolean readonly;
+ private boolean scrollbarStyle;
+
+ private int handleSize;
+ private double min;
+ private double max;
+ private int resolution;
+ private Double value;
+ private boolean vertical;
+ private int size = -1;
+ private boolean arrows;
+
+ /* DOM element for slider's base */
+ private final Element base;
+ private final int BASE_BORDER_WIDTH = 1;
+
+ /* DOM element for slider's handle */
+ private final Element handle;
+
+ /* DOM element for decrement arrow */
+ private final Element smaller;
+
+ /* DOM element for increment arrow */
+ private final Element bigger;
+
+ /* Temporary dragging/animation variables */
+ private boolean dragging = false;
+
+ public ISlider() {
+ super();
+
+ setElement(DOM.createDiv());
+ base = DOM.createDiv();
+ handle = DOM.createDiv();
+ smaller = DOM.createDiv();
+ bigger = DOM.createDiv();
+
+ setStyleName(CLASSNAME);
+ DOM.setElementProperty(base, "className", CLASSNAME + "-base");
+ DOM.setElementProperty(handle, "className", CLASSNAME + "-handle");
+ DOM.setElementProperty(smaller, "className", CLASSNAME + "-smaller");
+ DOM.setElementProperty(bigger, "className", CLASSNAME + "-bigger");
+
+ DOM.appendChild(getElement(), bigger);
+ DOM.appendChild(getElement(), smaller);
+ DOM.appendChild(getElement(), base);
+ DOM.appendChild(base, handle);
+
+ // Hide initially
+ DOM.setStyleAttribute(smaller, "display", "none");
+ DOM.setStyleAttribute(bigger, "display", "none");
+ DOM.setStyleAttribute(handle, "visibility", "hidden");
+
+ DOM.sinkEvents(getElement(), Event.MOUSEEVENTS | Event.ONMOUSEWHEEL);
+ DOM.sinkEvents(base, Event.ONCLICK);
+ DOM.sinkEvents(handle, Event.MOUSEEVENTS);
+ DOM.sinkEvents(smaller, Event.ONMOUSEDOWN | Event.ONMOUSEUP
+ | Event.ONMOUSEOUT);
+ DOM.sinkEvents(bigger, Event.ONMOUSEDOWN | Event.ONMOUSEUP
+ | Event.ONMOUSEOUT);
+ }
+
+ public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+
+ this.client = client;
+ id = uidl.getId();
+
+ // Ensure correct implementation
+ if (client.updateComponent(this, uidl, true)) {
+ return;
+ }
+
+ immediate = uidl.getBooleanAttribute("immediate");
+ disabled = uidl.getBooleanAttribute("disabled");
+ readonly = uidl.getBooleanAttribute("readonly");
+
+ vertical = uidl.hasAttribute("vertical");
+ arrows = uidl.hasAttribute("arrows");
+
+ String style = "";
+ if (uidl.hasAttribute("style")) {
+ style = uidl.getStringAttribute("style");
+ }
+
+ scrollbarStyle = style.indexOf("scrollbar") > -1;
+
+ if (arrows) {
+ DOM.setStyleAttribute(smaller, "display", "block");
+ DOM.setStyleAttribute(bigger, "display", "block");
+ }
+
+ if (vertical) {
+ addStyleName(CLASSNAME + "-vertical");
+ } else {
+ removeStyleName(CLASSNAME + "-vertical");
+ }
+
+ min = uidl.getDoubleAttribute("min");
+ max = uidl.getDoubleAttribute("max");
+ resolution = uidl.getIntAttribute("resolution");
+ value = new Double(uidl.getDoubleVariable("value"));
+
+ handleSize = uidl.getIntAttribute("hsize");
+
+ buildBase();
+
+ if (!vertical) {
+ // Draw handle with a delay to allow base to gain maximum width
+ DeferredCommand.addCommand(new Command() {
+ public void execute() {
+ buildHandle();
+ setValue(value, false);
+ }
+ });
+ } else {
+ buildHandle();
+ setValue(value, false);
+ }
+ }
+
+ private void buildBase() {
+ final String styleAttribute = vertical ? "height" : "width";
+ final String domProperty = vertical ? "offsetHeight" : "offsetWidth";
+
+ if (size == -1) {
+ final Element p = DOM.getParent(getElement());
+ if (DOM.getElementPropertyInt(p, domProperty) > 50) {
+ if (vertical) {
+ setHeight();
+ } else {
+ DOM.setStyleAttribute(base, styleAttribute, "");
+ }
+ } else {
+ // Set minimum size and adjust after all components have
+ // (supposedly) been drawn completely.
+ DOM.setStyleAttribute(base, styleAttribute, MIN_SIZE + "px");
+ DeferredCommand.addCommand(new Command() {
+ public void execute() {
+ final Element p = DOM.getParent(getElement());
+ if (DOM.getElementPropertyInt(p, domProperty) > (MIN_SIZE + 5)) {
+ if (vertical) {
+ setHeight();
+ } else {
+ DOM.setStyleAttribute(base, styleAttribute, "");
+ }
+ // Ensure correct position
+ setValue(value, false);
+ }
+ }
+ });
+ }
+ } else {
+ DOM.setStyleAttribute(base, styleAttribute, size + "px");
+ }
+
+ // TODO attach listeners for focusing and arrow keys
+ }
+
+ private void buildHandle() {
+ final String styleAttribute = vertical ? "height" : "width";
+ final String handleAttribute = vertical ? "marginTop" : "marginLeft";
+ final String domProperty = vertical ? "offsetHeight" : "offsetWidth";
+
+ DOM.setStyleAttribute(handle, handleAttribute, "0");
+
+ if (scrollbarStyle) {
+ // Only stretch the handle if scrollbar style is set.
+ int s = (int) (Double.parseDouble(DOM.getElementProperty(base,
+ domProperty)) / 100 * handleSize);
+ if (handleSize == -1) {
+ final int baseS = Integer.parseInt(DOM.getElementProperty(base,
+ domProperty));
+ final double range = (max - min) * (resolution + 1) * 3;
+ s = (int) (baseS - range);
+ }
+ if (s < 3) {
+ s = 3;
+ }
+ DOM.setStyleAttribute(handle, styleAttribute, s + "px");
+ } else {
+ DOM.setStyleAttribute(handle, styleAttribute, "");
+ }
+
+ // Restore visibility
+ DOM.setStyleAttribute(handle, "visibility", "visible");
+
+ }
+
+ private void setValue(Double value, boolean updateToServer) {
+ if (value == null) {
+ return;
+ }
+
+ if (value.doubleValue() < min) {
+ value = new Double(min);
+ } else if (value.doubleValue() > max) {
+ value = new Double(max);
+ }
+
+ // Update handle position
+ final String styleAttribute = vertical ? "marginTop" : "marginLeft";
+ final String domProperty = vertical ? "offsetHeight" : "offsetWidth";
+ final int handleSize = Integer.parseInt(DOM.getElementProperty(handle,
+ domProperty));
+ final int baseSize = Integer.parseInt(DOM.getElementProperty(base,
+ domProperty))
+ - (2 * BASE_BORDER_WIDTH);
+
+ final int range = baseSize - handleSize;
+ double v = value.doubleValue();
+ // Round value to resolution
+ if (resolution > 0) {
+ v = Math.round(v * Math.pow(10, resolution));
+ v = v / Math.pow(10, resolution);
+ } else {
+ v = Math.round(v);
+ }
+ final double valueRange = max - min;
+ double p = 0;
+ if (valueRange > 0) {
+ p = range * ((v - min) / valueRange);
+ }
+ if (p < 0) {
+ p = 0;
+ }
+ if (vertical) {
+ // IE6 rounding behaves a little unstable, reduce one pixel so the
+ // containing element (base) won't expand without limits
+ p = range - p - (BrowserInfo.get().isIE6() ? 1 : 0);
+ }
+ final double pos = p;
+
+ DOM.setStyleAttribute(handle, styleAttribute, (Math.round(pos)) + "px");
+
+ // TODO give more detailed info when dragging and do roundup
+ DOM.setElementAttribute(handle, "title", "" + v);
+
+ // Update value
+ this.value = new Double(v);
+
+ if (updateToServer) {
+ updateValueToServer();
+ }
+ }
+
+ @Override
+ public void onBrowserEvent(Event event) {
+ if (disabled || readonly) {
+ return;
+ }
+ final Element targ = DOM.eventGetTarget(event);
+
+ if (DOM.eventGetType(event) == Event.ONMOUSEWHEEL) {
+ processMouseWheelEvent(event);
+ } else if (dragging || targ == handle) {
+ processHandleEvent(event);
+ } else if (targ == smaller) {
+ decreaseValue(true);
+ } else if (targ == bigger) {
+ increaseValue(true);
+ } else {
+ processBaseEvent(event);
+ }
+ }
+
+ private Timer scrollTimer;
+
+ private void processMouseWheelEvent(final Event event) {
+ final int dir = DOM.eventGetMouseWheelVelocityY(event);
+
+ if (dir < 0) {
+ increaseValue(false);
+ } else {
+ decreaseValue(false);
+ }
+
+ if (scrollTimer != null) {
+ scrollTimer.cancel();
+ }
+ scrollTimer = new Timer() {
+ @Override
+ public void run() {
+ updateValueToServer();
+ }
+ };
+ scrollTimer.schedule(100);
+
+ DOM.eventPreventDefault(event);
+ DOM.eventCancelBubble(event, true);
+ }
+
+ private void processHandleEvent(Event event) {
+ switch (DOM.eventGetType(event)) {
+ case Event.ONMOUSEDOWN:
+ if (!disabled && !readonly) {
+ dragging = true;
+ DOM.setCapture(getElement());
+ DOM.eventPreventDefault(event); // prevent selecting text
+ DOM.eventCancelBubble(event, true);
+ }
+ break;
+ case Event.ONMOUSEMOVE:
+ if (dragging) {
+ // DOM.setCapture(getElement());
+ setValueByEvent(event, false);
+ }
+ break;
+ case Event.ONMOUSEUP:
+ dragging = false;
+ DOM.releaseCapture(getElement());
+ setValueByEvent(event, true);
+ break;
+ default:
+ break;
+ }
+ }
+
+ private void processBaseEvent(Event event) {
+ if (DOM.eventGetType(event) == Event.ONMOUSEDOWN) {
+ if (!disabled && !readonly && !dragging) {
+ setValueByEvent(event, true);
+ DOM.eventCancelBubble(event, true);
+ }
+ } else if (DOM.eventGetType(event) == Event.ONMOUSEDOWN && dragging) {
+ dragging = false;
+ DOM.releaseCapture(getElement());
+ setValueByEvent(event, true);
+ }
+ }
+
+ private void decreaseValue(boolean updateToServer) {
+ setValue(new Double(value.doubleValue() - Math.pow(10, -resolution)),
+ updateToServer);
+ }
+
+ private void increaseValue(boolean updateToServer) {
+ setValue(new Double(value.doubleValue() + Math.pow(10, -resolution)),
+ updateToServer);
+ }
+
+ private void setValueByEvent(Event event, boolean updateToServer) {
+ double v = min; // Fallback to min
+
+ final int coord = vertical ? DOM.eventGetClientY(event) : DOM
+ .eventGetClientX(event);
+ final String domProperty = vertical ? "offsetHeight" : "offsetWidth";
+
+ final double handleSize = Integer.parseInt(DOM.getElementProperty(
+ handle, domProperty));
+ final double baseSize = Integer.parseInt(DOM.getElementProperty(base,
+ domProperty));
+ final double baseOffset = vertical ? DOM.getAbsoluteTop(base)
+ - handleSize / 2 : DOM.getAbsoluteLeft(base) + handleSize / 2;
+
+ if (vertical) {
+ v = ((baseSize - (coord - baseOffset)) / (baseSize - handleSize))
+ * (max - min) + min;
+ } else {
+ v = ((coord - baseOffset) / (baseSize - handleSize)) * (max - min)
+ + min;
+ }
+
+ if (v < min) {
+ v = min;
+ } else if (v > max) {
+ v = max;
+ }
+
+ setValue(new Double(v), updateToServer);
+ }
+
+ public void iLayout() {
+ if (vertical) {
+ setHeight();
+ }
+ // Update handle position
+ setValue(value, false);
+ }
+
+ private void setHeight() {
+ if (size == -1) {
+ // Calculate decoration size
+ DOM.setStyleAttribute(base, "height", "0");
+ DOM.setStyleAttribute(base, "overflow", "hidden");
+ int h = DOM.getElementPropertyInt(getElement(), "offsetHeight");
+ if (h < MIN_SIZE) {
+ h = MIN_SIZE;
+ }
+ DOM.setStyleAttribute(base, "height", h + "px");
+ } else {
+ DOM.setStyleAttribute(base, "height", size + "px");
+ }
+ DOM.setStyleAttribute(base, "overflow", "");
+ }
+
+ private void updateValueToServer() {
+ client.updateVariable(id, "value", value.doubleValue(), immediate);
+ }
+
+}
diff --git a/src/com/vaadin/terminal/gwt/client/ui/ISplitPanel.java b/src/com/vaadin/terminal/gwt/client/ui/ISplitPanel.java
new file mode 100644
index 0000000000..74ca6b5125
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/ISplitPanel.java
@@ -0,0 +1,584 @@
+/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui;
+
+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.Event;
+import com.google.gwt.user.client.ui.ComplexPanel;
+import com.google.gwt.user.client.ui.RootPanel;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.BrowserInfo;
+import com.vaadin.terminal.gwt.client.Container;
+import com.vaadin.terminal.gwt.client.ContainerResizedListener;
+import com.vaadin.terminal.gwt.client.Paintable;
+import com.vaadin.terminal.gwt.client.RenderInformation;
+import com.vaadin.terminal.gwt.client.RenderSpace;
+import com.vaadin.terminal.gwt.client.UIDL;
+import com.vaadin.terminal.gwt.client.Util;
+
+public class ISplitPanel extends ComplexPanel implements Container,
+ 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 = false;
+
+ private String[] componentStyleNames;
+
+ private Element draggingCurtain;
+
+ private ApplicationConnection client;
+
+ private String width = "";
+
+ private String height = "";
+
+ private RenderSpace firstRenderSpace = new RenderSpace(0, 0, true);
+ private RenderSpace secondRenderSpace = new RenderSpace(0, 0, true);
+
+ RenderInformation renderInformation = new RenderInformation();
+
+ private String id;
+
+ private boolean immediate;
+
+ private boolean rendering = false;
+
+ 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, secondContainer);
+ DOM.appendChild(wrapper, firstContainer);
+ DOM.appendChild(wrapper, splitter);
+
+ DOM.setStyleAttribute(splitter, "position", "absolute");
+ DOM.setStyleAttribute(secondContainer, "position", "absolute");
+
+ DOM.setStyleAttribute(firstContainer, "overflow", "auto");
+ DOM.setStyleAttribute(secondContainer, "overflow", "auto");
+
+ }
+
+ private void setOrientation(int orientation) {
+ this.orientation = orientation;
+ if (orientation == ORIENTATION_HORIZONTAL) {
+ DOM.setStyleAttribute(splitter, "height", "100%");
+ DOM.setStyleAttribute(splitter, "top", "0");
+ DOM.setStyleAttribute(firstContainer, "height", "100%");
+ DOM.setStyleAttribute(secondContainer, "height", "100%");
+ } else {
+ DOM.setStyleAttribute(splitter, "width", "100%");
+ DOM.setStyleAttribute(splitter, "left", "0");
+ DOM.setStyleAttribute(firstContainer, "width", "100%");
+ DOM.setStyleAttribute(secondContainer, "width", "100%");
+ }
+
+ DOM.setElementProperty(firstContainer, "className", CLASSNAME
+ + "-first-container");
+ DOM.setElementProperty(secondContainer, "className", CLASSNAME
+ + "-second-container");
+ }
+
+ public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+ this.client = client;
+ id = uidl.getId();
+ rendering = true;
+
+ immediate = uidl.hasAttribute("immediate");
+
+ if (client.updateComponent(this, uidl, true)) {
+ rendering = false;
+ return;
+ }
+
+ if (uidl.hasAttribute("style")) {
+ componentStyleNames = uidl.getStringAttribute("style").split(" ");
+ } else {
+ componentStyleNames = new String[0];
+ }
+
+ setLocked(uidl.getBooleanAttribute("locked"));
+
+ setStylenames();
+
+ setSplitPosition(uidl.getStringAttribute("position"));
+
+ 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);
+
+ renderInformation.updateSize(getElement());
+
+ if (BrowserInfo.get().isIE7()) {
+ // Part III of IE7 hack
+ DeferredCommand.addCommand(new Command() {
+ public void execute() {
+ iLayout();
+ }
+ });
+ }
+ rendering = false;
+
+ }
+
+ private void setLocked(boolean newValue) {
+ if (locked != newValue) {
+ locked = newValue;
+ splitterSize = -1;
+ setStylenames();
+ }
+ }
+
+ private void setSplitPosition(String pos) {
+ if (orientation == ORIENTATION_HORIZONTAL) {
+ DOM.setStyleAttribute(splitter, "left", pos);
+ } else {
+ DOM.setStyleAttribute(splitter, "top", pos);
+ }
+ iLayout();
+ client.runDescendentsLayout(this);
+
+ }
+
+ /*
+ * Calculates absolutely positioned container places/sizes (non-Javadoc)
+ *
+ * @see com.vaadin.terminal.gwt.client.NeedsLayout#layout()
+ */
+ public void iLayout() {
+ if (!isAttached()) {
+ return;
+ }
+
+ renderInformation.updateSize(getElement());
+
+ int wholeSize;
+ int pixelPosition;
+
+ 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");
+
+ int contentHeight = renderInformation.getRenderedSize().getHeight();
+ firstRenderSpace.setHeight(contentHeight);
+ firstRenderSpace.setWidth(pixelPosition);
+ secondRenderSpace.setHeight(contentHeight);
+ secondRenderSpace.setWidth(secondContainerWidth);
+
+ 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");
+
+ int contentWidth = renderInformation.getRenderedSize().getWidth();
+ firstRenderSpace.setHeight(pixelPosition);
+ firstRenderSpace.setWidth(contentWidth);
+ secondRenderSpace.setHeight(secondContainerHeight);
+ secondRenderSpace.setWidth(contentWidth);
+
+ break;
+ }
+
+ // fixes scrollbars issues on webkit based browsers
+ Util.runWebkitOverflowAutoFix(secondContainer);
+ Util.runWebkitOverflowAutoFix(firstContainer);
+
+ }
+
+ 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;
+ }
+
+ @Override
+ 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 (trg == splitter || trg == DOM.getChild(splitter, 0)) {
+ resizing = true;
+ if (BrowserInfo.get().isGecko()) {
+ showDraggingCurtain();
+ }
+ 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();
+ // TODO Check if this is needed
+ client.runDescendentsLayout(this);
+
+ }
+
+ 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");
+ updateSplitPosition(newX);
+ }
+
+ 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");
+ updateSplitPosition(newY);
+ }
+
+ public void onMouseUp(Event event) {
+ DOM.releaseCapture(getElement());
+ if (BrowserInfo.get().isGecko()) {
+ hideDraggingCurtain();
+ }
+ resizing = false;
+ onMouseMove(event);
+ }
+
+ /**
+ * Used in FF to avoid losing mouse capture when pointer is moved on an
+ * iframe.
+ */
+ private void showDraggingCurtain() {
+ if (draggingCurtain == null) {
+ 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", ""
+ + IToolkitOverlay.Z_INDEX);
+ DOM.appendChild(RootPanel.getBodyElement(), draggingCurtain);
+ }
+ }
+
+ /**
+ * Hides dragging curtain
+ */
+ private void hideDraggingCurtain() {
+ if (draggingCurtain != null) {
+ DOM.removeChild(RootPanel.getBodyElement(), draggingCurtain);
+ draggingCurtain = null;
+ }
+ }
+
+ private 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;
+ }
+
+ @Override
+ public void setHeight(String height) {
+ if (this.height.equals(height)) {
+ return;
+ }
+
+ this.height = height;
+ super.setHeight(height);
+ if (!rendering && client != null) {
+ iLayout();
+ client.runDescendentsLayout(this);
+ }
+ }
+
+ @Override
+ public void setWidth(String width) {
+ if (this.width.equals(width)) {
+ return;
+ }
+
+ this.width = width;
+ super.setWidth(width);
+ if (!rendering && client != null) {
+ iLayout();
+ client.runDescendentsLayout(this);
+ }
+ }
+
+ public RenderSpace getAllocatedSpace(Widget child) {
+ if (child == firstChild) {
+ return firstRenderSpace;
+ } else if (child == secondChild) {
+ return secondRenderSpace;
+ }
+
+ return null;
+ }
+
+ public boolean hasChildComponent(Widget component) {
+ return (component != null && (component == firstChild || component == secondChild));
+ }
+
+ public void replaceChildComponent(Widget oldComponent, Widget newComponent) {
+ if (oldComponent == firstChild) {
+ setFirstWidget(newComponent);
+ } else if (oldComponent == secondChild) {
+ setSecondWidget(newComponent);
+ }
+ }
+
+ public boolean requestLayout(Set<Paintable> child) {
+ if (height != null && width != null) {
+ /*
+ * If the height and width has been specified the child components
+ * cannot make the size of the layout change
+ */
+
+ return true;
+ }
+
+ if (renderInformation.updateSize(getElement())) {
+ return false;
+ } else {
+ return true;
+ }
+
+ }
+
+ public void updateCaption(Paintable component, UIDL uidl) {
+ // TODO Implement caption handling
+ }
+
+ /**
+ * Updates the new split position back to server.
+ *
+ * @param pos
+ * The new position of the split handle.
+ */
+ private void updateSplitPosition(int pos) {
+ // We always send pixel values to server
+ client.updateVariable(id, "position", pos, immediate);
+ }
+
+ private void setStylenames() {
+ final String splitterSuffix = (orientation == ORIENTATION_HORIZONTAL ? "-hsplitter"
+ : "-vsplitter");
+ final String firstContainerSuffix = "-first-container";
+ final String secondContainerSuffix = "-second-container";
+ String lockedSuffix = "";
+
+ String splitterStyle = CLASSNAME + splitterSuffix;
+ String firstStyle = CLASSNAME + firstContainerSuffix;
+ String secondStyle = CLASSNAME + secondContainerSuffix;
+
+ if (locked) {
+ splitterStyle = CLASSNAME + splitterSuffix + "-locked";
+ lockedSuffix = "-locked";
+ }
+ for (int i = 0; i < componentStyleNames.length; i++) {
+ splitterStyle += " " + CLASSNAME + splitterSuffix + "-"
+ + componentStyleNames[i] + lockedSuffix;
+ firstStyle += " " + CLASSNAME + firstContainerSuffix + "-"
+ + componentStyleNames[i];
+ secondStyle += " " + CLASSNAME + secondContainerSuffix + "-"
+ + componentStyleNames[i];
+ }
+ DOM.setElementProperty(splitter, "className", splitterStyle);
+ DOM.setElementProperty(firstContainer, "className", firstStyle);
+ DOM.setElementProperty(secondContainer, "className", secondStyle);
+ }
+}
diff --git a/src/com/vaadin/terminal/gwt/client/ui/ISplitPanelHorizontal.java b/src/com/vaadin/terminal/gwt/client/ui/ISplitPanelHorizontal.java
new file mode 100644
index 0000000000..140881484b
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/ISplitPanelHorizontal.java
@@ -0,0 +1,12 @@
+/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui;
+
+public class ISplitPanelHorizontal extends ISplitPanel {
+
+ public ISplitPanelHorizontal() {
+ super(ISplitPanel.ORIENTATION_HORIZONTAL);
+ }
+}
diff --git a/src/com/vaadin/terminal/gwt/client/ui/ISplitPanelVertical.java b/src/com/vaadin/terminal/gwt/client/ui/ISplitPanelVertical.java
new file mode 100644
index 0000000000..eff00e8a43
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/ISplitPanelVertical.java
@@ -0,0 +1,12 @@
+/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui;
+
+public class ISplitPanelVertical extends ISplitPanel {
+
+ public ISplitPanelVertical() {
+ super(ISplitPanel.ORIENTATION_VERTICAL);
+ }
+}
diff --git a/src/com/vaadin/terminal/gwt/client/ui/ITablePaging.java b/src/com/vaadin/terminal/gwt/client/ui/ITablePaging.java
new file mode 100644
index 0000000000..224c549f3c
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/ITablePaging.java
@@ -0,0 +1,439 @@
+/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.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.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.Paintable;
+import com.vaadin.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));
+ }
+
+ @Override
+ 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/vaadin/terminal/gwt/client/ui/ITabsheet.java b/src/com/vaadin/terminal/gwt/client/ui/ITabsheet.java
new file mode 100644
index 0000000000..52f5a41062
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/ITabsheet.java
@@ -0,0 +1,841 @@
+/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Set;
+
+import com.google.gwt.dom.client.Style;
+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.ClickListener;
+import com.google.gwt.user.client.ui.ComplexPanel;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.BrowserInfo;
+import com.vaadin.terminal.gwt.client.ICaption;
+import com.vaadin.terminal.gwt.client.Paintable;
+import com.vaadin.terminal.gwt.client.RenderInformation;
+import com.vaadin.terminal.gwt.client.RenderSpace;
+import com.vaadin.terminal.gwt.client.UIDL;
+import com.vaadin.terminal.gwt.client.Util;
+
+public class ITabsheet extends ITabsheetBase {
+
+ private class TabSheetCaption extends ICaption {
+ TabSheetCaption() {
+ super(null, client);
+ }
+
+ @Override
+ public void onBrowserEvent(Event event) {
+ super.onBrowserEvent(event);
+ if (event.getTypeInt() == Event.ONLOAD) {
+ // icon onloads may change total width of tabsheet
+ if (isDynamicWidth()) {
+ updateDynamicWidth();
+ }
+ updateTabScroller();
+ }
+ }
+
+ @Override
+ public void setWidth(String width) {
+ super.setWidth(width);
+ if (BrowserInfo.get().isIE7()) {
+ /*
+ * IE7 apparently has problems with calculating width for
+ * floated elements inside a DIV with padding. Set the width
+ * explicitly for the caption.
+ */
+ fixTextWidth();
+ }
+ }
+
+ private void fixTextWidth() {
+ Element captionText = getTextElement();
+ int captionWidth = Util.getRequiredWidth(captionText);
+ int scrollWidth = captionText.getScrollWidth();
+ if (scrollWidth > captionWidth) {
+ captionWidth = scrollWidth;
+ }
+ captionText.getStyle().setPropertyPx("width", captionWidth);
+ }
+
+ }
+
+ class TabBar extends ComplexPanel implements ClickListener {
+
+ private Element tr = DOM.createTR();
+
+ private Element spacerTd = DOM.createTD();
+
+ TabBar() {
+ Element el = DOM.createTable();
+ Element tbody = DOM.createTBody();
+ DOM.appendChild(el, tbody);
+ DOM.appendChild(tbody, tr);
+ setStyleName(spacerTd, CLASSNAME + "-spacertd");
+ DOM.appendChild(tr, spacerTd);
+ DOM.appendChild(spacerTd, DOM.createDiv());
+ setElement(el);
+ }
+
+ protected Element getContainerElement() {
+ return tr;
+ }
+
+ private Widget oldSelected;
+
+ public int getTabCount() {
+ return getWidgetCount();
+ }
+
+ public void addTab(ICaption c) {
+ Element td = DOM.createTD();
+ setStyleName(td, CLASSNAME + "-tabitemcell");
+
+ if (getWidgetCount() == 0) {
+ setStyleName(td, CLASSNAME + "-tabitemcell-first", true);
+ }
+
+ Element div = DOM.createDiv();
+ setStyleName(div, CLASSNAME + "-tabitem");
+ DOM.appendChild(td, div);
+ DOM.insertBefore(tr, td, spacerTd);
+ c.addClickListener(this);
+ add(c, div);
+ }
+
+ public void onClick(Widget sender) {
+ int index = getWidgetIndex(sender);
+ onTabSelected(index);
+ }
+
+ public void selectTab(int index) {
+ Widget newSelected = getWidget(index);
+ Widget.setStyleName(DOM.getParent(newSelected.getElement()),
+ CLASSNAME + "-tabitem-selected", true);
+ if (oldSelected != null && oldSelected != newSelected) {
+ Widget.setStyleName(DOM.getParent(oldSelected.getElement()),
+ CLASSNAME + "-tabitem-selected", false);
+ }
+ oldSelected = newSelected;
+ }
+
+ public void removeTab(int i) {
+ Widget w = getWidget(i);
+ if (w == null) {
+ return;
+ }
+
+ Element caption = w.getElement();
+ Element div = DOM.getParent(caption);
+ Element td = DOM.getParent(div);
+ Element tr = DOM.getParent(td);
+ remove(w);
+
+ /*
+ * Widget is the Caption but we want to remove everything up to and
+ * including the parent TD
+ */
+
+ DOM.removeChild(tr, td);
+
+ /*
+ * If this widget was selected we need to unmark it as the last
+ * selected
+ */
+ if (w == oldSelected) {
+ oldSelected = null;
+ }
+ }
+
+ @Override
+ public boolean remove(Widget w) {
+ ((ICaption) w).removeClickListener(this);
+ return super.remove(w);
+ }
+
+ public TabSheetCaption getTab(int index) {
+ if (index >= getWidgetCount()) {
+ return null;
+ }
+ return (TabSheetCaption) getWidget(index);
+ }
+
+ public void setVisible(int index, boolean visible) {
+ Element e = DOM.getParent(getTab(index).getElement());
+ if (visible) {
+ DOM.setStyleAttribute(e, "display", "");
+ } else {
+ DOM.setStyleAttribute(e, "display", "none");
+ }
+ }
+
+ public void updateCaptionSize(int index) {
+ ICaption c = getTab(index);
+ c.setWidth(c.getRequiredWidth() + "px");
+
+ }
+
+ }
+
+ 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 = new TabBar();
+ private final ITabsheetPanel tp = new ITabsheetPanel();
+ private final Element contentNode, deco;
+
+ private final HashMap<String, ICaption> captions = new HashMap<String, ICaption>();
+
+ private String height;
+ private String width;
+
+ private boolean waitingForResponse;
+
+ private RenderInformation renderInformation = new RenderInformation();
+
+ /**
+ * 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 boolean rendering = false;
+
+ private void onTabSelected(final int tabIndex) {
+ if (disabled || waitingForResponse) {
+ return;
+ }
+ final Object tabKey = tabKeys.get(tabIndex);
+ if (disabledTabKeys.contains(tabKey)) {
+ return;
+ }
+ if (client != null && activeTabIndex != tabIndex) {
+ tb.selectTab(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(DOM.getParent(previousVisibleWidget
+ .getElement()), "visibility", "hidden");
+ client.updateVariable(id, "selected", tabKeys.get(tabIndex)
+ .toString(), true);
+ }
+ });
+ waitingForResponse = true;
+ }
+ }
+
+ private boolean isDynamicWidth() {
+ return width == null || width.equals("");
+ }
+
+ private boolean isDynamicHeight() {
+ return height == null || height.equals("");
+ }
+
+ 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
+ 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);
+
+ // TODO Use for Safari only. Fix annoying 1px first cell in TabBar.
+ // DOM.setStyleAttribute(DOM.getFirstChild(DOM.getFirstChild(DOM
+ // .getFirstChild(tb.getElement()))), "display", "none");
+
+ }
+
+ @Override
+ public void onBrowserEvent(Event event) {
+
+ // Tab scrolling
+ if (isScrolledTabs() && DOM.eventGetTarget(event) == scrollerPrev) {
+ if (scrollerIndex > 0) {
+ scrollerIndex--;
+ DOM.setStyleAttribute(DOM.getChild(DOM.getFirstChild(DOM
+ .getFirstChild(tb.getElement())), scrollerIndex),
+ "display", "");
+ tb.updateCaptionSize(scrollerIndex);
+ updateTabScroller();
+ }
+ } else if (isClippedTabs() && DOM.eventGetTarget(event) == scrollerNext) {
+ int tabs = tb.getTabCount();
+ if (scrollerIndex + 1 <= tabs) {
+ DOM.setStyleAttribute(DOM.getChild(DOM.getFirstChild(DOM
+ .getFirstChild(tb.getElement())), scrollerIndex),
+ "display", "none");
+ tb.updateCaptionSize(scrollerIndex);
+ scrollerIndex++;
+ updateTabScroller();
+ }
+ } else {
+ super.onBrowserEvent(event);
+ }
+ }
+
+ @Override
+ public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+ rendering = true;
+
+ super.updateFromUIDL(uidl, client);
+ if (cachedUpdate) {
+ return;
+ }
+
+ // 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 (!isDynamicWidth()) {
+ // FIXME: This makes tab sheet tabs go to 1px width on every update
+ // and then back to original 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");
+ updateDynamicWidth();
+ }
+
+ if (!isDynamicHeight()) {
+ // Must update height after the styles have been set
+ updateContentNodeHeight();
+ updateOpenTabSize();
+ }
+
+ iLayout();
+
+ // Re run relative size update to ensure optimal scrollbars
+ // TODO isolate to situation that visible tab has undefined height
+ try {
+ client.handleComponentRelativeSize(tp.getWidget(tp
+ .getVisibleWidget()));
+ } catch (Exception e) {
+ // Ignore, most likely empty tabsheet
+ }
+
+ renderInformation.updateSize(getElement());
+
+ waitingForResponse = false;
+ rendering = false;
+ }
+
+ private void updateDynamicWidth() {
+ // Find tab width
+ int tabsWidth = 0;
+
+ int count = tb.getTabCount();
+ for (int i = 0; i < count; i++) {
+ Element tabTd = tb.getTab(i).getElement().getParentElement().cast();
+ tabsWidth += tabTd.getOffsetWidth();
+ }
+
+ // Find content width
+ Style style = tp.getElement().getStyle();
+ String overflow = style.getProperty("overflow");
+ style.setProperty("overflow", "hidden");
+ style.setPropertyPx("width", tabsWidth);
+ Style wrapperstyle = tp.getWidget(tp.getVisibleWidget()).getElement()
+ .getParentElement().getStyle();
+ wrapperstyle.setPropertyPx("width", tabsWidth);
+ // Get content width from actual widget
+
+ int contentWidth = 0;
+ if (tp.getWidgetCount() > 0) {
+ contentWidth = tp.getWidget(tp.getVisibleWidget()).getOffsetWidth();
+ }
+ style.setProperty("overflow", overflow);
+
+ // Set widths to max(tabs,content)
+ if (tabsWidth < contentWidth) {
+ tabsWidth = contentWidth;
+ }
+
+ int outerWidth = tabsWidth + getContentAreaBorderWidth();
+
+ tabs.getStyle().setPropertyPx("width", outerWidth);
+ style.setPropertyPx("width", tabsWidth);
+ wrapperstyle.setPropertyPx("width", tabsWidth);
+
+ contentNode.getStyle().setPropertyPx("width", tabsWidth);
+ super.setWidth(outerWidth + "px");
+ updateOpenTabSize();
+ }
+
+ @Override
+ protected void renderTab(final UIDL tabUidl, int index, boolean selected,
+ boolean hidden) {
+ TabSheetCaption c = tb.getTab(index);
+ if (c == null) {
+ c = new TabSheetCaption();
+ tb.addTab(c);
+ }
+ c.updateCaption(tabUidl);
+
+ tb.setVisible(index, !hidden);
+
+ /*
+ * Force the width of the caption container so the content will not wrap
+ * and tabs won't be too narrow in certain browsers
+ */
+ c.setWidth(c.getRequiredWidth() + "px");
+ captions.put("" + index, c);
+
+ UIDL tabContentUIDL = null;
+ Paintable tabContent = null;
+ if (tabUidl.getChildCount() > 0) {
+ tabContentUIDL = tabUidl.getChildUIDL(0);
+ tabContent = client.getPaintable(tabContentUIDL);
+ }
+
+ if (tabContent != null) {
+ /* This is a tab with content information */
+
+ int oldIndex = tp.getWidgetIndex((Widget) tabContent);
+ if (oldIndex != -1 && oldIndex != index) {
+ /*
+ * The tab has previously been rendered in another position so
+ * we must move the cached content to correct position
+ */
+ tp.insert((Widget) tabContent, index);
+ }
+ } else {
+ /* A tab whose content has not yet been loaded */
+
+ /*
+ * Make sure there is a corresponding empty tab in tp. The same
+ * operation as the moving above but for not-loaded tabs.
+ */
+ if (index < tp.getWidgetCount()) {
+ Widget oldWidget = tp.getWidget(index);
+ if (!(oldWidget instanceof PlaceHolder)) {
+ tp.insert(new PlaceHolder(), index);
+ }
+ }
+
+ }
+
+ if (selected) {
+ renderContent(tabContentUIDL);
+ tb.selectTab(index);
+ } else {
+ if (tabContentUIDL != null) {
+ // updating a drawn child on hidden tab
+ if (tp.getWidgetIndex((Widget) tabContent) < 0) {
+ tp.insert((Widget) tabContent, index);
+ }
+ tabContent.updateFromUIDL(tabContentUIDL, client);
+ } else if (tp.getWidgetCount() <= index) {
+ tp.add(new PlaceHolder());
+ }
+ }
+ }
+
+ public class PlaceHolder extends ILabel {
+ public PlaceHolder() {
+ super("");
+ }
+ }
+
+ @Override
+ 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);
+ /*
+ * The size of a cached, relative sized component must be updated to
+ * report correct size to updateOpenTabSize().
+ */
+ if (contentUIDL.getBooleanAttribute("cached")) {
+ client.handleComponentRelativeSize((Widget) content);
+ }
+ updateOpenTabSize();
+ ITabsheet.this.removeStyleDependentName("loading");
+ if (previousVisibleWidget != null) {
+ DOM.setStyleAttribute(previousVisibleWidget.getElement(),
+ "visibility", "");
+ previousVisibleWidget = null;
+ }
+ }
+
+ @Override
+ public void setHeight(String height) {
+ super.setHeight(height);
+ this.height = height;
+ updateContentNodeHeight();
+
+ if (!rendering) {
+ updateOpenTabSize();
+ iLayout();
+ // TODO Check if this is needed
+ client.runDescendentsLayout(this);
+ }
+ }
+
+ private void updateContentNodeHeight() {
+ if (height != null && !"".equals(height)) {
+ int contentHeight = getOffsetHeight();
+ contentHeight -= DOM.getElementPropertyInt(deco, "offsetHeight");
+ contentHeight -= tb.getOffsetHeight();
+ if (contentHeight < 0) {
+ contentHeight = 0;
+ }
+
+ // Set proper values for content element
+ DOM.setStyleAttribute(contentNode, "height", contentHeight + "px");
+ renderSpace.setHeight(contentHeight);
+ } else {
+ DOM.setStyleAttribute(contentNode, "height", "");
+ renderSpace.setHeight(0);
+ }
+ }
+
+ @Override
+ public void setWidth(String width) {
+ if ((this.width == null && width.equals(""))
+ || (this.width != null && this.width.equals(width))) {
+ return;
+ }
+
+ super.setWidth(width);
+ if (width.equals("")) {
+ width = null;
+ }
+ this.width = width;
+ if (width == null) {
+ renderSpace.setWidth(0);
+ contentNode.getStyle().setProperty("width", "");
+ } else {
+ int contentWidth = getOffsetWidth() - getContentAreaBorderWidth();
+ if (contentWidth < 0) {
+ contentWidth = 0;
+ }
+ contentNode.getStyle().setProperty("width", contentWidth + "px");
+ renderSpace.setWidth(contentWidth);
+ }
+
+ if (!rendering) {
+ if (isDynamicHeight()) {
+ Util.updateRelativeChildrenAndSendSizeUpdateEvent(client, tp,
+ this);
+ }
+
+ updateOpenTabSize();
+ iLayout();
+ // TODO Check if this is needed
+ client.runDescendentsLayout(this);
+
+ }
+
+ }
+
+ public void iLayout() {
+ updateTabScroller();
+ tp.runWebkitOverflowAutoFix();
+ }
+
+ /**
+ * Sets the size of the visible tab (component). As the tab is set to
+ * position: absolute (to work around a firefox flickering bug) we must keep
+ * this up-to-date by hand.
+ */
+ private void updateOpenTabSize() {
+ /*
+ * The overflow=auto element must have a height specified, otherwise it
+ * will be just as high as the contents and no scrollbars will appear
+ */
+ int height = -1;
+ int width = -1;
+ int minWidth = 0;
+
+ if (!isDynamicHeight()) {
+ height = renderSpace.getHeight();
+ }
+ if (!isDynamicWidth()) {
+ width = renderSpace.getWidth();
+ } else {
+ /*
+ * If the tabbar is wider than the content we need to use the tabbar
+ * width as minimum width so scrollbars get placed correctly (at the
+ * right edge).
+ */
+ minWidth = tb.getOffsetWidth() - getContentAreaBorderWidth();
+ }
+ tp.fixVisibleTabSize(width, height, minWidth);
+
+ }
+
+ /**
+ * 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");
+ }
+
+ if (BrowserInfo.get().isSafari()) {
+ // fix tab height for safari, bugs sometimes if tabs contain icons
+ String property = tabs.getStyle().getProperty("height");
+ if (property == null || property.equals("")) {
+ tabs.getStyle().setPropertyPx("height", tb.getOffsetHeight());
+ }
+ /*
+ * another hack for webkits. tabscroller sometimes drops without
+ * "shaking it" reproducable in
+ * com.vaadin.tests.components.tabsheet.TabSheetIcons
+ */
+ final Style style = scroller.getStyle();
+ style.setProperty("whiteSpace", "normal");
+ DeferredCommand.addCommand(new Command() {
+ public void execute() {
+ style.setProperty("whiteSpace", "");
+ }
+ });
+ }
+
+ }
+
+ private void showAllTabs() {
+ scrollerIndex = 0;
+ Element tr = DOM.getFirstChild(DOM.getFirstChild(tb.getElement()));
+ for (int i = 0; i < tb.getTabCount(); i++) {
+ DOM.setStyleAttribute(DOM.getChild(tr, i), "display", "");
+ }
+ }
+
+ private boolean isScrolledTabs() {
+ return scrollerIndex > 0;
+ }
+
+ private boolean isClippedTabs() {
+ return tb.getOffsetWidth() > getOffsetWidth();
+ }
+
+ @Override
+ protected void clearPaintables() {
+
+ int i = tb.getTabCount();
+ while (i > 0) {
+ tb.removeTab(--i);
+ }
+ tp.clear();
+
+ }
+
+ @Override
+ protected Iterator getPaintableIterator() {
+ return tp.iterator();
+ }
+
+ public boolean hasChildComponent(Widget component) {
+ if (tp.getWidgetIndex(component) < 0) {
+ return false;
+ } else {
+ return true;
+ }
+ }
+
+ public void replaceChildComponent(Widget oldComponent, Widget newComponent) {
+ tp.replaceComponent(oldComponent, newComponent);
+ }
+
+ public void updateCaption(Paintable component, UIDL uidl) {
+ /* Tabsheet does not render its children's captions */
+ }
+
+ public boolean requestLayout(Set<Paintable> child) {
+ if (!isDynamicHeight() && !isDynamicWidth()) {
+ /*
+ * If the height and width has been specified for this container the
+ * child components cannot make the size of the layout change
+ */
+
+ return true;
+ }
+
+ updateOpenTabSize();
+
+ if (renderInformation.updateSize(getElement())) {
+ /*
+ * Size has changed so we let the child components know about the
+ * new size.
+ */
+ iLayout();
+ client.runDescendentsLayout(this);
+
+ return false;
+ } else {
+ /*
+ * Size has not changed so we do not need to propagate the event
+ * further
+ */
+ return true;
+ }
+
+ }
+
+ private int borderW = -1;
+
+ private int getContentAreaBorderWidth() {
+ if (borderW < 0) {
+ borderW = Util.measureHorizontalBorder(contentNode);
+ }
+ return borderW;
+ }
+
+ private RenderSpace renderSpace = new RenderSpace(0, 0, true);
+
+ public RenderSpace getAllocatedSpace(Widget child) {
+ // All tabs have equal amount of space allocated
+ return renderSpace;
+ }
+
+ @Override
+ protected int getTabCount() {
+ return tb.getWidgetCount();
+ }
+
+ @Override
+ protected Paintable getTab(int index) {
+ if (tp.getWidgetCount() > index) {
+ return (Paintable) tp.getWidget(index);
+ }
+ return null;
+ }
+
+ @Override
+ protected void removeTab(int index) {
+ tb.removeTab(index);
+ /*
+ * This must be checked because renderTab automatically removes the
+ * active tab content when it changes
+ */
+ if (tp.getWidgetCount() > index) {
+ tp.remove(index);
+ }
+ }
+
+}
diff --git a/src/com/vaadin/terminal/gwt/client/ui/ITabsheetBase.java b/src/com/vaadin/terminal/gwt/client/ui/ITabsheetBase.java
new file mode 100644
index 0000000000..081f5977aa
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/ITabsheetBase.java
@@ -0,0 +1,144 @@
+package com.vaadin.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.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.Container;
+import com.vaadin.terminal.gwt.client.Paintable;
+import com.vaadin.terminal.gwt.client.UIDL;
+
+abstract class ITabsheetBase extends ComplexPanel implements Container {
+
+ String id;
+ ApplicationConnection client;
+
+ protected final ArrayList tabKeys = new ArrayList();
+ protected int activeTabIndex = 0;
+ protected boolean disabled;
+ protected boolean readonly;
+ protected Set disabledTabKeys = new HashSet();
+ protected boolean cachedUpdate = false;
+
+ public ITabsheetBase(String classname) {
+ setElement(DOM.createDiv());
+ setStylePrimaryName(classname);
+ }
+
+ public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+ this.client = client;
+
+ // Ensure correct implementation
+ cachedUpdate = client.updateComponent(this, uidl, true);
+ if (cachedUpdate) {
+ return;
+ }
+
+ // Update member references
+ id = uidl.getId();
+ disabled = uidl.hasAttribute("disabled");
+
+ // Render content
+ final UIDL tabs = uidl.getChildUIDL(0);
+
+ // Paintables in the TabSheet before update
+ ArrayList oldPaintables = new ArrayList();
+ for (Iterator iterator = getPaintableIterator(); iterator.hasNext();) {
+ oldPaintables.add(iterator.next());
+ }
+
+ // Clear previous values
+ tabKeys.clear();
+ disabledTabKeys.clear();
+
+ 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");
+ final boolean hidden = tab.getBooleanAttribute("hidden");
+
+ if (tab.getBooleanAttribute("disabled")) {
+ disabledTabKeys.add(key);
+ }
+
+ tabKeys.add(key);
+
+ if (selected) {
+ activeTabIndex = index;
+ }
+ renderTab(tab, index, selected, hidden);
+ index++;
+ }
+
+ int tabCount = getTabCount();
+ while (tabCount-- > index) {
+ removeTab(index);
+ }
+
+ for (int i = 0; i < getTabCount(); i++) {
+ Paintable p = getTab(i);
+ oldPaintables.remove(p);
+ }
+
+ // Perform unregister for any paintables removed during update
+ for (Iterator iterator = oldPaintables.iterator(); iterator.hasNext();) {
+ Object oldPaintable = iterator.next();
+ if (oldPaintable instanceof Paintable) {
+ Widget w = (Widget) oldPaintable;
+ if (w.isAttached()) {
+ w.removeFromParent();
+ }
+ client.unregisterPaintable((Paintable) oldPaintable);
+ }
+ }
+
+ }
+
+ /**
+ * @return a list of currently shown Paintables
+ */
+ abstract protected Iterator getPaintableIterator();
+
+ /**
+ * Clears current tabs and contents
+ */
+ abstract protected void clearPaintables();
+
+ /**
+ * 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, boolean hidden);
+
+ /**
+ * 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);
+
+ /**
+ * Implement in extending classes. This method should return the number of
+ * tabs currently rendered.
+ */
+ protected abstract int getTabCount();
+
+ /**
+ * Implement in extending classes. This method should return the Paintable
+ * corresponding to the given index.
+ */
+ protected abstract Paintable getTab(int index);
+
+ /**
+ * Implement in extending classes. This method should remove the rendered
+ * tab with the specified index.
+ */
+ protected abstract void removeTab(int index);
+}
diff --git a/src/com/vaadin/terminal/gwt/client/ui/ITabsheetPanel.java b/src/com/vaadin/terminal/gwt/client/ui/ITabsheetPanel.java
new file mode 100644
index 0000000000..776e06b17b
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/ITabsheetPanel.java
@@ -0,0 +1,183 @@
+/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.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.ComplexPanel;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.terminal.gwt.client.Util;
+
+/**
+ * A panel that displays all of its child widgets in a 'deck', where only one
+ * can be visible at a time. It is used by
+ * {@link com.vaadin.terminal.gwt.client.ui.ITabsheet}.
+ *
+ * This class has the same basic functionality as the GWT DeckPanel
+ * {@link com.google.gwt.user.client.ui.DeckPanel}, with the exception that it
+ * doesn't manipulate the child widgets' width and height attributes.
+ */
+public class ITabsheetPanel extends ComplexPanel {
+
+ private Widget visibleWidget;
+
+ /**
+ * Creates an empty tabsheet panel.
+ */
+ public ITabsheetPanel() {
+ setElement(DOM.createDiv());
+ }
+
+ /**
+ * Adds the specified widget to the deck.
+ *
+ * @param w
+ * the widget to be added
+ */
+ @Override
+ public void add(Widget w) {
+ Element el = createContainerElement();
+ DOM.appendChild(getElement(), el);
+ super.add(w, el);
+ }
+
+ private Element createContainerElement() {
+ Element el = DOM.createDiv();
+ DOM.setStyleAttribute(el, "position", "absolute");
+ DOM.setStyleAttribute(el, "overflow", "auto");
+ hide(el);
+ return el;
+ }
+
+ /**
+ * Gets the index of the currently-visible widget.
+ *
+ * @return the visible widget's index
+ */
+ public int getVisibleWidget() {
+ return getWidgetIndex(visibleWidget);
+ }
+
+ /**
+ * Inserts a widget before the specified index.
+ *
+ * @param w
+ * the widget to be inserted
+ * @param beforeIndex
+ * the index before which it will be inserted
+ * @throws IndexOutOfBoundsException
+ * if <code>beforeIndex</code> is out of range
+ */
+ public void insert(Widget w, int beforeIndex) {
+ Element el = createContainerElement();
+ DOM.insertChild(getElement(), el, beforeIndex);
+ super.insert(w, el, beforeIndex, false);
+ }
+
+ @Override
+ public boolean remove(Widget w) {
+ Element child = w.getElement();
+ Element parent = null;
+ if (child != null) {
+ parent = DOM.getParent(child);
+ }
+ final boolean removed = super.remove(w);
+ if (removed) {
+ if (visibleWidget == w) {
+ visibleWidget = null;
+ }
+ if (parent != null) {
+ DOM.removeChild(getElement(), parent);
+ }
+ }
+ return removed;
+ }
+
+ /**
+ * Shows the widget at the specified index. This causes the currently-
+ * visible widget to be hidden.
+ *
+ * @param index
+ * the index of the widget to be shown
+ */
+ public void showWidget(int index) {
+ checkIndexBoundsForAccess(index);
+ Widget newVisible = getWidget(index);
+ if (visibleWidget != newVisible) {
+ if (visibleWidget != null) {
+ hide(DOM.getParent(visibleWidget.getElement()));
+ }
+ visibleWidget = newVisible;
+ unHide(DOM.getParent(visibleWidget.getElement()));
+ }
+ }
+
+ private void hide(Element e) {
+ DOM.setStyleAttribute(e, "visibility", "hidden");
+ DOM.setStyleAttribute(e, "top", "-100000px");
+ DOM.setStyleAttribute(e, "left", "-100000px");
+ }
+
+ private void unHide(Element e) {
+ DOM.setStyleAttribute(e, "top", "0px");
+ DOM.setStyleAttribute(e, "left", "0px");
+ DOM.setStyleAttribute(e, "visibility", "");
+ }
+
+ public void fixVisibleTabSize(int width, int height, int minWidth) {
+ if (visibleWidget == null) {
+ return;
+ }
+
+ boolean dynamicHeight = false;
+
+ if (height < 0) {
+ height = visibleWidget.getOffsetHeight();
+ dynamicHeight = true;
+ }
+ if (width < 0) {
+ width = visibleWidget.getOffsetWidth();
+ }
+ if (width < minWidth) {
+ width = minWidth;
+ }
+
+ Element wrapperDiv = (Element) visibleWidget.getElement()
+ .getParentElement();
+
+ // width first
+ getElement().getStyle().setPropertyPx("width", width);
+ wrapperDiv.getStyle().setPropertyPx("width", width);
+
+ if (dynamicHeight) {
+ // height of widget might have changed due wrapping
+ height = visibleWidget.getOffsetHeight();
+ }
+ // i-tabsheet-tabsheetpanel height
+ getElement().getStyle().setPropertyPx("height", height);
+
+ // widget wrapper height
+ wrapperDiv.getStyle().setPropertyPx("height", height);
+ runWebkitOverflowAutoFix();
+ }
+
+ public void runWebkitOverflowAutoFix() {
+ if (visibleWidget != null) {
+ Util.runWebkitOverflowAutoFix(DOM.getParent(visibleWidget
+ .getElement()));
+ }
+
+ }
+
+ public void replaceComponent(Widget oldComponent, Widget newComponent) {
+ boolean isVisible = (visibleWidget == oldComponent);
+ int widgetIndex = getWidgetIndex(oldComponent);
+ remove(oldComponent);
+ insert(newComponent, widgetIndex);
+ if (isVisible) {
+ showWidget(widgetIndex);
+ }
+ }
+}
diff --git a/src/com/vaadin/terminal/gwt/client/ui/ITextArea.java b/src/com/vaadin/terminal/gwt/client/ui/ITextArea.java
new file mode 100644
index 0000000000..7a03f85542
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/ITextArea.java
@@ -0,0 +1,72 @@
+/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.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.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.UIDL;
+
+/**
+ * This class represents a multiline textfield (textarea).
+ *
+ * TODO consider replacing this with a RichTextArea based implementation. IE
+ * does not support CSS height for textareas in Strict mode :-(
+ *
+ * @author IT Mill Ltd.
+ *
+ */
+public class ITextArea extends ITextField {
+ public static final String CLASSNAME = "i-textarea";
+
+ public ITextArea() {
+ super(DOM.createTextArea());
+ setStyleName(CLASSNAME);
+ }
+
+ @Override
+ public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+ // Call parent renderer explicitly
+ super.updateFromUIDL(uidl, client);
+
+ if (uidl.hasAttribute("rows")) {
+ setRows(new Integer(uidl.getStringAttribute("rows")).intValue());
+ }
+
+ if (getMaxLength() >= 0) {
+ sinkEvents(Event.ONKEYPRESS);
+ }
+ }
+
+ public void setRows(int rows) {
+ setRows(getElement(), rows);
+ }
+
+ private native void setRows(Element e, int r)
+ /*-{
+ try {
+ if(e.tagName.toLowerCase() == "textarea")
+ e.rows = r;
+ } catch (e) {}
+ }-*/;
+
+ @Override
+ public void onBrowserEvent(Event event) {
+ if (getMaxLength() >= 0 && event.getTypeInt() == Event.ONKEYPRESS) {
+ DeferredCommand.addCommand(new Command() {
+ public void execute() {
+ if (getText().length() > getMaxLength()) {
+ setText(getText().substring(0, getMaxLength()));
+ }
+ }
+ });
+ }
+ super.onBrowserEvent(event);
+ }
+
+}
diff --git a/src/com/vaadin/terminal/gwt/client/ui/ITextField.java b/src/com/vaadin/terminal/gwt/client/ui/ITextField.java
new file mode 100644
index 0000000000..74cd1cf329
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/ITextField.java
@@ -0,0 +1,270 @@
+/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.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.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.BrowserInfo;
+import com.vaadin.terminal.gwt.client.ITooltip;
+import com.vaadin.terminal.gwt.client.Paintable;
+import com.vaadin.terminal.gwt.client.UIDL;
+import com.vaadin.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 {
+
+ /**
+ * 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 String valueBeforeEdit = null;
+
+ private boolean immediate = false;
+ private int extraHorizontalPixels = -1;
+ private int extraVerticalPixels = -1;
+ private int maxLength = -1;
+
+ private static final String CLASSNAME_PROMPT = "prompt";
+ private static final String ATTR_INPUTPROMPT = "prompt";
+ private String inputPrompt = null;
+ private boolean prompting = false;
+
+ public ITextField() {
+ this(DOM.createInputText());
+ }
+
+ protected ITextField(Element node) {
+ super(node);
+ if (BrowserInfo.get().isIE()) {
+ // Fixes IE margin problem (#2058)
+ DOM.setStyleAttribute(node, "marginTop", "-1px");
+ DOM.setStyleAttribute(node, "marginBottom", "-1px");
+ }
+ setStyleName(CLASSNAME);
+ addChangeListener(this);
+ addFocusListener(this);
+ sinkEvents(ITooltip.TOOLTIP_EVENTS);
+ }
+
+ @Override
+ 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);
+ }
+
+ inputPrompt = uidl.getStringAttribute(ATTR_INPUTPROMPT);
+
+ setMaxLength(uidl.hasAttribute("maxLength") ? uidl
+ .getIntAttribute("maxLength") : -1);
+
+ immediate = uidl.getBooleanAttribute("immediate");
+
+ if (uidl.hasAttribute("cols")) {
+ setColumns(new Integer(uidl.getStringAttribute("cols")).intValue());
+ }
+
+ String text = uidl.getStringVariable("text");
+ prompting = inputPrompt != null && (text == null || text.equals(""));
+ if (prompting) {
+ setText(inputPrompt);
+ addStyleDependentName(CLASSNAME_PROMPT);
+ } else {
+ setText(text);
+ removeStyleDependentName(CLASSNAME_PROMPT);
+ }
+ valueBeforeEdit = uidl.getStringVariable("text");
+ }
+
+ private void setMaxLength(int newMaxLength) {
+ if (newMaxLength > 0) {
+ maxLength = newMaxLength;
+ if (getElement().getTagName().toLowerCase().equals("textarea")) {
+ // NOP no maxlength property for textarea
+ } else {
+ getElement().setPropertyInt("maxLength", maxLength);
+ }
+ } else if (maxLength != -1) {
+ if (getElement().getTagName().toLowerCase().equals("textarea")) {
+ // NOP no maxlength property for textarea
+ } else {
+ getElement().setAttribute("maxlength", "");
+ }
+ maxLength = -1;
+ }
+
+ }
+
+ protected int getMaxLength() {
+ return maxLength;
+ }
+
+ public void onChange(Widget sender) {
+ if (client != null && id != null) {
+ String newText = getText();
+ if (!prompting && newText != null
+ && !newText.equals(valueBeforeEdit)) {
+ client.updateVariable(id, "text", getText(), immediate);
+ valueBeforeEdit = newText;
+ }
+ }
+ }
+
+ private static ITextField focusedTextField;
+
+ public static void flushChangesFromFocusedTextField() {
+ if (focusedTextField != null) {
+ focusedTextField.onChange(null);
+ }
+ }
+
+ public void onFocus(Widget sender) {
+ addStyleDependentName(CLASSNAME_FOCUS);
+ if (prompting) {
+ setText("");
+ removeStyleDependentName(CLASSNAME_PROMPT);
+ }
+ focusedTextField = this;
+ }
+
+ public void onLostFocus(Widget sender) {
+ removeStyleDependentName(CLASSNAME_FOCUS);
+ focusedTextField = null;
+ String text = getText();
+ prompting = inputPrompt != null && (text == null || "".equals(text));
+ if (prompting) {
+ setText(inputPrompt);
+ addStyleDependentName(CLASSNAME_PROMPT);
+ }
+ onChange(sender);
+ }
+
+ 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) {}
+ }-*/;
+
+ /**
+ * @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);
+ }
+
+ @Override
+ public void setHeight(String height) {
+ if (height.endsWith("px")) {
+ int h = Integer.parseInt(height.substring(0, height.length() - 2));
+ h -= getExtraVerticalPixels();
+ if (h < 0) {
+ h = 0;
+ }
+
+ super.setHeight(h + "px");
+ } else {
+ super.setHeight(height);
+ }
+ }
+
+ @Override
+ public void setWidth(String width) {
+ if (width.endsWith("px")) {
+ int w = Integer.parseInt(width.substring(0, width.length() - 2));
+ w -= getExtraHorizontalPixels();
+ if (w < 0) {
+ w = 0;
+ }
+
+ super.setWidth(w + "px");
+ } else {
+ super.setWidth(width);
+ }
+ }
+
+}
diff --git a/src/com/vaadin/terminal/gwt/client/ui/ITextualDate.java b/src/com/vaadin/terminal/gwt/client/ui/ITextualDate.java
new file mode 100644
index 0000000000..311390cbcc
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/ITextualDate.java
@@ -0,0 +1,301 @@
+/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui;
+
+import java.util.Date;
+
+import com.google.gwt.i18n.client.DateTimeFormat;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.ui.ChangeListener;
+import com.google.gwt.user.client.ui.TextBox;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.BrowserInfo;
+import com.vaadin.terminal.gwt.client.ClientExceptionHandler;
+import com.vaadin.terminal.gwt.client.ContainerResizedListener;
+import com.vaadin.terminal.gwt.client.Focusable;
+import com.vaadin.terminal.gwt.client.LocaleNotLoadedException;
+import com.vaadin.terminal.gwt.client.LocaleService;
+import com.vaadin.terminal.gwt.client.Paintable;
+import com.vaadin.terminal.gwt.client.UIDL;
+
+public class ITextualDate extends IDateField implements Paintable, Field,
+ ChangeListener, ContainerResizedListener, Focusable {
+
+ private static final String PARSE_ERROR_CLASSNAME = CLASSNAME
+ + "-parseerror";
+
+ private final TextBox text;
+
+ private String formatStr;
+
+ private String width;
+
+ private boolean needLayout;
+
+ protected int fieldExtraWidth = -1;
+
+ public ITextualDate() {
+ super();
+ text = new TextBox();
+ // use normal textfield styles as a basis
+ text.setStyleName(ITextField.CLASSNAME);
+ // add datefield spesific style name also
+ text.addStyleName(CLASSNAME + "-textfield");
+ text.addChangeListener(this);
+ add(text);
+ }
+
+ @Override
+ public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+
+ int origRes = currentResolution;
+ super.updateFromUIDL(uidl, client);
+ if (origRes != currentResolution) {
+ // force recreating format string
+ formatStr = null;
+ }
+ if (uidl.hasAttribute("format")) {
+ formatStr = uidl.getStringAttribute("format");
+ }
+
+ buildDate();
+ // not a FocusWidget -> needs own tabindex handling
+ if (uidl.hasAttribute("tabindex")) {
+ text.setTabIndex(uidl.getIntAttribute("tabindex"));
+ }
+ }
+
+ protected String getFormatString() {
+ if (formatStr == null) {
+ if (currentResolution == RESOLUTION_YEAR) {
+ formatStr = "yyyy"; // force full year
+ } else {
+
+ try {
+ String frmString = LocaleService
+ .getDateFormat(currentLocale);
+ frmString = cleanFormat(frmString);
+ String delim = LocaleService
+ .getClockDelimiter(currentLocale);
+
+ if (currentResolution >= RESOLUTION_HOUR) {
+ if (dts.isTwelveHourClock()) {
+ frmString += " hh";
+ } else {
+ frmString += " HH";
+ }
+ if (currentResolution >= RESOLUTION_MIN) {
+ frmString += ":mm";
+ if (currentResolution >= RESOLUTION_SEC) {
+ frmString += ":ss";
+ if (currentResolution >= RESOLUTION_MSEC) {
+ frmString += ".SSS";
+ }
+ }
+ }
+ if (dts.isTwelveHourClock()) {
+ frmString += " aaa";
+ }
+
+ }
+
+ formatStr = frmString;
+ } catch (LocaleNotLoadedException e) {
+ ClientExceptionHandler.displayError(e);
+ }
+ }
+ }
+ return formatStr;
+ }
+
+ /**
+ *
+ */
+ protected void buildDate() {
+ removeStyleName(PARSE_ERROR_CLASSNAME);
+ // Create the initial text for the textfield
+ String dateText;
+ if (date != null) {
+ dateText = DateTimeFormat.getFormat(getFormatString()).format(date);
+ } else {
+ dateText = "";
+ }
+
+ text.setText(dateText);
+ text.setEnabled(enabled && !readonly);
+
+ if (readonly) {
+ text.addStyleName("i-readonly");
+ } else {
+ text.removeStyleName("i-readonly");
+ }
+
+ }
+
+ public void onChange(Widget sender) {
+ if (sender == text) {
+ if (!text.getText().equals("")) {
+ try {
+ DateTimeFormat format = DateTimeFormat
+ .getFormat(getFormatString());
+ date = format.parse(text.getText());
+ long stamp = date.getTime();
+ if (stamp == 0) {
+ // If date parsing fails in firefox the stamp will be 0
+ date = null;
+ addStyleName(PARSE_ERROR_CLASSNAME);
+ } else {
+ // remove possibly added invalid value indication
+ removeStyleName(PARSE_ERROR_CLASSNAME);
+ }
+ } catch (final Exception e) {
+ ClientExceptionHandler.displayError(e.getMessage());
+
+ addStyleName(PARSE_ERROR_CLASSNAME);
+ // this is a hack that may eventually be removed
+ client.updateVariable(id, "lastInvalidDateString", text
+ .getText(), false);
+ date = null;
+ }
+ } else {
+ date = null;
+ // remove possibly added invalid value indication
+ removeStyleName(PARSE_ERROR_CLASSNAME);
+ }
+ // always send the date string
+ client.updateVariable(id, "dateString", text.getText(), false);
+
+ if (date != null) {
+ showingDate = new Date(date.getTime());
+ }
+
+ // Update variables
+ // (only the smallest defining resolution needs to be
+ // immediate)
+ client.updateVariable(id, "year",
+ date != null ? date.getYear() + 1900 : -1,
+ currentResolution == IDateField.RESOLUTION_YEAR
+ && immediate);
+ if (currentResolution >= IDateField.RESOLUTION_MONTH) {
+ client.updateVariable(id, "month", date != null ? date
+ .getMonth() + 1 : -1,
+ currentResolution == IDateField.RESOLUTION_MONTH
+ && immediate);
+ }
+ if (currentResolution >= IDateField.RESOLUTION_DAY) {
+ client.updateVariable(id, "day", date != null ? date.getDate()
+ : -1, currentResolution == IDateField.RESOLUTION_DAY
+ && immediate);
+ }
+ if (currentResolution >= IDateField.RESOLUTION_HOUR) {
+ client.updateVariable(id, "hour", date != null ? date
+ .getHours() : -1,
+ currentResolution == IDateField.RESOLUTION_HOUR
+ && immediate);
+ }
+ if (currentResolution >= IDateField.RESOLUTION_MIN) {
+ client.updateVariable(id, "min", date != null ? date
+ .getMinutes() : -1,
+ currentResolution == IDateField.RESOLUTION_MIN
+ && immediate);
+ }
+ if (currentResolution >= IDateField.RESOLUTION_SEC) {
+ client.updateVariable(id, "sec", date != null ? date
+ .getSeconds() : -1,
+ currentResolution == IDateField.RESOLUTION_SEC
+ && immediate);
+ }
+ if (currentResolution == IDateField.RESOLUTION_MSEC) {
+ client.updateVariable(id, "msec",
+ date != null ? getMilliseconds() : -1, immediate);
+ }
+
+ }
+ }
+
+ private String cleanFormat(String format) {
+ // Remove unnecessary d & M if resolution is too low
+ if (currentResolution < IDateField.RESOLUTION_DAY) {
+ format = format.replaceAll("d", "");
+ }
+ if (currentResolution < IDateField.RESOLUTION_MONTH) {
+ format = format.replaceAll("M", "");
+ }
+
+ // Remove unsupported patterns
+ // TODO support for 'G', era designator (used at least in Japan)
+ format = format.replaceAll("[GzZwWkK]", "");
+
+ // Remove extra delimiters ('/' and '.')
+ while (format.startsWith("/") || format.startsWith(".")
+ || format.startsWith("-")) {
+ format = format.substring(1);
+ }
+ while (format.endsWith("/") || format.endsWith(".")
+ || format.endsWith("-")) {
+ format = format.substring(0, format.length() - 1);
+ }
+
+ // Remove duplicate delimiters
+ format = format.replaceAll("//", "/");
+ format = format.replaceAll("\\.\\.", ".");
+ format = format.replaceAll("--", "-");
+
+ return format.trim();
+ }
+
+ @Override
+ public void setWidth(String newWidth) {
+ if (!"".equals(newWidth) && (width == null || !newWidth.equals(width))) {
+ if (BrowserInfo.get().isIE6()) {
+ // in IE6 cols ~ min-width
+ DOM.setElementProperty(text.getElement(), "size", "1");
+ }
+ needLayout = true;
+ width = newWidth;
+ super.setWidth(width);
+ iLayout();
+ if (newWidth.indexOf("%") < 0) {
+ needLayout = false;
+ }
+ } else {
+ if ("".equals(newWidth) && width != null && !"".equals(width)) {
+ if (BrowserInfo.get().isIE6()) {
+ // revert IE6 hack
+ DOM.setElementProperty(text.getElement(), "size", "");
+ }
+ super.setWidth("");
+ needLayout = true;
+ iLayout();
+ needLayout = false;
+ width = null;
+ }
+ }
+ }
+
+ /**
+ * Returns pixels in x-axis reserved for other than textfield content.
+ *
+ * @return extra width in pixels
+ */
+ protected int getFieldExtraWidth() {
+ if (fieldExtraWidth < 0) {
+ text.setWidth("0px");
+ fieldExtraWidth = text.getOffsetWidth();
+ }
+ return fieldExtraWidth;
+ }
+
+ public void iLayout() {
+ if (needLayout) {
+ text.setWidth((getOffsetWidth() - getFieldExtraWidth()) + "px");
+ }
+ }
+
+ public void focus() {
+ text.setFocus(true);
+ }
+}
diff --git a/src/com/vaadin/terminal/gwt/client/ui/ITime.java b/src/com/vaadin/terminal/gwt/client/ui/ITime.java
new file mode 100644
index 0000000000..43c1f9f261
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/ITime.java
@@ -0,0 +1,317 @@
+/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui;
+
+import java.util.Date;
+
+import com.google.gwt.user.client.ui.ChangeListener;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.ListBox;
+import com.google.gwt.user.client.ui.Widget;
+
+public class ITime extends FlowPanel implements ChangeListener {
+
+ private final IDateField datefield;
+
+ private ListBox hours;
+
+ private ListBox mins;
+
+ private ListBox sec;
+
+ private ListBox msec;
+
+ private ListBox ampm;
+
+ private int resolution = IDateField.RESOLUTION_HOUR;
+
+ private boolean readonly;
+
+ public ITime(IDateField parent) {
+ super();
+ datefield = parent;
+ setStyleName(IDateField.CLASSNAME + "-time");
+ }
+
+ private void buildTime(boolean redraw) {
+ final boolean thc = datefield.getDateTimeService().isTwelveHourClock();
+ if (redraw) {
+ clear();
+ final int numHours = thc ? 12 : 24;
+ hours = new ListBox();
+ hours.setStyleName(INativeSelect.CLASSNAME);
+ for (int i = 0; i < numHours; i++) {
+ hours.addItem((i < 10) ? "0" + i : "" + i);
+ }
+ hours.addChangeListener(this);
+ if (thc) {
+ ampm = new ListBox();
+ ampm.setStyleName(INativeSelect.CLASSNAME);
+ final String[] ampmText = datefield.getDateTimeService()
+ .getAmPmStrings();
+ ampm.addItem(ampmText[0]);
+ ampm.addItem(ampmText[1]);
+ ampm.addChangeListener(this);
+ }
+
+ if (datefield.getCurrentResolution() >= IDateField.RESOLUTION_MIN) {
+ mins = new ListBox();
+ mins.setStyleName(INativeSelect.CLASSNAME);
+ for (int i = 0; i < 60; i++) {
+ mins.addItem((i < 10) ? "0" + i : "" + i);
+ }
+ mins.addChangeListener(this);
+ }
+ if (datefield.getCurrentResolution() >= IDateField.RESOLUTION_SEC) {
+ sec = new ListBox();
+ sec.setStyleName(INativeSelect.CLASSNAME);
+ for (int i = 0; i < 60; i++) {
+ sec.addItem((i < 10) ? "0" + i : "" + i);
+ }
+ sec.addChangeListener(this);
+ }
+ if (datefield.getCurrentResolution() == IDateField.RESOLUTION_MSEC) {
+ msec = new ListBox();
+ msec.setStyleName(INativeSelect.CLASSNAME);
+ for (int i = 0; i < 1000; i++) {
+ if (i < 10) {
+ msec.addItem("00" + i);
+ } else if (i < 100) {
+ msec.addItem("0" + i);
+ } else {
+ msec.addItem("" + i);
+ }
+ }
+ msec.addChangeListener(this);
+ }
+
+ final String delimiter = datefield.getDateTimeService()
+ .getClockDelimeter();
+ final boolean ro = datefield.isReadonly();
+
+ if (ro) {
+ int h = 0;
+ if (datefield.getCurrentDate() != null) {
+ h = datefield.getCurrentDate().getHours();
+ }
+ if (thc) {
+ h -= h < 12 ? 0 : 12;
+ }
+ add(new ILabel(h < 10 ? "0" + h : "" + h));
+ } else {
+ add(hours);
+ }
+
+ if (datefield.getCurrentResolution() >= IDateField.RESOLUTION_MIN) {
+ add(new ILabel(delimiter));
+ if (ro) {
+ final int m = mins.getSelectedIndex();
+ add(new ILabel(m < 10 ? "0" + m : "" + m));
+ } else {
+ add(mins);
+ }
+ }
+ if (datefield.getCurrentResolution() >= IDateField.RESOLUTION_SEC) {
+ add(new ILabel(delimiter));
+ if (ro) {
+ final int s = sec.getSelectedIndex();
+ add(new ILabel(s < 10 ? "0" + s : "" + s));
+ } else {
+ add(sec);
+ }
+ }
+ if (datefield.getCurrentResolution() == IDateField.RESOLUTION_MSEC) {
+ add(new ILabel("."));
+ if (ro) {
+ final int m = datefield.getMilliseconds();
+ final String ms = m < 100 ? "0" + m : "" + m;
+ add(new ILabel(m < 10 ? "0" + ms : ms));
+ } else {
+ add(msec);
+ }
+ }
+ if (datefield.getCurrentResolution() == IDateField.RESOLUTION_HOUR) {
+ add(new ILabel(delimiter + "00")); // o'clock
+ }
+ if (thc) {
+ add(new ILabel("&nbsp;"));
+ if (ro) {
+ add(new ILabel(ampm.getItemText(datefield.getCurrentDate()
+ .getHours() < 12 ? 0 : 1)));
+ } else {
+ add(ampm);
+ }
+ }
+
+ if (ro) {
+ return;
+ }
+ }
+
+ // Update times
+ Date cdate = datefield.getCurrentDate();
+ boolean selected = true;
+ if (cdate == null) {
+ cdate = new Date();
+ selected = false;
+ }
+ if (thc) {
+ int h = cdate.getHours();
+ ampm.setSelectedIndex(h < 12 ? 0 : 1);
+ h -= ampm.getSelectedIndex() * 12;
+ hours.setSelectedIndex(h);
+ } else {
+ hours.setSelectedIndex(cdate.getHours());
+ }
+ if (datefield.getCurrentResolution() >= IDateField.RESOLUTION_MIN) {
+ mins.setSelectedIndex(cdate.getMinutes());
+ }
+ if (datefield.getCurrentResolution() >= IDateField.RESOLUTION_SEC) {
+ sec.setSelectedIndex(cdate.getSeconds());
+ }
+ if (datefield.getCurrentResolution() == IDateField.RESOLUTION_MSEC) {
+ if (selected) {
+ msec.setSelectedIndex(datefield.getMilliseconds());
+ } else {
+ msec.setSelectedIndex(0);
+ }
+ }
+ if (thc) {
+ ampm.setSelectedIndex(cdate.getHours() < 12 ? 0 : 1);
+ }
+
+ if (datefield.isReadonly() && !redraw) {
+ // Do complete redraw when in read-only status
+ clear();
+ final String delimiter = datefield.getDateTimeService()
+ .getClockDelimeter();
+
+ int h = cdate.getHours();
+ if (thc) {
+ h -= h < 12 ? 0 : 12;
+ }
+ add(new ILabel(h < 10 ? "0" + h : "" + h));
+
+ if (datefield.getCurrentResolution() >= IDateField.RESOLUTION_MIN) {
+ add(new ILabel(delimiter));
+ final int m = mins.getSelectedIndex();
+ add(new ILabel(m < 10 ? "0" + m : "" + m));
+ }
+ if (datefield.getCurrentResolution() >= IDateField.RESOLUTION_SEC) {
+ add(new ILabel(delimiter));
+ final int s = sec.getSelectedIndex();
+ add(new ILabel(s < 10 ? "0" + s : "" + s));
+ }
+ if (datefield.getCurrentResolution() == IDateField.RESOLUTION_MSEC) {
+ add(new ILabel("."));
+ final int m = datefield.getMilliseconds();
+ final String ms = m < 100 ? "0" + m : "" + m;
+ add(new ILabel(m < 10 ? "0" + ms : ms));
+ }
+ if (datefield.getCurrentResolution() == IDateField.RESOLUTION_HOUR) {
+ add(new ILabel(delimiter + "00")); // o'clock
+ }
+ if (thc) {
+ add(new ILabel("&nbsp;"));
+ add(new ILabel(ampm.getItemText(cdate.getHours() < 12 ? 0 : 1)));
+ }
+ }
+
+ final boolean enabled = datefield.isEnabled();
+ hours.setEnabled(enabled);
+ if (mins != null) {
+ mins.setEnabled(enabled);
+ }
+ if (sec != null) {
+ sec.setEnabled(enabled);
+ }
+ if (msec != null) {
+ msec.setEnabled(enabled);
+ }
+ if (ampm != null) {
+ ampm.setEnabled(enabled);
+ }
+
+ }
+
+ public void updateTime(boolean redraw) {
+ buildTime(redraw || resolution != datefield.getCurrentResolution()
+ || readonly != datefield.isReadonly());
+ if (datefield instanceof ITextualDate) {
+ ((ITextualDate) datefield).buildDate();
+ }
+ resolution = datefield.getCurrentResolution();
+ readonly = datefield.isReadonly();
+ }
+
+ public void onChange(Widget sender) {
+ if (datefield.getCurrentDate() == null) {
+ // was null on server, need to set
+ Date now = datefield.getShowingDate();
+ if (now == null) {
+ now = new Date();
+ datefield.setShowingDate(now);
+ }
+ datefield.setCurrentDate(new Date(now.getTime()));
+
+ // Init variables with current time
+ datefield.getClient().updateVariable(datefield.getId(), "year",
+ now.getYear() + 1900, false);
+ datefield.getClient().updateVariable(datefield.getId(), "month",
+ now.getMonth() + 1, false);
+ datefield.getClient().updateVariable(datefield.getId(), "day",
+ now.getDate(), false);
+ datefield.getClient().updateVariable(datefield.getId(), "hour",
+ now.getHours(), false);
+ datefield.getClient().updateVariable(datefield.getId(), "min",
+ now.getMinutes(), false);
+ datefield.getClient().updateVariable(datefield.getId(), "sec",
+ now.getSeconds(), false);
+ datefield.getClient().updateVariable(datefield.getId(), "msec",
+ datefield.getMilliseconds(), false);
+ }
+ if (sender == hours) {
+ int h = hours.getSelectedIndex();
+ if (datefield.getDateTimeService().isTwelveHourClock()) {
+ h = h + ampm.getSelectedIndex() * 12;
+ }
+ datefield.getCurrentDate().setHours(h);
+ datefield.getShowingDate().setHours(h);
+ datefield.getClient().updateVariable(datefield.getId(), "hour", h,
+ datefield.isImmediate());
+ updateTime(false);
+ } else if (sender == mins) {
+ final int m = mins.getSelectedIndex();
+ datefield.getCurrentDate().setMinutes(m);
+ datefield.getShowingDate().setMinutes(m);
+ datefield.getClient().updateVariable(datefield.getId(), "min", m,
+ datefield.isImmediate());
+ updateTime(false);
+ } else if (sender == sec) {
+ final int s = sec.getSelectedIndex();
+ datefield.getCurrentDate().setSeconds(s);
+ datefield.getShowingDate().setSeconds(s);
+ datefield.getClient().updateVariable(datefield.getId(), "sec", s,
+ datefield.isImmediate());
+ updateTime(false);
+ } else if (sender == msec) {
+ final int ms = msec.getSelectedIndex();
+ datefield.setMilliseconds(ms);
+ datefield.setShowingMilliseconds(ms);
+ datefield.getClient().updateVariable(datefield.getId(), "msec", ms,
+ datefield.isImmediate());
+ updateTime(false);
+ } else if (sender == ampm) {
+ final int h = hours.getSelectedIndex() + ampm.getSelectedIndex()
+ * 12;
+ datefield.getCurrentDate().setHours(h);
+ datefield.getShowingDate().setHours(h);
+ datefield.getClient().updateVariable(datefield.getId(), "hour", h,
+ datefield.isImmediate());
+ updateTime(false);
+ }
+ }
+
+}
diff --git a/src/com/vaadin/terminal/gwt/client/ui/IToolkitOverlay.java b/src/com/vaadin/terminal/gwt/client/ui/IToolkitOverlay.java
new file mode 100644
index 0000000000..8399544e4f
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/IToolkitOverlay.java
@@ -0,0 +1,313 @@
+/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui;
+
+import com.google.gwt.animation.client.Animation;
+import com.google.gwt.dom.client.Document;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.Element;
+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.vaadin.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 IToolkitOverlay extends PopupPanel {
+
+ /*
+ * The z-index value from where all overlays live. This can be overridden in
+ * any extending class.
+ */
+ protected static int Z_INDEX = 20000;
+
+ /*
+ * Shadow element style. If an extending class wishes to use a different
+ * style of shadow, it can use setShadowStyle(String) to give the shadow
+ * element a new style name.
+ */
+ public static final String CLASSNAME_SHADOW = "i-shadow";
+
+ /*
+ * The shadow element for this overlay.
+ */
+ private Element shadow;
+
+ /**
+ * The HTML snippet that is used to render the actual shadow. In consists of
+ * nine different DIV-elements with the following class names:
+ *
+ * <pre>
+ * .i-shadow[-stylename]
+ * ----------------------------------------------
+ * | .top-left | .top | .top-right |
+ * |---------------|-----------|----------------|
+ * | | | |
+ * | .left | .center | .right |
+ * | | | |
+ * |---------------|-----------|----------------|
+ * | .bottom-left | .bottom | .bottom-right |
+ * ----------------------------------------------
+ * </pre>
+ *
+ * See default theme 'shadow.css' for implementation example.
+ */
+ private static final String SHADOW_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>";
+
+ public IToolkitOverlay() {
+ super();
+ adjustZIndex();
+ }
+
+ public IToolkitOverlay(boolean autoHide) {
+ super(autoHide);
+ adjustZIndex();
+ }
+
+ public IToolkitOverlay(boolean autoHide, boolean modal) {
+ super(autoHide, modal);
+ adjustZIndex();
+ }
+
+ public IToolkitOverlay(boolean autoHide, boolean modal, boolean showShadow) {
+ super(autoHide, modal);
+ if (showShadow) {
+ shadow = DOM.createDiv();
+ shadow.setClassName(CLASSNAME_SHADOW);
+ shadow.setInnerHTML(SHADOW_HTML);
+ DOM.setStyleAttribute(shadow, "position", "absolute");
+
+ addPopupListener(new PopupListener() {
+ public void onPopupClosed(PopupPanel sender, boolean autoClosed) {
+ if (shadow.getParentElement() != null) {
+ shadow.getParentElement().removeChild(shadow);
+ }
+ }
+ });
+ }
+ adjustZIndex();
+ }
+
+ private void adjustZIndex() {
+ setZIndex(Z_INDEX);
+ }
+
+ /**
+ * Set the z-index (visual stack position) for this overlay.
+ *
+ * @param zIndex
+ * The new z-index
+ */
+ protected void setZIndex(int zIndex) {
+ DOM.setStyleAttribute(getElement(), "zIndex", "" + zIndex);
+ if (shadow != null) {
+ DOM.setStyleAttribute(shadow, "zIndex", "" + zIndex);
+ }
+ if (BrowserInfo.get().isIE6()) {
+ adjustIE6Frame(getElement(), zIndex - 1);
+ }
+ }
+
+ /**
+ * Get the z-index (visual stack position) of this overlay.
+ *
+ * @return The z-index for this overlay.
+ */
+ private int getZIndex() {
+ return Integer.parseInt(DOM.getStyleAttribute(getElement(), "zIndex"));
+ }
+
+ @Override
+ public void setPopupPosition(int left, int top) {
+ super.setPopupPosition(left, top);
+ if (shadow != null) {
+ updateShadowSizeAndPosition(isAnimationEnabled() ? 0 : 1);
+ }
+ }
+
+ @Override
+ public void show() {
+ super.show();
+ if (shadow != null) {
+ if (isAnimationEnabled()) {
+ ShadowAnimation sa = new ShadowAnimation();
+ sa.run(200);
+ } else {
+ updateShadowSizeAndPosition(1.0);
+ }
+ }
+ if (BrowserInfo.get().isIE6()) {
+ adjustIE6Frame(getElement(), getZIndex());
+ }
+ }
+
+ @Override
+ public void setVisible(boolean visible) {
+ super.setVisible(visible);
+ if (shadow != null) {
+ shadow.getStyle().setProperty("visibility",
+ visible ? "visible" : "hidden");
+ }
+ }
+
+ /*
+ * Needed to position overlays on top of native SELECT elements in IE6. See
+ * bug #2004
+ */
+ private native void adjustIE6Frame(Element popup, int zindex)
+ /*-{
+ // relies on PopupImplIE6
+ if(popup.__frame)
+ popup.__frame.style.zIndex = zindex;
+ }-*/;
+
+ @Override
+ public void setWidth(String width) {
+ super.setWidth(width);
+ if (shadow != null) {
+ updateShadowSizeAndPosition(1.0);
+ }
+ }
+
+ @Override
+ public void setHeight(String height) {
+ super.setHeight(height);
+ if (shadow != null) {
+ updateShadowSizeAndPosition(1.0);
+ }
+ }
+
+ /**
+ * Sets the shadow style for this overlay. Will override any previous style
+ * for the shadow. The default style name is defined by CLASSNAME_SHADOW.
+ * The given style will be prefixed with CLASSNAME_SHADOW.
+ *
+ * @param style
+ * The new style name for the shadow element. Will be prefixed by
+ * CLASSNAME_SHADOW, e.g. style=='foobar' -> actual style
+ * name=='i-shadow-foobar'.
+ */
+ protected void setShadowStyle(String style) {
+ if (shadow != null) {
+ shadow.setClassName(CLASSNAME_SHADOW + "-" + style);
+ }
+ }
+
+ /*
+ * Extending classes should always call this method after they change the
+ * size of overlay without using normal 'setWidth(String)' and
+ * 'setHeight(String)' methods (if not calling super.setWidth/Height).
+ */
+ protected void updateShadowSizeAndPosition() {
+ updateShadowSizeAndPosition(1.0);
+ }
+
+ /**
+ * Recalculates proper position and dimensions for the shadow element. Can
+ * be used to animate the shadow, using the 'progress' parameter (used to
+ * animate the shadow in sync with GWT PopupPanel's default animation
+ * 'PopupPanel.AnimationType.CENTER').
+ *
+ * @param progress
+ * A value between 0.0 and 1.0, indicating the progress of the
+ * animation (0=start, 1=end).
+ */
+ private void updateShadowSizeAndPosition(final double progress) {
+ // Don't do anything if overlay element is not attached
+ if (!isAttached()) {
+ return;
+ }
+ // Calculate proper z-index
+ String zIndex = null;
+ try {
+ // Odd behaviour with Windows Hosted Mode forces us to use
+ // this redundant try/catch block (See dev.itmill.com #2011)
+ zIndex = DOM.getStyleAttribute(getElement(), "zIndex");
+ } catch (Exception ignore) {
+ // Ignored, will cause no harm
+ }
+ if (zIndex == null) {
+ zIndex = "" + Z_INDEX;
+ }
+ // Calculate position and size
+ if (BrowserInfo.get().isIE()) {
+ // Shake IE
+ getOffsetHeight();
+ getOffsetWidth();
+ }
+
+ int x = getAbsoluteLeft();
+ int y = getAbsoluteTop();
+
+ /* This is needed for IE7 at least */
+ // Account for the difference between absolute position and the
+ // body's positioning context.
+ x -= Document.get().getBodyOffsetLeft();
+ y -= Document.get().getBodyOffsetTop();
+
+ int width = getOffsetWidth();
+ int height = getOffsetHeight();
+
+ if (width < 0) {
+ width = 0;
+ }
+ if (height < 0) {
+ height = 0;
+ }
+
+ // Animate the shadow size
+ x += (int) (width * (1.0 - progress) / 2.0);
+ y += (int) (height * (1.0 - progress) / 2.0);
+ width = (int) (width * progress);
+ height = (int) (height * progress);
+
+ // Opera needs some shaking to get parts of the shadow showing
+ // properly
+ // (ticket #2704)
+ if (BrowserInfo.get().isOpera()) {
+ // Clear the height of all middle elements
+ DOM.getChild(shadow, 3).getStyle().setProperty("height", "auto");
+ DOM.getChild(shadow, 4).getStyle().setProperty("height", "auto");
+ DOM.getChild(shadow, 5).getStyle().setProperty("height", "auto");
+ }
+
+ // Update correct values
+ DOM.setStyleAttribute(shadow, "zIndex", zIndex);
+ DOM.setStyleAttribute(shadow, "width", width + "px");
+ DOM.setStyleAttribute(shadow, "height", height + "px");
+ DOM.setStyleAttribute(shadow, "top", y + "px");
+ DOM.setStyleAttribute(shadow, "left", x + "px");
+ DOM.setStyleAttribute(shadow, "display", progress < 0.9 ? "none" : "");
+
+ // Opera fix, part 2 (ticket #2704)
+ if (BrowserInfo.get().isOpera()) {
+ // We'll fix the height of all the middle elements
+ DOM.getChild(shadow, 3).getStyle().setPropertyPx("height",
+ DOM.getChild(shadow, 3).getOffsetHeight());
+ DOM.getChild(shadow, 4).getStyle().setPropertyPx("height",
+ DOM.getChild(shadow, 4).getOffsetHeight());
+ DOM.getChild(shadow, 5).getStyle().setPropertyPx("height",
+ DOM.getChild(shadow, 5).getOffsetHeight());
+ }
+
+ // Attach to dom if not there already
+ if (shadow.getParentElement() == null) {
+ RootPanel.get().getElement().insertBefore(shadow, getElement());
+ }
+
+ }
+
+ protected class ShadowAnimation extends Animation {
+ @Override
+ protected void onUpdate(double progress) {
+ if (shadow != null) {
+ updateShadowSizeAndPosition(progress);
+ }
+ }
+ }
+}
diff --git a/src/com/vaadin/terminal/gwt/client/ui/ITree.java b/src/com/vaadin/terminal/gwt/client/ui/ITree.java
new file mode 100644
index 0000000000..b322293964
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/ITree.java
@@ -0,0 +1,469 @@
+/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.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.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.BrowserInfo;
+import com.vaadin.terminal.gwt.client.MouseEventDetails;
+import com.vaadin.terminal.gwt.client.Paintable;
+import com.vaadin.terminal.gwt.client.UIDL;
+import com.vaadin.terminal.gwt.client.Util;
+
+/**
+ *
+ */
+public class ITree extends FlowPanel implements Paintable {
+
+ public static final String CLASSNAME = "i-tree";
+
+ private Set<String> selectedIds = new HashSet<String>();
+ private ApplicationConnection client;
+ private String paintableId;
+ private boolean selectable;
+ private boolean isMultiselect;
+
+ private final HashMap<String, TreeNode> keyToNode = new HashMap<String, TreeNode>();
+
+ /**
+ * This map contains captions and icon urls for actions like: * "33_c" ->
+ * "Edit" * "33_i" -> "http://dom.com/edit.png"
+ */
+ private final HashMap<String, String> actionMap = new HashMap<String, String>();
+
+ private boolean immediate;
+
+ private boolean isNullSelectionAllowed = true;
+
+ private boolean disabled = false;
+
+ private boolean readonly;
+
+ private boolean emitClickEvents;
+
+ private boolean rendering;
+
+ 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 actionMap.get(actionKey + "_c");
+ }
+
+ public String getActionIcon(String actionKey) {
+ return 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;
+ }
+
+ rendering = true;
+
+ this.client = client;
+
+ if (uidl.hasAttribute("partialUpdate")) {
+ handleUpdate(uidl);
+ rendering = false;
+ return;
+ }
+
+ paintableId = uidl.getId();
+
+ immediate = uidl.hasAttribute("immediate");
+
+ disabled = uidl.getBooleanAttribute("disabled");
+ readonly = uidl.getBooleanAttribute("readonly");
+ emitClickEvents = uidl.getBooleanAttribute("listenClicks");
+
+ 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");
+
+ rendering = false;
+
+ }
+
+ private void handleUpdate(UIDL uidl) {
+ final TreeNode rootNode = keyToNode.get(uidl
+ .getStringAttribute("rootKey"));
+ if (rootNode != null) {
+ if (!rootNode.getState()) {
+ // expanding node happened server side
+ rootNode.setState(true, false);
+ }
+ rootNode.renderChildNodes(uidl.getChildIterator());
+ }
+
+ if (uidl.hasVariable("selected")) {
+ // update selection in case selected nodes were not visible
+ selectedIds = uidl.getStringArrayVariableAsSet("selected");
+ }
+
+ }
+
+ public void setSelected(TreeNode treeNode, boolean selected) {
+ if (selected) {
+ if (!isMultiselect) {
+ while (selectedIds.size() > 0) {
+ final String id = selectedIds.iterator().next();
+ final TreeNode oldSelection = keyToNode.get(id);
+ if (oldSelection != null) {
+ // can be null if the node is not visible (parent
+ // collapsed)
+ 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;
+
+ private Element ie6compatnode;
+
+ public TreeNode() {
+ constructDom();
+ sinkEvents(Event.ONCLICK | Event.ONDBLCLICK | Event.ONMOUSEUP
+ | Event.ONCONTEXTMENU);
+ }
+
+ @Override
+ public void onBrowserEvent(Event event) {
+ super.onBrowserEvent(event);
+ if (disabled) {
+ return;
+ }
+ final int type = DOM.eventGetType(event);
+ final Element target = DOM.eventGetTarget(event);
+ if (emitClickEvents && target == nodeCaptionSpan
+ && (type == Event.ONDBLCLICK || type == Event.ONMOUSEUP)) {
+ fireClick(event);
+ }
+ if (type == Event.ONCLICK) {
+ if (getElement() == target || ie6compatnode == target) {
+ // state change
+ toggleState();
+ } else if (!readonly && target == nodeCaptionSpan) {
+ // caption click = selection change && possible click event
+ toggleSelection();
+ }
+ DOM.eventCancelBubble(event, true);
+ } else if (type == Event.ONCONTEXTMENU) {
+ showContextMenu(event);
+ }
+ }
+
+ private void fireClick(Event evt) {
+ // non-immediate iff an immediate select event is going to happen
+ boolean imm = !immediate
+ || !selectable
+ || (!isNullSelectionAllowed && isSelected() && selectedIds
+ .size() == 1);
+ MouseEventDetails details = new MouseEventDetails(evt);
+ client.updateVariable(paintableId, "clickedKey", key, false);
+ client.updateVariable(paintableId, "clickEvent",
+ details.toString(), imm);
+ }
+
+ private void toggleSelection() {
+ if (selectable) {
+ ITree.this.setSelected(this, !isSelected());
+ }
+ }
+
+ private void toggleState() {
+ setState(!getState(), true);
+ }
+
+ protected void constructDom() {
+ // workaround for a very weird IE6 issue #1245
+ ie6compatnode = DOM.createDiv();
+ setStyleName(ie6compatnode, CLASSNAME + "-ie6compatnode");
+ DOM.setInnerText(ie6compatnode, " ");
+ DOM.appendChild(getElement(), ie6compatnode);
+
+ DOM.sinkEvents(ie6compatnode, Event.ONCLICK);
+
+ nodeCaptionDiv = DOM.createDiv();
+ DOM.setElementProperty(nodeCaptionDiv, "className", CLASSNAME
+ + "-caption");
+ Element wrapper = DOM.createDiv();
+ nodeCaptionSpan = DOM.createSpan();
+ DOM.appendChild(getElement(), nodeCaptionDiv);
+ DOM.appendChild(nodeCaptionDiv, wrapper);
+ DOM.appendChild(wrapper, nodeCaptionSpan);
+
+ childNodeContainer = new FlowPanel();
+ childNodeContainer.setStylePrimaryName(CLASSNAME + "-children");
+ setWidget(childNodeContainer);
+ }
+
+ 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(DOM.getFirstChild(nodeCaptionDiv), icon
+ .getElement(), nodeCaptionSpan);
+ }
+ icon.setUri(uidl.getStringAttribute("icon"));
+ } else {
+ if (icon != null) {
+ DOM.removeChild(DOM.getFirstChild(nodeCaptionDiv), icon
+ .getElement());
+ icon = null;
+ }
+ }
+
+ if (BrowserInfo.get().isIE6() && isAttached()) {
+ fixWidth();
+ }
+ }
+
+ 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;
+
+ if (!rendering) {
+ Util.notifyParentOfSizeChange(ITree.this, false);
+ }
+ }
+
+ 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 specific style name. This method ought
+ * to be called only from ITree.
+ *
+ * @param selected
+ */
+ protected void setSelected(boolean selected) {
+ // add style name to caption dom structure only, not to subtree
+ setStyleName(nodeCaptionDiv, "i-tree-node-selected", selected);
+ }
+
+ protected boolean isSelected() {
+ return ITree.this.isSelected(this);
+ }
+
+ public void showContextMenu(Event event) {
+ if (!readonly && !disabled) {
+ if (actionKeys != null) {
+ int left = event.getClientX();
+ int top = event.getClientY();
+ top += Window.getScrollTop();
+ left += Window.getScrollLeft();
+ client.getContextMenu().showAt(this, left, top);
+ }
+ event.cancelBubble(true);
+ event.preventDefault();
+ }
+ }
+
+ /*
+ * We need to fix the width of TreeNodes so that the float in
+ * ie6compatNode does not wrap (see ticket #1245)
+ */
+ private void fixWidth() {
+ nodeCaptionDiv.getStyle().setProperty("styleFloat", "left");
+ nodeCaptionDiv.getStyle().setProperty("display", "inline");
+ nodeCaptionDiv.getStyle().setProperty("marginLeft", "0");
+ final int captionWidth = ie6compatnode.getOffsetWidth()
+ + nodeCaptionDiv.getOffsetWidth();
+ setWidth(captionWidth + "px");
+ }
+
+ @Override
+ public void onAttach() {
+ super.onAttach();
+ if (BrowserInfo.get().isIE6()) {
+ fixWidth();
+ }
+ }
+ }
+}
diff --git a/src/com/vaadin/terminal/gwt/client/ui/ITwinColSelect.java b/src/com/vaadin/terminal/gwt/client/ui/ITwinColSelect.java
new file mode 100644
index 0000000000..c29f520f8d
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/ITwinColSelect.java
@@ -0,0 +1,244 @@
+/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.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.ui.FlowPanel;
+import com.google.gwt.user.client.ui.HTML;
+import com.google.gwt.user.client.ui.ListBox;
+import com.google.gwt.user.client.ui.Panel;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.terminal.gwt.client.UIDL;
+
+public class ITwinColSelect extends IOptionGroupBase {
+
+ private static final String CLASSNAME = "i-select-twincol";
+
+ private static final int VISIBLE_COUNT = 10;
+
+ private static final int DEFAULT_COLUMN_COUNT = 10;
+
+ private final ListBox options;
+
+ private final ListBox selections;
+
+ private final IButton add;
+
+ private final IButton remove;
+
+ private FlowPanel buttons;
+
+ private Panel panel;
+
+ private boolean widthSet = false;
+
+ public ITwinColSelect() {
+ super(CLASSNAME);
+ options = new ListBox();
+ options.addClickListener(this);
+ selections = new ListBox();
+ selections.addClickListener(this);
+ options.setVisibleItemCount(VISIBLE_COUNT);
+ selections.setVisibleItemCount(VISIBLE_COUNT);
+ options.setStyleName(CLASSNAME + "-options");
+ selections.setStyleName(CLASSNAME + "-selections");
+ buttons = new FlowPanel();
+ buttons.setStyleName(CLASSNAME + "-buttons");
+ add = new IButton();
+ add.setText(">>");
+ add.addClickListener(this);
+ remove = new IButton();
+ remove.setText("<<");
+ remove.addClickListener(this);
+ panel = ((Panel) optionsContainer);
+ panel.add(options);
+ buttons.add(add);
+ final HTML br = new HTML("<span/>");
+ br.setStyleName(CLASSNAME + "-deco");
+ buttons.add(br);
+ buttons.add(remove);
+ panel.add(buttons);
+ panel.add(selections);
+ }
+
+ @Override
+ protected void buildOptions(UIDL uidl) {
+ final boolean enabled = !isDisabled() && !isReadonly();
+ options.setMultipleSelect(isMultiselect());
+ selections.setMultipleSelect(isMultiselect());
+ options.setEnabled(enabled);
+ selections.setEnabled(enabled);
+ add.setEnabled(enabled);
+ remove.setEnabled(enabled);
+ options.clear();
+ selections.clear();
+ for (final Iterator i = uidl.getChildIterator(); i.hasNext();) {
+ final UIDL optionUidl = (UIDL) i.next();
+ if (optionUidl.hasAttribute("selected")) {
+ selections.addItem(optionUidl.getStringAttribute("caption"),
+ optionUidl.getStringAttribute("key"));
+ } else {
+ options.addItem(optionUidl.getStringAttribute("caption"),
+ optionUidl.getStringAttribute("key"));
+ }
+ }
+
+ int cols = -1;
+ if (getColumns() > 0) {
+ cols = getColumns();
+ } else if (!widthSet) {
+ cols = DEFAULT_COLUMN_COUNT;
+ }
+
+ if (cols >= 0) {
+ options.setWidth(cols + "em");
+ selections.setWidth(cols + "em");
+ buttons.setWidth("3.5em");
+ optionsContainer.setWidth((2 * cols + 4) + "em");
+ }
+ if (getRows() > 0) {
+ options.setVisibleItemCount(getRows());
+ selections.setVisibleItemCount(getRows());
+
+ }
+
+ }
+
+ @Override
+ protected Object[] getSelectedItems() {
+ final Vector selectedItemKeys = new Vector();
+ for (int i = 0; i < selections.getItemCount(); i++) {
+ selectedItemKeys.add(selections.getValue(i));
+ }
+ return selectedItemKeys.toArray();
+ }
+
+ private boolean[] getItemsToAdd() {
+ final boolean[] selectedIndexes = new boolean[options.getItemCount()];
+ for (int i = 0; i < options.getItemCount(); i++) {
+ if (options.isItemSelected(i)) {
+ selectedIndexes[i] = true;
+ } else {
+ selectedIndexes[i] = false;
+ }
+ }
+ return selectedIndexes;
+ }
+
+ private boolean[] getItemsToRemove() {
+ final boolean[] selectedIndexes = new boolean[selections.getItemCount()];
+ for (int i = 0; i < selections.getItemCount(); i++) {
+ if (selections.isItemSelected(i)) {
+ selectedIndexes[i] = true;
+ } else {
+ selectedIndexes[i] = false;
+ }
+ }
+ return selectedIndexes;
+ }
+
+ @Override
+ public void onClick(Widget sender) {
+ super.onClick(sender);
+ if (sender == add) {
+ final boolean[] sel = getItemsToAdd();
+ for (int i = 0; i < sel.length; i++) {
+ if (sel[i]) {
+ final int optionIndex = i
+ - (sel.length - options.getItemCount());
+ selectedKeys.add(options.getValue(optionIndex));
+
+ // Move selection to another column
+ final String text = options.getItemText(optionIndex);
+ final String value = options.getValue(optionIndex);
+ selections.addItem(text, value);
+ selections.setItemSelected(selections.getItemCount() - 1,
+ true);
+ options.removeItem(optionIndex);
+ }
+ }
+ client.updateVariable(id, "selected", selectedKeys.toArray(),
+ isImmediate());
+
+ } else if (sender == remove) {
+ final boolean[] sel = getItemsToRemove();
+ for (int i = 0; i < sel.length; i++) {
+ if (sel[i]) {
+ final int selectionIndex = i
+ - (sel.length - selections.getItemCount());
+ selectedKeys.remove(selections.getValue(selectionIndex));
+
+ // Move selection to another column
+ final String text = selections.getItemText(selectionIndex);
+ final String value = selections.getValue(selectionIndex);
+ options.addItem(text, value);
+ options.setItemSelected(options.getItemCount() - 1, true);
+ selections.removeItem(selectionIndex);
+ }
+ }
+ client.updateVariable(id, "selected", selectedKeys.toArray(),
+ isImmediate());
+ } else if (sender == options) {
+ // unselect all in other list, to avoid mistakes (i.e wrong button)
+ final int c = selections.getItemCount();
+ for (int i = 0; i < c; i++) {
+ selections.setItemSelected(i, false);
+ }
+ } else if (sender == selections) {
+ // unselect all in other list, to avoid mistakes (i.e wrong button)
+ final int c = options.getItemCount();
+ for (int i = 0; i < c; i++) {
+ options.setItemSelected(i, false);
+ }
+ }
+ }
+
+ @Override
+ public void setHeight(String height) {
+ super.setHeight(height);
+ if ("".equals(height)) {
+ options.setHeight("");
+ selections.setHeight("");
+ } else {
+ setFullHeightInternals();
+ }
+ }
+
+ private void setFullHeightInternals() {
+ options.setHeight("100%");
+ selections.setHeight("100%");
+ }
+
+ @Override
+ public void setWidth(String width) {
+ super.setWidth(width);
+ if (!"".equals(width) && width != null) {
+ setRelativeInternalWidths();
+ }
+ }
+
+ private void setRelativeInternalWidths() {
+ DOM.setStyleAttribute(getElement(), "position", "relative");
+ buttons.setWidth("15%");
+ options.setWidth("42%");
+ selections.setWidth("42%");
+ widthSet = true;
+ }
+
+ @Override
+ protected void setTabIndex(int tabIndex) {
+ options.setTabIndex(tabIndex);
+ selections.setTabIndex(tabIndex);
+ add.setTabIndex(tabIndex);
+ remove.setTabIndex(tabIndex);
+ }
+
+ public void focus() {
+ options.setFocus(true);
+ }
+}
diff --git a/src/com/vaadin/terminal/gwt/client/ui/IUnknownComponent.java b/src/com/vaadin/terminal/gwt/client/ui/IUnknownComponent.java
new file mode 100644
index 0000000000..5715ab02c8
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/IUnknownComponent.java
@@ -0,0 +1,40 @@
+/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.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.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.Paintable;
+import com.vaadin.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/vaadin/terminal/gwt/client/ui/IUpload.java b/src/com/vaadin/terminal/gwt/client/ui/IUpload.java
new file mode 100644
index 0000000000..a03beed3e2
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/IUpload.java
@@ -0,0 +1,152 @@
+/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.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.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.Paintable;
+import com.vaadin.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() {
+ @Override
+ 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/vaadin/terminal/gwt/client/ui/IUriFragmentUtility.java b/src/com/vaadin/terminal/gwt/client/ui/IUriFragmentUtility.java
new file mode 100644
index 0000000000..d2237c5f60
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/IUriFragmentUtility.java
@@ -0,0 +1,67 @@
+package com.vaadin.terminal.gwt.client.ui;
+
+import com.google.gwt.dom.client.Document;
+import com.google.gwt.user.client.History;
+import com.google.gwt.user.client.HistoryListener;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.BrowserInfo;
+import com.vaadin.terminal.gwt.client.Paintable;
+import com.vaadin.terminal.gwt.client.UIDL;
+
+/**
+ * Client side implementation for UriFragmentUtility. Uses GWT's History object
+ * as an implementation.
+ *
+ */
+public class IUriFragmentUtility extends Widget implements Paintable,
+ HistoryListener {
+
+ private String fragment;
+ private ApplicationConnection client;
+ private String paintableId;
+ private boolean immediate;
+
+ public IUriFragmentUtility() {
+ setElement(Document.get().createDivElement());
+ if (BrowserInfo.get().isIE6()) {
+ getElement().getStyle().setProperty("overflow", "hidden");
+ getElement().getStyle().setProperty("height", "0");
+ }
+ History.addHistoryListener(this);
+ History.fireCurrentHistoryState();
+ }
+
+ public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+ if (client.updateComponent(this, uidl, false)) {
+ return;
+ }
+ String uidlFragment = uidl.getStringVariable("fragment");
+ immediate = uidl.getBooleanAttribute("immediate");
+ if (this.client == null) {
+ // initial paint has some special logic
+ this.client = client;
+ paintableId = uidl.getId();
+ if (!fragment.equals(uidlFragment)) {
+ // initial server side fragment (from link/bookmark/typed) does
+ // not equal the one on
+ // server, send initial fragment to server
+ History.fireCurrentHistoryState();
+ }
+ } else {
+ if (uidlFragment != null && !uidlFragment.equals(fragment)) {
+ fragment = uidlFragment;
+ // normal fragment change from server, add new history item
+ History.newItem(uidlFragment, false);
+ }
+ }
+ }
+
+ public void onHistoryChanged(String historyToken) {
+ fragment = historyToken;
+ if (client != null) {
+ client.updateVariable(paintableId, "fragment", fragment, immediate);
+ }
+ }
+
+}
diff --git a/src/com/vaadin/terminal/gwt/client/ui/IVerticalLayout.java b/src/com/vaadin/terminal/gwt/client/ui/IVerticalLayout.java
new file mode 100644
index 0000000000..9adeb8158e
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/IVerticalLayout.java
@@ -0,0 +1,11 @@
+package com.vaadin.terminal.gwt.client.ui;
+
+public class IVerticalLayout extends IOrderedLayout {
+
+ public static final String CLASSNAME = "i-verticallayout";
+
+ public IVerticalLayout() {
+ super(CLASSNAME, ORIENTATION_VERTICAL);
+ }
+
+}
diff --git a/src/com/vaadin/terminal/gwt/client/ui/IView.java b/src/com/vaadin/terminal/gwt/client/ui/IView.java
new file mode 100644
index 0000000000..2d8ce9a986
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/IView.java
@@ -0,0 +1,575 @@
+/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.Set;
+
+import com.google.gwt.dom.client.DivElement;
+import com.google.gwt.dom.client.Document;
+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.WindowResizeListener;
+import com.google.gwt.user.client.ui.HasFocus;
+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.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.BrowserInfo;
+import com.vaadin.terminal.gwt.client.Container;
+import com.vaadin.terminal.gwt.client.Focusable;
+import com.vaadin.terminal.gwt.client.Paintable;
+import com.vaadin.terminal.gwt.client.RenderSpace;
+import com.vaadin.terminal.gwt.client.UIDL;
+import com.vaadin.terminal.gwt.client.Util;
+
+/**
+ *
+ */
+public class IView extends SimplePanel implements Container,
+ WindowResizeListener, WindowCloseListener {
+
+ private static final String CLASSNAME = "i-view";
+
+ private String theme;
+
+ private Paintable layout;
+
+ private final LinkedHashSet<IWindow> subWindows = new LinkedHashSet<IWindow>();
+
+ private String id;
+
+ private ShortcutActionHandler actionHandler;
+
+ /** stored width for IE resize optimization */
+ private int width;
+
+ /** stored height for IE resize optimization */
+ private int height;
+
+ private ApplicationConnection connection;
+
+ /**
+ * 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;
+
+ private int scrollTop;
+
+ private int scrollLeft;
+
+ private boolean rendering;
+
+ private boolean scrollable;
+
+ private boolean immediate;
+
+ public IView(String elementId) {
+ super();
+ setStyleName(CLASSNAME);
+
+ DOM.sinkEvents(getElement(), Event.ONKEYDOWN | Event.ONSCROLL);
+
+ // iview is focused when created so element needs tabIndex
+ // 1 due 0 is at the end of natural tabbing order
+ DOM.setElementProperty(getElement(), "tabIndex", "1");
+
+ RootPanel root = RootPanel.get(elementId);
+ root.add(this);
+ root.removeStyleName("i-app-loading");
+
+ BrowserInfo browser = BrowserInfo.get();
+
+ // set focus to iview element by default to listen possible keyboard
+ // shortcuts
+ if (browser.isOpera() || browser.isSafari()
+ && browser.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) {
+ rendering = true;
+
+ id = uidl.getId();
+ boolean firstPaint = connection == null;
+ connection = client;
+
+ immediate = uidl.hasAttribute("immediate");
+
+ 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"));
+ }
+
+ if (uidl.hasAttribute("name")) {
+ client.setWindowName(uidl.getStringAttribute("name"));
+ }
+
+ com.google.gwt.user.client.Window.setTitle(uidl
+ .getStringAttribute("caption"));
+
+ // Process children
+ int childIndex = 0;
+
+ // Open URL:s
+ boolean isClosed = false; // was this window closed?
+ 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) {
+ // This window is closing. Send close event before
+ // going to the new url
+ isClosed = true;
+ onWindowClosed();
+ goTo(url);
+ } else {
+ String options;
+ if (open.hasAttribute("border")) {
+ if (open.getStringAttribute("border").equals("minimal")) {
+ options = "menubar=yes,location=no,status=no";
+ } else {
+ options = "menubar=no,location=no,status=no";
+ }
+
+ } else {
+ options = "resizable=yes,menubar=yes,toolbar=yes,directories=yes,location=yes,scrollbars=yes,status=yes";
+ }
+
+ if (open.hasAttribute("width")) {
+ int w = open.getIntAttribute("width");
+ options += ",width=" + w;
+ }
+ if (open.hasAttribute("height")) {
+ int h = open.getIntAttribute("height");
+ options += ",height=" + h;
+ }
+
+ Window.open(url, target, options);
+ }
+ childIndex++;
+ }
+ if (isClosed) {
+ // don't render the content
+ rendering = false;
+ return;
+ }
+
+ // 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<IWindow> removedSubWindows = new HashSet<IWindow>(
+ 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((IWindow) 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 INotification(delay).show(html, position, style);
+ }
+ }
+ }
+
+ // Close old windows
+ for (final Iterator<IWindow> rem = removedSubWindows.iterator(); rem
+ .hasNext();) {
+ final IWindow w = rem.next();
+ client.unregisterPaintable(w);
+ subWindows.remove(w);
+ w.hide();
+ }
+
+ if (uidl.hasAttribute("focused")) {
+ final String focusPid = uidl.getStringAttribute("focused");
+ // set focused component when render phase is finished
+ DeferredCommand.addCommand(new Command() {
+ public void execute() {
+ final Paintable toBeFocused = connection
+ .getPaintable(focusPid);
+
+ /*
+ * Two types of Widgets can be focused, either implementing
+ * GWT HasFocus of a thinner Toolkit specific Focusable
+ * interface.
+ */
+ if (toBeFocused instanceof HasFocus) {
+ final HasFocus toBeFocusedWidget = (HasFocus) toBeFocused;
+ toBeFocusedWidget.setFocus(true);
+ } else if (toBeFocused instanceof Focusable) {
+ ((Focusable) toBeFocused).focus();
+ } else {
+ ApplicationConnection.getConsole().log(
+ "Could not focus component");
+ }
+ }
+ });
+ }
+
+ // Add window listeners on first paint, to prevent premature
+ // variablechanges
+ if (firstPaint) {
+ Window.addWindowCloseListener(this);
+ Window.addWindowResizeListener(this);
+ }
+
+ onWindowResized(Window.getClientWidth(), Window.getClientHeight());
+
+ if (BrowserInfo.get().isSafari()) {
+ Util.runWebkitOverflowAutoFix(getElement());
+ }
+
+ // finally set scroll position from UIDL
+ if (uidl.hasVariable("scrollTop")) {
+ scrollable = true;
+ scrollTop = uidl.getIntVariable("scrollTop");
+ DOM.setElementPropertyInt(getElement(), "scrollTop", scrollTop);
+ scrollLeft = uidl.getIntVariable("scrollLeft");
+ DOM.setElementPropertyInt(getElement(), "scrollLeft", scrollLeft);
+ } else {
+ scrollable = false;
+ }
+
+ rendering = false;
+ }
+
+ @Override
+ public void onBrowserEvent(Event event) {
+ super.onBrowserEvent(event);
+ int type = DOM.eventGetType(event);
+ if (type == Event.ONKEYDOWN && actionHandler != null) {
+ actionHandler.handleKeyboardEvent(event);
+ return;
+ } else if (scrollable && type == Event.ONSCROLL) {
+ updateScrollPosition();
+ }
+ }
+
+ /**
+ * Updates scroll position from DOM and saves variables to server.
+ */
+ private void updateScrollPosition() {
+ int oldTop = scrollTop;
+ int oldLeft = scrollLeft;
+ scrollTop = DOM.getElementPropertyInt(getElement(), "scrollTop");
+ scrollLeft = DOM.getElementPropertyInt(getElement(), "scrollLeft");
+ if (connection != null && !rendering) {
+ if (oldTop != scrollTop) {
+ connection.updateVariable(id, "scrollTop", scrollTop, false);
+ }
+ if (oldLeft != scrollLeft) {
+ connection.updateVariable(id, "scrollLeft", scrollLeft, false);
+ }
+ }
+ }
+
+ public void onWindowResized(int width, int height) {
+ if (BrowserInfo.get().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() {
+ @Override
+ 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");
+ connection.runDescendentsLayout(IView.this);
+
+ sendClientResized();
+ }
+ }
+ };
+ } else {
+ resizeTimer.cancel();
+ }
+ resizeTimer.schedule(200);
+ } else {
+ if (width == IView.this.width && height == IView.this.height) {
+ // No point in doing resize operations if window size has not
+ // changed
+ return;
+ }
+
+ IView.this.width = Window.getClientWidth();
+ IView.this.height = Window.getClientHeight();
+
+ ApplicationConnection.getConsole().log(
+ "Running layout functions due window resize");
+
+ connection.runDescendentsLayout(this);
+ Util.runWebkitOverflowAutoFix(getElement());
+
+ sendClientResized();
+ }
+
+ }
+
+ /**
+ * Send new dimensions to the server.
+ */
+ private void sendClientResized() {
+ connection.updateVariable(id, "height", height, false);
+ connection.updateVariable(id, "width", width, immediate);
+ }
+
+ public native static void goTo(String url)
+ /*-{
+ $wnd.location = url;
+ }-*/;
+
+ public void onWindowClosed() {
+ // Change focus on this window in order to ensure that all state is
+ // collected from textfields
+ ITextField.flushChangesFromFocusedTextField();
+
+ // Send the closing state to server
+ connection.updateVariable(id, "close", true, false);
+ connection.sendPendingVariableChangesSync();
+ }
+
+ public String onWindowClosing() {
+ return null;
+ }
+
+ private final RenderSpace myRenderSpace = new RenderSpace() {
+ private int excessHeight = -1;
+ private int excessWidth = -1;
+
+ @Override
+ public int getHeight() {
+ return getElement().getOffsetHeight() - getExcessHeight();
+ }
+
+ private int getExcessHeight() {
+ if (excessHeight < 0) {
+ detectExcessSize();
+ }
+ return excessHeight;
+ }
+
+ private void detectExcessSize() {
+ // TODO define that iview cannot be themed and decorations should
+ // get to parent element, then get rid of this expensive and error
+ // prone function
+ final String overflow = getElement().getStyle().getProperty(
+ "overflow");
+ getElement().getStyle().setProperty("overflow", "hidden");
+ if (BrowserInfo.get().isIE()
+ && getElement().getPropertyInt("clientWidth") == 0) {
+ // can't detect possibly themed border/padding width in some
+ // situations (with some layout configurations), use empty div
+ // to measure width properly
+ DivElement div = Document.get().createDivElement();
+ div.setInnerHTML("&nbsp;");
+ div.getStyle().setProperty("overflow", "hidden");
+ div.getStyle().setProperty("height", "1px");
+ getElement().appendChild(div);
+ excessWidth = getElement().getOffsetWidth()
+ - div.getOffsetWidth();
+ getElement().removeChild(div);
+ } else {
+ excessWidth = getElement().getOffsetWidth()
+ - getElement().getPropertyInt("clientWidth");
+ }
+ excessHeight = getElement().getOffsetHeight()
+ - getElement().getPropertyInt("clientHeight");
+
+ getElement().getStyle().setProperty("overflow", overflow);
+ }
+
+ @Override
+ public int getWidth() {
+ return getElement().getOffsetWidth() - getExcessWidth();
+ }
+
+ private int getExcessWidth() {
+ if (excessWidth < 0) {
+ detectExcessSize();
+ }
+ return excessWidth;
+ }
+
+ @Override
+ public int getScrollbarSize() {
+ return Util.getNativeScrollbarSize();
+ }
+ };
+
+ public RenderSpace getAllocatedSpace(Widget child) {
+ return myRenderSpace;
+ }
+
+ public boolean hasChildComponent(Widget component) {
+ return (component != null && component == layout);
+ }
+
+ public void replaceChildComponent(Widget oldComponent, Widget newComponent) {
+ // TODO This is untested as no layouts require this
+ if (oldComponent != layout) {
+ return;
+ }
+
+ setWidget(newComponent);
+ layout = (Paintable) newComponent;
+ }
+
+ public boolean requestLayout(Set<Paintable> child) {
+ /*
+ * Can never propagate further and we do not want need to re-layout the
+ * layout which has caused this request.
+ */
+ return true;
+
+ }
+
+ public void updateCaption(Paintable component, UIDL uidl) {
+ // NOP Subwindows never draw caption for their first child (layout)
+ }
+
+ /**
+ * Return an iterator for current subwindows. This method is meant for
+ * testing purposes only.
+ *
+ * @return
+ */
+ public ArrayList<IWindow> getSubWindowList() {
+ ArrayList<IWindow> windows = new ArrayList<IWindow>(subWindows.size());
+ for (IWindow widget : subWindows) {
+ windows.add(widget);
+ }
+ return windows;
+ }
+
+}
diff --git a/src/com/vaadin/terminal/gwt/client/ui/IWindow.java b/src/com/vaadin/terminal/gwt/client/ui/IWindow.java
new file mode 100644
index 0000000000..eaa1c99da1
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/IWindow.java
@@ -0,0 +1,1001 @@
+/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui;
+
+import java.util.Iterator;
+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.Window;
+import com.google.gwt.user.client.ui.Frame;
+import com.google.gwt.user.client.ui.HasWidgets;
+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.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.BrowserInfo;
+import com.vaadin.terminal.gwt.client.Container;
+import com.vaadin.terminal.gwt.client.Paintable;
+import com.vaadin.terminal.gwt.client.RenderSpace;
+import com.vaadin.terminal.gwt.client.UIDL;
+import com.vaadin.terminal.gwt.client.Util;
+
+/**
+ * "Sub window" component.
+ *
+ * TODO update position / scroll position / size to client
+ *
+ * @author IT Mill Ltd
+ */
+public class IWindow extends IToolkitOverlay implements Container,
+ ScrollListener {
+
+ private static final int MIN_HEIGHT = 100;
+
+ private static final int MIN_WIDTH = 150;
+
+ private static Vector<IWindow> windowOrder = new Vector<IWindow>();
+
+ public static final String CLASSNAME = "i-window";
+
+ /**
+ * Pixels used by inner borders and paddings horizontally (calculated only
+ * once)
+ */
+ private int borderWidth = -1;
+
+ /**
+ * Pixels used by inner borders and paddings vertically (calculated only
+ * once)
+ */
+ private int borderHeight = -1;
+
+ private static final int STACKING_OFFSET_PIXELS = 15;
+
+ public static final int Z_INDEX = 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 vaadinModality = false;
+
+ private boolean resizable = true;
+
+ private Element modalityCurtain;
+ private Element draggingCurtain;
+
+ private Element headerText;
+
+ private boolean readonly;
+
+ boolean dynamicWidth = false;
+ boolean dynamicHeight = false;
+ boolean layoutRelativeWidth = false;
+ boolean layoutRelativeHeight = false;
+
+ // If centered (via UIDL), the window should stay in the centered -mode
+ // until a position is received from the server, or the user moves or
+ // resizes the window.
+ boolean centered = false;
+
+ private RenderSpace renderSpace = new RenderSpace(MIN_WIDTH, MIN_HEIGHT,
+ true);
+
+ private String width;
+
+ private String height;
+
+ private boolean immediate;
+
+ public IWindow() {
+ super(false, false, true); // no autohide, not modal, shadow
+ // Different style of shadow for windows
+ setShadowStyle("window");
+
+ 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++) {
+ 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) {
+ setZIndex(order + Z_INDEX);
+ }
+
+ @Override
+ protected void setZIndex(int zIndex) {
+ super.setZIndex(zIndex);
+ if (vaadinModality) {
+ DOM.setStyleAttribute(modalityCurtain, "zIndex", "" + zIndex);
+ }
+ }
+
+ protected void constructDOM() {
+ setStyleName(CLASSNAME);
+
+ 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);
+
+ 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")) {
+ hide();
+ return;
+ }
+
+ if (!uidl.hasAttribute("cached")) {
+ if (uidl.getBooleanAttribute("modal") != vaadinModality) {
+ setVaadinModality(!vaadinModality);
+ }
+ if (!isAttached()) {
+ show();
+ }
+ }
+
+ if (client.updateComponent(this, uidl, false)) {
+ return;
+ }
+
+ immediate = uidl.hasAttribute("immediate");
+
+ if (uidl.getBooleanAttribute("resizable") != resizable) {
+ setResizable(!resizable);
+ }
+
+ if (isReadOnly() != uidl.getBooleanAttribute("readonly")) {
+ setReadOnly(!isReadOnly());
+ }
+
+ // 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 (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);
+ layout = lo;
+ }
+
+ dynamicWidth = !uidl.hasAttribute("width");
+ dynamicHeight = !uidl.hasAttribute("height");
+
+ layoutRelativeWidth = uidl.hasAttribute("layoutRelativeWidth");
+ layoutRelativeHeight = uidl.hasAttribute("layoutRelativeHeight");
+
+ if (dynamicWidth && layoutRelativeWidth) {
+ /*
+ * Relative layout width, fix window width before rendering (width
+ * according to caption)
+ */
+ setNaturalWidth();
+ }
+
+ layout.updateFromUIDL(childUidl, client);
+ if (!dynamicHeight && layoutRelativeWidth) {
+ /*
+ * Relative layout width, and fixed height. Must update the size to
+ * be able to take scrollbars into account (layout gets narrower
+ * space if it is higher than the window) -> only vertical scrollbar
+ */
+ client.runDescendentsLayout(this);
+ }
+
+ /*
+ * No explicit width is set and the layout does not have relative width
+ * so fix the size according to the layout.
+ */
+ if (dynamicWidth && !layoutRelativeWidth) {
+ setNaturalWidth();
+ }
+
+ if (dynamicHeight && layoutRelativeHeight) {
+ // Prevent resizing until height has been fixed
+ resizable = false;
+ }
+
+ // 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 INotification(delay).show(html, position, style);
+ }
+ }
+ }
+
+ }
+
+ // setting scrollposition must happen after children is rendered
+ contentPanel.setScrollPosition(uidl.getIntVariable("scrollTop"));
+ contentPanel.setHorizontalScrollPosition(uidl
+ .getIntVariable("scrollLeft"));
+
+ // Center this window on screen if requested
+ // This has to be here because we might not know the content size before
+ // everything is painted into the window
+ if (uidl.getBooleanAttribute("center")) {
+ // mark as centered - this is unset on move/resize
+ centered = true;
+ center();
+ } else {
+ // don't try to center the window anymore
+ centered = false;
+ }
+
+ updateShadowSizeAndPosition();
+
+ // ensure window is not larger than browser window
+ if (getOffsetWidth() > Window.getClientWidth()) {
+ setWidth(Window.getClientWidth() + "px");
+ }
+ if (getOffsetHeight() > Window.getClientHeight()) {
+ setHeight(Window.getClientHeight() + "px");
+ }
+
+ if (dynamicHeight && layoutRelativeHeight) {
+ /*
+ * Window height is undefined, layout is 100% high so the layout
+ * should define the initial window height but on resize the layout
+ * should be as high as the window. We fix the height to deal with
+ * this.
+ */
+
+ int h = contents.getOffsetHeight() + getExtraHeight();
+ int w = contents.getOffsetWidth();
+
+ client.updateVariable(id, "height", h, false);
+ client.updateVariable(id, "width", w, true);
+ }
+
+ }
+
+ private void setNaturalWidth() {
+ /*
+ * For some reason IE6 has title DIV set to width 100% which messes this
+ * up. Also IE6 has a 0 wide element so we use the container element.
+ */
+ int naturalWidth;
+ if (BrowserInfo.get().isIE6()) {
+ String headerW = headerText.getStyle().getProperty("width");
+ headerText.getStyle().setProperty("width", "auto");
+ naturalWidth = getElement().getOffsetWidth();
+ headerText.getStyle().setProperty("width", headerW);
+ } else {
+ // use max(layout width, window width)
+ // i.e layout content width or caption width
+ int lowidth = contentPanel.getElement().getScrollWidth()
+ + borderWidth; // layout does not know about border
+ int elwidth = getElement().getOffsetWidth();
+ naturalWidth = (lowidth > elwidth ? lowidth : elwidth);
+ }
+
+ setWidth(naturalWidth + "px");
+ }
+
+ private void setReadOnly(boolean readonly) {
+ this.readonly = readonly;
+ if (readonly) {
+ DOM.setStyleAttribute(closeBox, "display", "none");
+ } else {
+ DOM.setStyleAttribute(closeBox, "display", "");
+ }
+ }
+
+ private boolean isReadOnly() {
+ return readonly;
+ }
+
+ @Override
+ public void show() {
+ if (vaadinModality) {
+ 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", "");
+ }
+ }
+ }
+
+ @Override
+ public void hide() {
+ if (vaadinModality) {
+ hideModalityCurtain();
+ }
+ super.hide();
+ }
+
+ private void setVaadinModality(boolean modality) {
+ vaadinModality = modality;
+ if (vaadinModality) {
+ modalityCurtain = DOM.createDiv();
+ DOM.setElementProperty(modalityCurtain, "className", CLASSNAME
+ + "-modalitycurtain");
+ if (isAttached()) {
+ showModalityCurtain();
+ bringToFront();
+ } else {
+ DeferredCommand.addCommand(new Command() {
+ public void execute() {
+ // vaadinModality 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.setStyleAttribute(modalityCurtain, "zIndex", ""
+ + (windowOrder.indexOf(this) + Z_INDEX));
+ 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", ""
+ + IToolkitOverlay.Z_INDEX);
+
+ DOM.appendChild(RootPanel.getBodyElement(), draggingCurtain);
+ } else if (!show && draggingCurtain != null) {
+
+ setFF2CaretFixEnabled(true); // makes FF2 slow
+
+ DOM.removeChild(RootPanel.getBodyElement(), draggingCurtain);
+ draggingCurtain = null;
+ }
+
+ }
+
+ private void setResizable(boolean resizability) {
+ resizable = resizability;
+ if (resizability) {
+ DOM.setElementProperty(resizeBox, "className", CLASSNAME
+ + "-resizebox");
+ } else {
+ DOM.setElementProperty(resizeBox, "className", CLASSNAME
+ + "-resizebox " + CLASSNAME + "-resizebox-disabled");
+ }
+ }
+
+ @Override
+ 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);
+ }
+
+ @Override
+ protected Element getContainerElement() {
+ // in GWT 1.5 this method is used in PopupPanel constructor
+ if (contents == null) {
+ return super.getContainerElement();
+ }
+ return contents;
+ }
+
+ @Override
+ public void onBrowserEvent(final Event event) {
+ if (event != null) {
+ final int type = event.getTypeInt();
+
+ 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 || resizeBox == target) {
+ onResizeEvent(event);
+ event.cancelBubble(true);
+ } else if (target == closeBox) {
+ if (type == Event.ONCLICK) {
+ onCloseClick();
+ event.cancelBubble(true);
+ }
+ } else if (dragging || !DOM.isOrHasChild(contents, target)) {
+ onDragEvent(event);
+ event.cancelBubble(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) {
+ if (resizable) {
+ switch (event.getTypeInt()) {
+ case Event.ONMOUSEDOWN:
+ if (!isActive()) {
+ bringToFront();
+ }
+ showDraggingCurtain(true);
+ if (BrowserInfo.get().isIE()) {
+ DOM.setStyleAttribute(resizeBox, "visibility", "hidden");
+ }
+ resizing = true;
+ startX = event.getScreenX();
+ startY = event.getScreenY();
+ origW = getElement().getOffsetWidth();
+ origH = getElement().getOffsetHeight();
+ DOM.setCapture(getElement());
+ event.preventDefault();
+ break;
+ case Event.ONMOUSEUP:
+ showDraggingCurtain(false);
+ if (BrowserInfo.get().isIE()) {
+ DOM.setStyleAttribute(resizeBox, "visibility", "");
+ }
+ resizing = false;
+ DOM.releaseCapture(getElement());
+ setSize(event, true);
+ break;
+ case Event.ONLOSECAPTURE:
+ showDraggingCurtain(false);
+ if (BrowserInfo.get().isIE()) {
+ DOM.setStyleAttribute(resizeBox, "visibility", "");
+ }
+ resizing = false;
+ case Event.ONMOUSEMOVE:
+ if (resizing) {
+ centered = false;
+ setSize(event, false);
+ event.preventDefault();
+ }
+ break;
+ default:
+ event.preventDefault();
+ break;
+ }
+ }
+ }
+
+ private void setSize(Event event, boolean updateVariables) {
+ int w = event.getScreenX() - startX + origW;
+ if (w < MIN_WIDTH + borderWidth) {
+ w = MIN_WIDTH + borderWidth;
+ }
+
+ int h = event.getScreenY() - startY + origH;
+ if (h < MIN_HEIGHT + getExtraHeight()) {
+ h = MIN_HEIGHT + getExtraHeight();
+ }
+
+ 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, immediate);
+ }
+
+ // Update child widget dimensions
+ if (client != null) {
+ client.handleComponentRelativeSize((Widget) layout);
+ client.runDescendentsLayout((HasWidgets) layout);
+ }
+
+ Util.runWebkitOverflowAutoFix(contentPanel.getElement());
+ }
+
+ @Override
+ /*
+ * Width is set to the out-most element (i-window).
+ *
+ * This function should never be called with percentage values (it will
+ * throw an exception)
+ */
+ public void setWidth(String width) {
+ this.width = width;
+ if (!isAttached()) {
+ return;
+ }
+ if (width != null && !"".equals(width)) {
+ int pixelWidth;
+ // Convert non-pixel values to pixels
+ if (width.indexOf("px") < 0) {
+ DOM.setStyleAttribute(getElement(), "width", width);
+ pixelWidth = getElement().getOffsetWidth();
+ width = pixelWidth + "px";
+ }
+ if (BrowserInfo.get().isIE6()) {
+ getElement().getStyle().setProperty("overflow", "hidden");
+ }
+ getElement().getStyle().setProperty("width", width);
+
+ pixelWidth = getElement().getOffsetWidth() - borderWidth;
+ if (pixelWidth < MIN_WIDTH) {
+ pixelWidth = MIN_WIDTH;
+ int rootWidth = pixelWidth + borderWidth;
+ DOM.setStyleAttribute(getElement(), "width", rootWidth + "px");
+ }
+
+ renderSpace.setWidth(pixelWidth);
+
+ // IE6 needs the actual inner content width on the content element,
+ // otherwise it won't wrap the content properly (no scrollbars
+ // appear, content flows out of window)
+ if (BrowserInfo.get().isIE6()) {
+ DOM.setStyleAttribute(contentPanel.getElement(), "width",
+ pixelWidth + "px");
+ }
+ updateShadowSizeAndPosition();
+ }
+ }
+
+ @Override
+ /*
+ * Height is set to the out-most element (i-window).
+ *
+ * This function should never be called with percentage values (it will
+ * throw an exception)
+ */
+ public void setHeight(String height) {
+ this.height = height;
+ if (!isAttached()) {
+ return;
+ }
+ if (height != null && !"".equals(height)) {
+ DOM.setStyleAttribute(getElement(), "height", height);
+ int pixels = getElement().getOffsetHeight() - getExtraHeight();
+ if (pixels < MIN_HEIGHT) {
+ pixels = MIN_HEIGHT;
+ int rootHeight = pixels + getExtraHeight();
+ DOM.setStyleAttribute(getElement(), "height", (rootHeight)
+ + "px");
+
+ }
+ renderSpace.setHeight(pixels);
+ height = pixels + "px";
+ contentPanel.getElement().getStyle().setProperty("height", height);
+ updateShadowSizeAndPosition();
+
+ }
+ }
+
+ private int extraH = 0;
+
+ private int getExtraHeight() {
+ extraH = header.getOffsetHeight() + footer.getOffsetHeight();
+ return extraH;
+ }
+
+ 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) {
+ centered = false;
+ 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;
+ }
+ }
+
+ @Override
+ public boolean onEventPreview(Event event) {
+ if (dragging) {
+ onDragEvent(event);
+ return false;
+ } else if (resizing) {
+ onResizeEvent(event);
+ return false;
+ } else if (vaadinModality) {
+ // return false when modal and outside window
+ final Element target = event.getTarget().cast();
+ 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);
+ }
+
+ @Override
+ 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);
+ }
+
+ @Override
+ protected void onAttach() {
+ super.onAttach();
+
+ // Calculate space required by window borders, so we can accurately
+ // calculate space for content
+
+ // IE (IE6 especially) requires some magic tricks to pull the border
+ // size correctly (remember that we want to accomodate for paddings as
+ // well)
+ if (BrowserInfo.get().isIE()) {
+ DOM.setStyleAttribute(contents, "width", "7000px");
+ DOM.setStyleAttribute(contentPanel.getElement(), "width", "7000px");
+ int contentWidth = DOM.getElementPropertyInt(contentPanel
+ .getElement(), "offsetWidth");
+ contentWidth = DOM.getElementPropertyInt(contentPanel.getElement(),
+ "offsetWidth");
+ final int windowWidth = DOM.getElementPropertyInt(getElement(),
+ "offsetWidth");
+ DOM.setStyleAttribute(contentPanel.getElement(), "width", "");
+ DOM.setStyleAttribute(contents, "width", "");
+
+ borderWidth = windowWidth - contentWidth;
+ }
+
+ // Standards based browsers get away with it a little easier :)
+ else {
+ final int contentWidth = DOM.getElementPropertyInt(contentPanel
+ .getElement(), "offsetWidth");
+ final int windowWidth = DOM.getElementPropertyInt(getElement(),
+ "offsetWidth");
+ borderWidth = windowWidth - contentWidth;
+ }
+
+ setWidth(width);
+ setHeight(height);
+
+ }
+
+ public RenderSpace getAllocatedSpace(Widget child) {
+ if (child == layout) {
+ return renderSpace;
+ } else {
+ // Exception ??
+ return null;
+ }
+ }
+
+ public boolean hasChildComponent(Widget component) {
+ if (component == layout) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ public void replaceChildComponent(Widget oldComponent, Widget newComponent) {
+ contentPanel.setWidget(newComponent);
+ }
+
+ public boolean requestLayout(Set<Paintable> child) {
+ if (dynamicWidth && !layoutRelativeWidth) {
+ setNaturalWidth();
+ }
+ if (centered) {
+ center();
+ }
+ updateShadowSizeAndPosition();
+ return true;
+ }
+
+ public void updateCaption(Paintable component, UIDL uidl) {
+ // NOP, window has own caption, layout captio not rendered
+ }
+
+}
diff --git a/src/com/vaadin/terminal/gwt/client/ui/Icon.java b/src/com/vaadin/terminal/gwt/client/ui/Icon.java
new file mode 100644
index 0000000000..d17227955e
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/Icon.java
@@ -0,0 +1,44 @@
+/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui;
+
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.Event;
+import com.google.gwt.user.client.ui.UIObject;
+import com.vaadin.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", "");
+ 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)) {
+ /*
+ * Start sinking onload events, widgets responsibility to react. We
+ * must do this BEFORE we set src as IE fires the event immediately
+ * if the image is found in cache (#2592).
+ */
+ sinkEvents(Event.ONLOAD);
+
+ String uri = client.translateToolkitUri(uidlUri);
+ DOM.setElementProperty(getElement(), "src", uri);
+ myUri = uidlUri;
+ }
+ }
+
+}
diff --git a/src/com/vaadin/terminal/gwt/client/ui/MenuBar.java b/src/com/vaadin/terminal/gwt/client/ui/MenuBar.java
new file mode 100644
index 0000000000..a711864a37
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/MenuBar.java
@@ -0,0 +1,514 @@
+/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.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
+ */
+@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;
+ }
+
+ @Override
+ 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;
+ }
+
+ @Override
+ 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 IToolkitOverlay(true) {
+ {
+ setWidget(item.getSubMenu());
+ item.getSubMenu().onShow();
+ }
+
+ @Override
+ 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/vaadin/terminal/gwt/client/ui/MenuItem.java b/src/com/vaadin/terminal/gwt/client/ui/MenuItem.java
new file mode 100644
index 0000000000..7ef8b6dff6
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/MenuItem.java
@@ -0,0 +1,189 @@
+/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.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
+ */
+@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/vaadin/terminal/gwt/client/ui/ShortcutActionHandler.java b/src/com/vaadin/terminal/gwt/client/ui/ShortcutActionHandler.java
new file mode 100644
index 0000000000..3acdeab171
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/ShortcutActionHandler.java
@@ -0,0 +1,178 @@
+/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.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.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.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(final 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)) {
+ DOM.eventPreventDefault(event);
+ 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/vaadin/terminal/gwt/client/ui/SubPartAware.java b/src/com/vaadin/terminal/gwt/client/ui/SubPartAware.java
new file mode 100644
index 0000000000..ceec985f4f
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/SubPartAware.java
@@ -0,0 +1,11 @@
+package com.vaadin.terminal.gwt.client.ui;
+
+import com.google.gwt.user.client.Element;
+
+public interface SubPartAware {
+
+ Element getSubPartElement(String subPart);
+
+ String getSubPartName(Element subElement);
+
+} \ No newline at end of file
diff --git a/src/com/vaadin/terminal/gwt/client/ui/Table.java b/src/com/vaadin/terminal/gwt/client/ui/Table.java
new file mode 100644
index 0000000000..ac59bc0064
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/Table.java
@@ -0,0 +1,15 @@
+/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui;
+
+import com.google.gwt.user.client.ui.HasWidgets;
+import com.vaadin.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/vaadin/terminal/gwt/client/ui/TreeAction.java b/src/com/vaadin/terminal/gwt/client/ui/TreeAction.java
new file mode 100644
index 0000000000..a77730f749
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/TreeAction.java
@@ -0,0 +1,56 @@
+/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.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.
+ */
+ @Override
+ 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/vaadin/terminal/gwt/client/ui/TreeImages.java b/src/com/vaadin/terminal/gwt/client/ui/TreeImages.java
new file mode 100644
index 0000000000..db53653e71
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/TreeImages.java
@@ -0,0 +1,31 @@
+/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui;
+
+import com.google.gwt.user.client.ui.AbstractImagePrototype;
+
+public interface TreeImages extends com.google.gwt.user.client.ui.TreeImages {
+
+ /**
+ * An image indicating an open branch.
+ *
+ * @return a prototype of this image
+ * @gwt.resource
+ * com/itmill/toolkit/terminal/gwt/public/default/tree/img/expanded
+ * .png
+ */
+ AbstractImagePrototype treeOpen();
+
+ /**
+ * An image indicating a closed branch.
+ *
+ * @return a prototype of this image
+ * @gwt.resource
+ * com/itmill/toolkit/terminal/gwt/public/default/tree/img/collapsed
+ * .png
+ */
+ AbstractImagePrototype treeClosed();
+
+}
diff --git a/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_alignment.png b/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_alignment.png
new file mode 100644
index 0000000000..49b918ec0c
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_alignment.png
Binary files differ
diff --git a/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_component_handles_the_caption.png b/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_component_handles_the_caption.png
new file mode 100644
index 0000000000..9fd6635765
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_component_handles_the_caption.png
Binary files differ
diff --git a/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_h150.png b/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_h150.png
new file mode 100644
index 0000000000..7cd07369dc
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_h150.png
Binary files differ
diff --git a/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_horizontal.png b/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_horizontal.png
new file mode 100644
index 0000000000..c2e1f49efe
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_horizontal.png
Binary files differ
diff --git a/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_horizontal_spacing.png b/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_horizontal_spacing.png
new file mode 100644
index 0000000000..417c9aecfd
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_horizontal_spacing.png
Binary files differ
diff --git a/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_margin.png b/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_margin.png
new file mode 100644
index 0000000000..2f1e461b0a
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_margin.png
Binary files differ
diff --git a/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_no_caption.png b/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_no_caption.png
new file mode 100644
index 0000000000..63984cdee7
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_no_caption.png
Binary files differ
diff --git a/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_normal_caption.png b/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_normal_caption.png
new file mode 100644
index 0000000000..1e730c072b
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_normal_caption.png
Binary files differ
diff --git a/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_special-margin.png b/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_special-margin.png
new file mode 100644
index 0000000000..34e47d1551
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_special-margin.png
Binary files differ
diff --git a/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_vertical.png b/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_vertical.png
new file mode 100644
index 0000000000..99e3709acc
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_vertical.png
Binary files differ
diff --git a/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_vertical_spacing.png b/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_vertical_spacing.png
new file mode 100644
index 0000000000..be9a4cd8c5
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_vertical_spacing.png
Binary files differ
diff --git a/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_w300.png b/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_w300.png
new file mode 100644
index 0000000000..0b555ad1e7
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_w300.png
Binary files differ
diff --git a/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_w300_h150.png b/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_w300_h150.png
new file mode 100644
index 0000000000..8ff42ed0f4
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/doc-files/IOrderedLayout_w300_h150.png
Binary files differ
diff --git a/src/com/vaadin/terminal/gwt/client/ui/layout/CellBasedLayout.java b/src/com/vaadin/terminal/gwt/client/ui/layout/CellBasedLayout.java
new file mode 100644
index 0000000000..5c2c2f0204
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/layout/CellBasedLayout.java
@@ -0,0 +1,335 @@
+package com.vaadin.terminal.gwt.client.ui.layout;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import com.google.gwt.dom.client.DivElement;
+import com.google.gwt.dom.client.Document;
+import com.google.gwt.dom.client.Style;
+import com.google.gwt.user.client.ui.ComplexPanel;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.BrowserInfo;
+import com.vaadin.terminal.gwt.client.Container;
+import com.vaadin.terminal.gwt.client.Paintable;
+import com.vaadin.terminal.gwt.client.UIDL;
+import com.vaadin.terminal.gwt.client.ui.IMarginInfo;
+
+public abstract class CellBasedLayout extends ComplexPanel implements Container {
+
+ protected Map<Widget, ChildComponentContainer> widgetToComponentContainer = new HashMap<Widget, ChildComponentContainer>();
+
+ protected ApplicationConnection client = null;
+
+ protected DivElement root;
+
+ public static final int ORIENTATION_VERTICAL = 0;
+ public static final int ORIENTATION_HORIZONTAL = 1;
+
+ protected Margins activeMargins = new Margins(0, 0, 0, 0);
+ protected IMarginInfo activeMarginsInfo = new IMarginInfo(-1);
+
+ protected boolean spacingEnabled = false;
+ protected final Spacing spacingFromCSS = new Spacing(12, 12);
+ protected final Spacing activeSpacing = new Spacing(0, 0);
+
+ private boolean dynamicWidth;
+
+ private boolean dynamicHeight;
+
+ private final DivElement clearElement = Document.get().createDivElement();
+
+ private String lastStyleName = "";
+
+ private boolean marginsNeedsRecalculation = false;
+
+ protected String STYLENAME_SPACING = "";
+ protected String STYLENAME_MARGIN_TOP = "";
+ protected String STYLENAME_MARGIN_RIGHT = "";
+ protected String STYLENAME_MARGIN_BOTTOM = "";
+ protected String STYLENAME_MARGIN_LEFT = "";
+
+ public static class Spacing {
+
+ public int hSpacing = 0;
+ public int vSpacing = 0;
+
+ public Spacing(int hSpacing, int vSpacing) {
+ this.hSpacing = hSpacing;
+ this.vSpacing = vSpacing;
+ }
+
+ @Override
+ public String toString() {
+ return "Spacing [hSpacing=" + hSpacing + ",vSpacing=" + vSpacing
+ + "]";
+ }
+
+ }
+
+ public CellBasedLayout() {
+ super();
+
+ setElement(Document.get().createDivElement());
+ getElement().getStyle().setProperty("overflow", "hidden");
+ if (BrowserInfo.get().isIE()) {
+ getElement().getStyle().setProperty("position", "relative");
+ getElement().getStyle().setProperty("zoom", "1");
+ }
+
+ root = Document.get().createDivElement();
+ root.getStyle().setProperty("overflow", "hidden");
+ if (BrowserInfo.get().isIE()) {
+ root.getStyle().setProperty("position", "relative");
+ }
+
+ getElement().appendChild(root);
+
+ Style style = clearElement.getStyle();
+ style.setProperty("width", "0px");
+ style.setProperty("height", "0px");
+ style.setProperty("clear", "both");
+ style.setProperty("overflow", "hidden");
+ root.appendChild(clearElement);
+
+ }
+
+ public boolean hasChildComponent(Widget component) {
+ return widgetToComponentContainer.containsKey(component);
+ }
+
+ public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
+ this.client = client;
+
+ // Only non-cached UIDL:s can introduce changes
+ if (uidl.getBooleanAttribute("cached")) {
+ return;
+ }
+
+ /**
+ * Margin and spacind detection depends on classNames and must be set
+ * before setting size. Here just update the details from UIDL and from
+ * overridden setStyleName run actual margin detections.
+ */
+ updateMarginAndSpacingInfo(uidl);
+
+ /*
+ * This call should be made first. Ensure correct implementation, handle
+ * size etc.
+ */
+ if (client.updateComponent(this, uidl, true)) {
+ return;
+ }
+
+ handleDynamicDimensions(uidl);
+
+ }
+
+ @Override
+ public void setStyleName(String styleName) {
+ super.setStyleName(styleName);
+
+ if (isAttached() && marginsNeedsRecalculation
+ || !lastStyleName.equals(styleName)) {
+ measureMarginsAndSpacing();
+ lastStyleName = styleName;
+ marginsNeedsRecalculation = false;
+ }
+
+ }
+
+ private void handleDynamicDimensions(UIDL uidl) {
+ String w = uidl.hasAttribute("width") ? uidl
+ .getStringAttribute("width") : "";
+
+ String h = uidl.hasAttribute("height") ? uidl
+ .getStringAttribute("height") : "";
+
+ if (w.equals("")) {
+ dynamicWidth = true;
+ } else {
+ dynamicWidth = false;
+ }
+
+ if (h.equals("")) {
+ dynamicHeight = true;
+ } else {
+ dynamicHeight = false;
+ }
+
+ }
+
+ protected void addOrMoveChild(ChildComponentContainer childComponent,
+ int position) {
+ if (childComponent.getParent() == this) {
+ if (getWidgetIndex(childComponent) != position) {
+ // Detach from old position child.
+ childComponent.removeFromParent();
+
+ // Logical attach.
+ getChildren().insert(childComponent, position);
+
+ root.insertBefore(childComponent.getElement(), root
+ .getChildNodes().getItem(position));
+
+ adopt(childComponent);
+ }
+ } else {
+ widgetToComponentContainer.put(childComponent.getWidget(),
+ childComponent);
+
+ // Logical attach.
+ getChildren().insert(childComponent, position);
+
+ // avoid inserts (they are slower than appends)
+ boolean insert = true;
+ if (widgetToComponentContainer.size() == position) {
+ insert = false;
+ }
+ if (insert) {
+ root.insertBefore(childComponent.getElement(), root
+ .getChildNodes().getItem(position));
+ } else {
+ root.insertBefore(childComponent.getElement(), clearElement);
+ }
+ // Adopt.
+ adopt(childComponent);
+
+ }
+
+ }
+
+ protected ChildComponentContainer getComponentContainer(Widget child) {
+ return widgetToComponentContainer.get(child);
+ }
+
+ protected boolean isDynamicWidth() {
+ return dynamicWidth;
+ }
+
+ protected boolean isDynamicHeight() {
+ return dynamicHeight;
+ }
+
+ private void updateMarginAndSpacingInfo(UIDL uidl) {
+ int bitMask = uidl.getIntAttribute("margins");
+ if (activeMarginsInfo.getBitMask() != bitMask) {
+ activeMarginsInfo = new IMarginInfo(bitMask);
+ marginsNeedsRecalculation = true;
+ }
+ boolean spacing = uidl.getBooleanAttribute("spacing");
+ if (spacing != spacingEnabled) {
+ marginsNeedsRecalculation = true;
+ spacingEnabled = spacing;
+ }
+ }
+
+ protected boolean measureMarginsAndSpacing() {
+ if (!isAttached()) {
+ return false;
+ }
+
+ DivElement measurement = Document.get().createDivElement();
+ Style style = measurement.getStyle();
+ style.setProperty("position", "absolute");
+ style.setProperty("top", "0");
+ style.setProperty("left", "0");
+ style.setProperty("width", "0");
+ style.setProperty("height", "0");
+ style.setProperty("visibility", "hidden");
+ style.setProperty("overflow", "hidden");
+ root.appendChild(measurement);
+
+ // Measure spacing (actually CSS padding)
+ measurement.setClassName(STYLENAME_SPACING
+ + (spacingEnabled ? "-on" : "-off"));
+ activeSpacing.vSpacing = measurement.getOffsetHeight();
+ activeSpacing.hSpacing = measurement.getOffsetWidth();
+
+ DivElement measurement2 = Document.get().createDivElement();
+ style = measurement2.getStyle();
+ style.setProperty("width", "0px");
+ style.setProperty("height", "0px");
+ style.setProperty("visibility", "hidden");
+ style.setProperty("overflow", "hidden");
+
+ measurement.appendChild(measurement2);
+
+ String sn = getStylePrimaryName() + "-margin";
+
+ if (activeMarginsInfo.hasTop()) {
+ sn += " " + STYLENAME_MARGIN_TOP;
+ }
+ if (activeMarginsInfo.hasBottom()) {
+ sn += " " + STYLENAME_MARGIN_BOTTOM;
+ }
+ if (activeMarginsInfo.hasLeft()) {
+ sn += " " + STYLENAME_MARGIN_LEFT;
+ }
+ if (activeMarginsInfo.hasRight()) {
+ sn += " " + STYLENAME_MARGIN_RIGHT;
+ }
+
+ // Measure top and left margins (actually CSS padding)
+ measurement.setClassName(sn);
+
+ activeMargins.setMarginTop(measurement2.getOffsetTop());
+ activeMargins.setMarginLeft(measurement2.getOffsetLeft());
+ activeMargins.setMarginRight(measurement.getOffsetWidth()
+ - activeMargins.getMarginLeft());
+ activeMargins.setMarginBottom(measurement.getOffsetHeight()
+ - activeMargins.getMarginTop());
+
+ // ApplicationConnection.getConsole().log("Margins: " + activeMargins);
+ // ApplicationConnection.getConsole().log("Spacing: " + activeSpacing);
+ // Util.alert("Margins: " + activeMargins);
+ root.removeChild(measurement);
+
+ // apply margin
+ style = root.getStyle();
+ style.setPropertyPx("marginLeft", activeMargins.getMarginLeft());
+ style.setPropertyPx("marginRight", activeMargins.getMarginRight());
+ style.setPropertyPx("marginTop", activeMargins.getMarginTop());
+ style.setPropertyPx("marginBottom", activeMargins.getMarginBottom());
+
+ return true;
+ }
+
+ protected ChildComponentContainer getFirstChildComponentContainer() {
+ int size = getChildren().size();
+ if (size < 1) {
+ return null;
+ }
+
+ return (ChildComponentContainer) getChildren().get(0);
+ }
+
+ protected void removeChildrenAfter(int pos) {
+ // Remove all children after position "pos" but leave the clear element
+ // in place
+
+ int toRemove = getChildren().size() - pos;
+ while (toRemove-- > 0) {
+ ChildComponentContainer child = (ChildComponentContainer) getChildren()
+ .get(pos);
+ widgetToComponentContainer.remove(child.getWidget());
+ remove(child);
+ Paintable p = (Paintable) child.getWidget();
+ client.unregisterPaintable(p);
+ }
+
+ }
+
+ public void replaceChildComponent(Widget oldComponent, Widget newComponent) {
+ ChildComponentContainer componentContainer = widgetToComponentContainer
+ .remove(oldComponent);
+ if (componentContainer == null) {
+ return;
+ }
+
+ componentContainer.setWidget(newComponent);
+ client.unregisterPaintable((Paintable) oldComponent);
+ widgetToComponentContainer.put(newComponent, componentContainer);
+ }
+
+}
diff --git a/src/com/vaadin/terminal/gwt/client/ui/layout/ChildComponentContainer.java b/src/com/vaadin/terminal/gwt/client/ui/layout/ChildComponentContainer.java
new file mode 100644
index 0000000000..158138359d
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/layout/ChildComponentContainer.java
@@ -0,0 +1,736 @@
+package com.vaadin.terminal.gwt.client.ui.layout;
+
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+
+import com.google.gwt.dom.client.DivElement;
+import com.google.gwt.dom.client.Document;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.dom.client.Style;
+import com.google.gwt.dom.client.TableElement;
+import com.google.gwt.user.client.ui.Panel;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.BrowserInfo;
+import com.vaadin.terminal.gwt.client.ICaption;
+import com.vaadin.terminal.gwt.client.Paintable;
+import com.vaadin.terminal.gwt.client.UIDL;
+import com.vaadin.terminal.gwt.client.Util;
+import com.vaadin.terminal.gwt.client.RenderInformation.FloatSize;
+import com.vaadin.terminal.gwt.client.RenderInformation.Size;
+import com.vaadin.terminal.gwt.client.ui.AlignmentInfo;
+
+public class ChildComponentContainer extends Panel {
+
+ /**
+ * Size of the container DIV excluding any margins and also excluding the
+ * expansion amount (containerExpansion)
+ */
+ private Size contSize = new Size(0, 0);
+
+ /**
+ * Size of the widget inside the container DIV
+ */
+ private Size widgetSize = new Size(0, 0);
+ /**
+ * Size of the caption
+ */
+ private int captionRequiredWidth = 0;
+ private int captionWidth = 0;
+ private int captionHeight = 0;
+
+ /**
+ * Padding added to the container when it is larger than the component.
+ */
+ private Size containerExpansion = new Size(0, 0);
+
+ private float expandRatio;
+
+ private int containerMarginLeft = 0;
+ private int containerMarginTop = 0;
+
+ AlignmentInfo alignment = AlignmentInfo.TOP_LEFT;
+
+ private int alignmentLeftOffsetForWidget = 0;
+ private int alignmentLeftOffsetForCaption = 0;
+ /**
+ * Top offset for implementing alignment. Top offset is set to the container
+ * DIV as it otherwise would have to be set to either the Caption or the
+ * Widget depending on whether there is a caption and where the caption is
+ * located.
+ */
+ private int alignmentTopOffset = 0;
+
+ // private Margins alignmentOffset = new Margins(0, 0, 0, 0);
+ private ICaption caption = null;
+ private DivElement containerDIV;
+ private DivElement widgetDIV;
+ private Widget widget;
+ private FloatSize relativeSize = null;
+
+ public ChildComponentContainer(Widget widget, int orientation) {
+ super();
+
+ containerDIV = Document.get().createDivElement();
+
+ widgetDIV = Document.get().createDivElement();
+ if (BrowserInfo.get().isFF2()) {
+ Style style = widgetDIV.getStyle();
+ // FF2 chokes on some floats very easily. Measuring size escpecially
+ // becomes terribly slow
+ TableElement tableEl = Document.get().createTableElement();
+ tableEl
+ .setInnerHTML("<tbody><tr><td><div></div></td></tr></tbody>");
+ DivElement div = (DivElement) tableEl.getFirstChildElement()
+ .getFirstChildElement().getFirstChildElement()
+ .getFirstChildElement();
+ tableEl.setCellPadding(0);
+ tableEl.setCellSpacing(0);
+ tableEl.setBorder(0);
+ div.getStyle().setProperty("padding", "0");
+
+ setElement(tableEl);
+ containerDIV = div;
+ } else {
+ setFloat(widgetDIV, "left");
+ setElement(containerDIV);
+ containerDIV.getStyle().setProperty("height", "0");
+ containerDIV.getStyle().setProperty("width", "0px");
+ containerDIV.getStyle().setProperty("overflow", "hidden");
+ }
+
+ if (BrowserInfo.get().isIE()) {
+ /*
+ * IE requires position: relative on overflow:hidden elements if
+ * they should hide position:relative elements. Without this e.g. a
+ * 1000x1000 Panel inside an 500x500 OrderedLayout will not be
+ * clipped but fully shown.
+ */
+ containerDIV.getStyle().setProperty("position", "relative");
+ widgetDIV.getStyle().setProperty("position", "relative");
+ }
+
+ containerDIV.appendChild(widgetDIV);
+
+ setOrientation(orientation);
+
+ setWidget(widget);
+
+ }
+
+ public void setWidget(Widget w) {
+ // Validate
+ if (w == widget) {
+ return;
+ }
+
+ // Detach new child.
+ if (w != null) {
+ w.removeFromParent();
+ }
+
+ // Remove old child.
+ if (widget != null) {
+ remove(widget);
+ }
+
+ // Logical attach.
+ widget = w;
+
+ if (w != null) {
+ // Physical attach.
+ widgetDIV.appendChild(widget.getElement());
+ adopt(w);
+ }
+ }
+
+ private static void setFloat(Element div, String floatString) {
+ if (BrowserInfo.get().isIE()) {
+ div.getStyle().setProperty("styleFloat", floatString);
+ // IE requires display:inline for margin-left to work together
+ // with float:left
+ if (floatString.equals("left")) {
+ div.getStyle().setProperty("display", "inline");
+ } else {
+ div.getStyle().setProperty("display", "block");
+ }
+
+ } else {
+ div.getStyle().setProperty("cssFloat", floatString);
+ }
+ }
+
+ public void setOrientation(int orientation) {
+ if (orientation == CellBasedLayout.ORIENTATION_HORIZONTAL) {
+ setFloat(getElement(), "left");
+ } else {
+ setFloat(getElement(), "");
+ }
+ setHeight("0px");
+ // setWidth("0px");
+ contSize.setHeight(0);
+ contSize.setWidth(0);
+ containerMarginLeft = 0;
+ containerMarginTop = 0;
+ containerDIV.getStyle().setProperty("paddingLeft", "0");
+ containerDIV.getStyle().setProperty("paddingTop", "0");
+
+ containerExpansion.setHeight(0);
+ containerExpansion.setWidth(0);
+
+ // Clear old alignments
+ clearAlignments();
+
+ }
+
+ public void renderChild(UIDL childUIDL, ApplicationConnection client,
+ int fixedWidth) {
+ /*
+ * Must remove width specification from container before rendering to
+ * allow components to grow in horizontal direction.
+ *
+ * For fixed width layouts we specify the width directly so that height
+ * is automatically calculated correctly (e.g. for Labels).
+ */
+ /*
+ * This should no longer be needed (after #2563) as all components are
+ * such that they can be rendered inside a 0x0 DIV.
+ */
+ // if (fixedWidth > 0) {
+ // setLimitedContainerWidth(fixedWidth);
+ // } else {
+ // setUnlimitedContainerWidth();
+ // }
+ ((Paintable) widget).updateFromUIDL(childUIDL, client);
+ }
+
+ public void setUnlimitedContainerWidth() {
+ setLimitedContainerWidth(1000000);
+ }
+
+ public void setLimitedContainerWidth(int width) {
+ containerDIV.getStyle().setProperty("width", width + "px");
+ }
+
+ public void updateWidgetSize() {
+ /*
+ * Widget wrapper includes margin which the widget offsetWidth/Height
+ * does not include
+ */
+ int w = Util.getRequiredWidth(widgetDIV);
+ int h = Util.getRequiredHeight(widgetDIV);
+
+ widgetSize.setWidth(w);
+ widgetSize.setHeight(h);
+
+ // ApplicationConnection.getConsole().log(
+ // Util.getSimpleName(widget) + " size is " + w + "," + h);
+
+ }
+
+ public void setMarginLeft(int marginLeft) {
+ containerMarginLeft = marginLeft;
+ containerDIV.getStyle().setPropertyPx("paddingLeft", marginLeft);
+ }
+
+ public void setMarginTop(int marginTop) {
+ containerMarginTop = marginTop;
+ containerDIV.getStyle().setPropertyPx("paddingTop",
+ marginTop + alignmentTopOffset);
+
+ updateContainerDOMSize();
+ }
+
+ public void updateAlignments(int parentWidth, int parentHeight) {
+ if (parentHeight == -1) {
+ parentHeight = contSize.getHeight();
+ }
+ if (parentWidth == -1) {
+ parentWidth = contSize.getWidth();
+ }
+
+ alignmentTopOffset = calculateVerticalAlignmentTopOffset(parentHeight);
+
+ calculateHorizontalAlignment(parentWidth);
+
+ applyAlignments();
+
+ }
+
+ private void applyAlignments() {
+
+ // Update top margin to take alignment into account
+ setMarginTop(containerMarginTop);
+
+ if (caption != null) {
+ caption.getElement().getStyle().setPropertyPx("marginLeft",
+ alignmentLeftOffsetForCaption);
+ }
+ widgetDIV.getStyle().setPropertyPx("marginLeft",
+ alignmentLeftOffsetForWidget);
+ }
+
+ public int getCaptionRequiredWidth() {
+ if (caption == null) {
+ return 0;
+ }
+
+ return captionRequiredWidth;
+ }
+
+ public int getCaptionWidth() {
+ if (caption == null) {
+ return 0;
+ }
+
+ return captionWidth;
+ }
+
+ public int getCaptionHeight() {
+ if (caption == null) {
+ return 0;
+ }
+
+ return captionHeight;
+ }
+
+ public int getCaptionWidthAfterComponent() {
+ if (caption == null || !caption.shouldBePlacedAfterComponent()) {
+ return 0;
+ }
+
+ return getCaptionWidth();
+ }
+
+ public int getCaptionHeightAboveComponent() {
+ if (caption == null || caption.shouldBePlacedAfterComponent()) {
+ return 0;
+ }
+
+ return getCaptionHeight();
+ }
+
+ private int calculateVerticalAlignmentTopOffset(int emptySpace) {
+ if (alignment.isTop()) {
+ return 0;
+ }
+
+ if (caption != null) {
+ if (caption.shouldBePlacedAfterComponent()) {
+ /*
+ * Take into account the rare case that the caption on the right
+ * side of the component AND is higher than the component
+ */
+ emptySpace -= Math.max(widgetSize.getHeight(), caption
+ .getHeight());
+ } else {
+ emptySpace -= widgetSize.getHeight();
+ emptySpace -= getCaptionHeight();
+ }
+ } else {
+ /*
+ * There is no caption and thus we do not need to take anything but
+ * the widget into account
+ */
+ emptySpace -= widgetSize.getHeight();
+ }
+
+ int top = 0;
+ if (alignment.isVerticalCenter()) {
+ top = emptySpace / 2;
+ } else if (alignment.isBottom()) {
+ top = emptySpace;
+ }
+
+ if (top < 0) {
+ top = 0;
+ }
+ return top;
+ }
+
+ private void calculateHorizontalAlignment(int emptySpace) {
+ alignmentLeftOffsetForCaption = 0;
+ alignmentLeftOffsetForWidget = 0;
+
+ if (alignment.isLeft()) {
+ return;
+ }
+
+ int captionSpace = emptySpace;
+ int widgetSpace = emptySpace;
+
+ if (caption != null) {
+ // There is a caption
+ if (caption.shouldBePlacedAfterComponent()) {
+ /*
+ * The caption is after component. In this case the caption
+ * needs no alignment.
+ */
+ captionSpace = 0;
+ widgetSpace -= widgetSize.getWidth();
+ widgetSpace -= getCaptionWidth();
+ } else {
+ /*
+ * The caption is above the component. Caption and widget needs
+ * separate alignment offsets.
+ */
+ widgetSpace -= widgetSize.getWidth();
+ captionSpace -= getCaptionWidth();
+ }
+ } else {
+ /*
+ * There is no caption and thus we do not need to take anything but
+ * the widget into account
+ */
+ captionSpace = 0;
+ widgetSpace -= widgetSize.getWidth();
+ }
+
+ if (alignment.isHorizontalCenter()) {
+ alignmentLeftOffsetForCaption = captionSpace / 2;
+ alignmentLeftOffsetForWidget = widgetSpace / 2;
+ } else if (alignment.isRight()) {
+ alignmentLeftOffsetForCaption = captionSpace;
+ alignmentLeftOffsetForWidget = widgetSpace;
+ }
+
+ if (alignmentLeftOffsetForCaption < 0) {
+ alignmentLeftOffsetForCaption = 0;
+ }
+ if (alignmentLeftOffsetForWidget < 0) {
+ alignmentLeftOffsetForWidget = 0;
+ }
+
+ }
+
+ public void setAlignment(AlignmentInfo alignmentInfo) {
+ alignment = alignmentInfo;
+
+ }
+
+ public Size getWidgetSize() {
+ return widgetSize;
+ }
+
+ public void updateCaption(UIDL uidl, ApplicationConnection client) {
+ if (ICaption.isNeeded(uidl)) {
+ // We need a caption
+
+ ICaption newCaption = caption;
+
+ if (newCaption == null) {
+ newCaption = new ICaption((Paintable) widget, client);
+ // Set initial height to avoid Safari flicker
+ newCaption.setHeight("18px");
+ // newCaption.setHeight(newCaption.getHeight()); // This might
+ // be better... ??
+ }
+
+ boolean positionChanged = newCaption.updateCaption(uidl);
+
+ if (newCaption != caption || positionChanged) {
+ setCaption(newCaption);
+ }
+
+ } else {
+ // Caption is not needed
+ if (caption != null) {
+ remove(caption);
+ }
+
+ }
+
+ updateCaptionSize();
+ }
+
+ public void updateCaptionSize() {
+ captionWidth = 0;
+ captionHeight = 0;
+
+ if (caption != null) {
+ captionWidth = caption.getRenderedWidth();
+ captionHeight = caption.getHeight();
+ captionRequiredWidth = caption.getRequiredWidth();
+
+ /*
+ * ApplicationConnection.getConsole().log(
+ * "Caption rendered width: " + captionWidth +
+ * ", caption required width: " + captionRequiredWidth +
+ * ", caption height: " + captionHeight);
+ */
+ }
+
+ }
+
+ private void setCaption(ICaption newCaption) {
+ // Validate
+ // if (newCaption == caption) {
+ // return;
+ // }
+
+ // Detach new child.
+ if (newCaption != null) {
+ newCaption.removeFromParent();
+ }
+
+ // Remove old child.
+ if (caption != null && newCaption != caption) {
+ remove(caption);
+ }
+
+ // Logical attach.
+ caption = newCaption;
+
+ if (caption != null) {
+ // Physical attach.
+ if (caption.shouldBePlacedAfterComponent()) {
+ Util.setFloat(caption.getElement(), "left");
+ containerDIV.appendChild(caption.getElement());
+ } else {
+ Util.setFloat(caption.getElement(), "");
+ containerDIV.insertBefore(caption.getElement(), widgetDIV);
+ }
+
+ adopt(caption);
+ }
+
+ }
+
+ @Override
+ public boolean remove(Widget child) {
+ // Validate
+ if (child != caption && child != widget) {
+ return false;
+ }
+
+ // Orphan
+ orphan(child);
+
+ // Physical && Logical Detach
+ if (child == caption) {
+ containerDIV.removeChild(child.getElement());
+ caption = null;
+ } else {
+ widgetDIV.removeChild(child.getElement());
+ widget = null;
+ }
+
+ return true;
+ }
+
+ public Iterator<Widget> iterator() {
+ return new ChildComponentContainerIterator<Widget>();
+ }
+
+ public class ChildComponentContainerIterator<T> implements Iterator<Widget> {
+ private int id = 0;
+
+ public boolean hasNext() {
+ return (id < size());
+ }
+
+ public Widget next() {
+ Widget w = get(id);
+ id++;
+ return w;
+ }
+
+ private Widget get(int i) {
+ if (i == 0) {
+ if (widget != null) {
+ return widget;
+ } else if (caption != null) {
+ return caption;
+ } else {
+ throw new NoSuchElementException();
+ }
+ } else if (i == 1) {
+ if (widget != null && caption != null) {
+ return caption;
+ } else {
+ throw new NoSuchElementException();
+ }
+ } else {
+ throw new NoSuchElementException();
+ }
+ }
+
+ public void remove() {
+ int toRemove = id - 1;
+ if (toRemove == 0) {
+ if (widget != null) {
+ ChildComponentContainer.this.remove(widget);
+ } else if (caption != null) {
+ ChildComponentContainer.this.remove(caption);
+ } else {
+ throw new IllegalStateException();
+ }
+
+ } else if (toRemove == 1) {
+ if (widget != null && caption != null) {
+ ChildComponentContainer.this.remove(caption);
+ } else {
+ throw new IllegalStateException();
+ }
+ } else {
+ throw new IllegalStateException();
+ }
+
+ id--;
+ }
+ }
+
+ public int size() {
+ if (widget != null) {
+ if (caption != null) {
+ return 2;
+ } else {
+ return 1;
+ }
+ } else {
+ if (caption != null) {
+ return 1;
+ } else {
+ return 0;
+ }
+ }
+ }
+
+ public Widget getWidget() {
+ return widget;
+ }
+
+ /**
+ * Return true if the size of the widget has been specified in the selected
+ * orientation.
+ *
+ * @return
+ */
+ public boolean widgetHasSizeSpecified(int orientation) {
+ String size;
+ if (orientation == CellBasedLayout.ORIENTATION_HORIZONTAL) {
+ size = widget.getElement().getStyle().getProperty("width");
+ } else {
+ size = widget.getElement().getStyle().getProperty("height");
+ }
+ return (size != null && !size.equals(""));
+ }
+
+ public boolean isComponentRelativeSized(int orientation) {
+ if (relativeSize == null) {
+ return false;
+ }
+ if (orientation == CellBasedLayout.ORIENTATION_HORIZONTAL) {
+ return relativeSize.getWidth() >= 0;
+ } else {
+ return relativeSize.getHeight() >= 0;
+ }
+ }
+
+ public void setRelativeSize(FloatSize relativeSize) {
+ this.relativeSize = relativeSize;
+ }
+
+ public Size getContSize() {
+ return contSize;
+ }
+
+ public void clearAlignments() {
+ alignmentLeftOffsetForCaption = 0;
+ alignmentLeftOffsetForWidget = 0;
+ alignmentTopOffset = 0;
+ applyAlignments();
+
+ }
+
+ public void setExpandRatio(int expandRatio) {
+ this.expandRatio = (expandRatio / 1000.0f);
+ }
+
+ public int expand(int orientation, int spaceForExpansion) {
+ int expansionAmount = (int) ((double) spaceForExpansion * expandRatio);
+
+ if (orientation == CellBasedLayout.ORIENTATION_HORIZONTAL) {
+ // HORIZONTAL
+ containerExpansion.setWidth(expansionAmount);
+ } else {
+ // VERTICAL
+ containerExpansion.setHeight(expansionAmount);
+ }
+
+ return expansionAmount;
+ }
+
+ public void expandExtra(int orientation, int extra) {
+ if (orientation == CellBasedLayout.ORIENTATION_HORIZONTAL) {
+ // HORIZONTAL
+ containerExpansion.setWidth(containerExpansion.getWidth() + extra);
+ } else {
+ // VERTICAL
+ containerExpansion
+ .setHeight(containerExpansion.getHeight() + extra);
+ }
+
+ }
+
+ public void setContainerSize(int widgetAndCaptionWidth,
+ int widgetAndCaptionHeight) {
+
+ int containerWidth = widgetAndCaptionWidth;
+ containerWidth += containerExpansion.getWidth();
+
+ int containerHeight = widgetAndCaptionHeight;
+ containerHeight += containerExpansion.getHeight();
+
+ // ApplicationConnection.getConsole().log(
+ // "Setting container size for " + Util.getSimpleName(widget)
+ // + " to " + containerWidth + "," + containerHeight);
+
+ if (containerWidth < 0) {
+ ApplicationConnection.getConsole().log(
+ "containerWidth should never be negative: "
+ + containerWidth);
+ containerWidth = 0;
+ }
+ if (containerHeight < 0) {
+ ApplicationConnection.getConsole().log(
+ "containerHeight should never be negative: "
+ + containerHeight);
+ containerHeight = 0;
+ }
+
+ contSize.setWidth(containerWidth);
+ contSize.setHeight(containerHeight);
+
+ updateContainerDOMSize();
+ }
+
+ public void updateContainerDOMSize() {
+ int width = contSize.getWidth();
+ int height = contSize.getHeight() - alignmentTopOffset;
+ if (width < 0) {
+ width = 0;
+ }
+ if (height < 0) {
+ height = 0;
+ }
+
+ setWidth(width + "px");
+ setHeight(height + "px");
+
+ // Also update caption max width
+ if (caption != null) {
+ if (caption.shouldBePlacedAfterComponent()) {
+ caption.setMaxWidth(captionWidth);
+ } else {
+ caption.setMaxWidth(width);
+ }
+ captionWidth = caption.getRenderedWidth();
+
+ // Remove initial height
+ caption.setHeight("");
+ }
+
+ }
+
+}
diff --git a/src/com/vaadin/terminal/gwt/client/ui/layout/Margins.java b/src/com/vaadin/terminal/gwt/client/ui/layout/Margins.java
new file mode 100644
index 0000000000..5010744ee5
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/layout/Margins.java
@@ -0,0 +1,83 @@
+package com.vaadin.terminal.gwt.client.ui.layout;
+
+public class Margins {
+
+ private int marginTop;
+ private int marginBottom;
+ private int marginLeft;
+ private int marginRight;
+
+ private int horizontal = 0;
+ private int vertical = 0;
+
+ public Margins(int marginTop, int marginBottom, int marginLeft,
+ int marginRight) {
+ super();
+ this.marginTop = marginTop;
+ this.marginBottom = marginBottom;
+ this.marginLeft = marginLeft;
+ this.marginRight = marginRight;
+
+ updateHorizontal();
+ updateVertical();
+ }
+
+ public int getMarginTop() {
+ return marginTop;
+ }
+
+ public int getMarginBottom() {
+ return marginBottom;
+ }
+
+ public int getMarginLeft() {
+ return marginLeft;
+ }
+
+ public int getMarginRight() {
+ return marginRight;
+ }
+
+ public int getHorizontal() {
+ return horizontal;
+ }
+
+ public int getVertical() {
+ return vertical;
+ }
+
+ public void setMarginTop(int marginTop) {
+ this.marginTop = marginTop;
+ updateVertical();
+ }
+
+ public void setMarginBottom(int marginBottom) {
+ this.marginBottom = marginBottom;
+ updateVertical();
+ }
+
+ public void setMarginLeft(int marginLeft) {
+ this.marginLeft = marginLeft;
+ updateHorizontal();
+ }
+
+ public void setMarginRight(int marginRight) {
+ this.marginRight = marginRight;
+ updateHorizontal();
+ }
+
+ private void updateVertical() {
+ vertical = marginTop + marginBottom;
+ }
+
+ private void updateHorizontal() {
+ horizontal = marginLeft + marginRight;
+ }
+
+ @Override
+ public String toString() {
+ return "Margins [marginLeft=" + marginLeft + ",marginTop=" + marginTop
+ + ",marginRight=" + marginRight + ",marginBottom="
+ + marginBottom + "]";
+ }
+}
diff --git a/src/com/vaadin/terminal/gwt/client/ui/richtextarea/IRichTextArea.java b/src/com/vaadin/terminal/gwt/client/ui/richtextarea/IRichTextArea.java
new file mode 100644
index 0000000000..b6a3c85238
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/richtextarea/IRichTextArea.java
@@ -0,0 +1,251 @@
+/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui.richtextarea;
+
+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.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.KeyboardListener;
+import com.google.gwt.user.client.ui.RichTextArea;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.BrowserInfo;
+import com.vaadin.terminal.gwt.client.Paintable;
+import com.vaadin.terminal.gwt.client.UIDL;
+import com.vaadin.terminal.gwt.client.Util;
+import com.vaadin.terminal.gwt.client.ui.Field;
+
+/**
+ * This class implements a basic client side rich text editor component.
+ *
+ * @author IT Mill Ltd.
+ *
+ */
+public class IRichTextArea extends Composite implements Paintable, Field,
+ ChangeListener, FocusListener, KeyboardListener {
+
+ /**
+ * 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 IRichTextToolbar formatter = new IRichTextToolbar(rta);
+
+ private HTML html = new HTML();
+
+ private final FlowPanel fp = new FlowPanel();
+
+ private boolean enabled = true;
+
+ private int extraHorizontalPixels = -1;
+ private int extraVerticalPixels = -1;
+
+ private int maxLength = -1;
+
+ private int toolbarNaturalWidth = 500;
+
+ 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(final UIDL uidl, ApplicationConnection client) {
+ this.client = client;
+ id = uidl.getId();
+
+ if (uidl.hasVariable("text")) {
+ if (BrowserInfo.get().isIE()) {
+ // rta is rather buggy in IE (as pretty much everything is)
+ // it needs some "shaking" not to fall into uneditable state
+ // see #2374
+ rta.getBasicFormatter().toggleBold();
+ rta.getBasicFormatter().toggleBold();
+ }
+ rta.setHTML(uidl.getStringVariable("text"));
+
+ }
+ setEnabled(!uidl.getBooleanAttribute("disabled"));
+
+ if (client.updateComponent(this, uidl, true)) {
+ return;
+ }
+
+ immediate = uidl.getBooleanAttribute("immediate");
+ int newMaxLength = uidl.hasAttribute("maxLength") ? uidl
+ .getIntAttribute("maxLength") : -1;
+ if (newMaxLength >= 0) {
+ if (maxLength == -1) {
+ rta.addKeyboardListener(this);
+ }
+ maxLength = newMaxLength;
+ } else if (maxLength != -1) {
+ getElement().setAttribute("maxlength", "");
+ maxLength = -1;
+ rta.removeKeyboardListener(this);
+ }
+ }
+
+ 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);
+
+ }
+
+ /**
+ * @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.
+ */
+ 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);
+ }
+
+ @Override
+ public void setHeight(String height) {
+ if (height.endsWith("px")) {
+ int h = Integer.parseInt(height.substring(0, height.length() - 2));
+ h -= getExtraVerticalPixels();
+ if (h < 0) {
+ h = 0;
+ }
+
+ super.setHeight(h + "px");
+ } else {
+ super.setHeight(height);
+ }
+
+ if (height == null || height.equals("")) {
+ rta.setHeight("");
+ } else {
+ int editorHeight = getOffsetHeight() - getExtraVerticalPixels()
+ - formatter.getOffsetHeight();
+ rta.setHeight(editorHeight + "px");
+ }
+ }
+
+ @Override
+ public void setWidth(String width) {
+ if (width.endsWith("px")) {
+ int w = Integer.parseInt(width.substring(0, width.length() - 2));
+ w -= getExtraHorizontalPixels();
+ if (w < 0) {
+ w = 0;
+ }
+
+ super.setWidth(w + "px");
+ } else if (width.equals("")) {
+ /*
+ * IE cannot calculate the width of the 100% iframe correctly if
+ * there is no width specified for the parent. In this case we would
+ * use the toolbar but IE cannot calculate the width of that one
+ * correctly either in all cases. So we end up using a default width
+ * for a RichTextArea with no width definition in all browsers (for
+ * compatibility).
+ */
+
+ super.setWidth(toolbarNaturalWidth + "px");
+ } else {
+ super.setWidth(width);
+ }
+ }
+
+ public void onKeyDown(Widget sender, char keyCode, int modifiers) {
+ // NOP
+ }
+
+ public void onKeyPress(Widget sender, char keyCode, int modifiers) {
+ if (maxLength >= 0) {
+ DeferredCommand.addCommand(new Command() {
+ public void execute() {
+ if (rta.getHTML().length() > maxLength) {
+ rta.setHTML(rta.getHTML().substring(0, maxLength));
+ }
+ }
+ });
+ }
+ }
+
+ public void onKeyUp(Widget sender, char keyCode, int modifiers) {
+ // NOP
+ }
+
+}
diff --git a/src/com/vaadin/terminal/gwt/client/ui/richtextarea/IRichTextToolbar$Strings.properties b/src/com/vaadin/terminal/gwt/client/ui/richtextarea/IRichTextToolbar$Strings.properties
new file mode 100644
index 0000000000..363b704584
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/richtextarea/IRichTextToolbar$Strings.properties
@@ -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/vaadin/terminal/gwt/client/ui/richtextarea/IRichTextToolbar.java b/src/com/vaadin/terminal/gwt/client/ui/richtextarea/IRichTextToolbar.java
new file mode 100644
index 0000000000..433be77464
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/richtextarea/IRichTextToolbar.java
@@ -0,0 +1,474 @@
+/*
+ * 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.vaadin.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.FlowPanel;
+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.Widget;
+
+/**
+ * A modified version of 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 IRichTextToolbar 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 {
+
+ @ImageBundle.Resource("bold.gif")
+ AbstractImagePrototype bold();
+
+ @ImageBundle.Resource("createLink.gif")
+ AbstractImagePrototype createLink();
+
+ @ImageBundle.Resource("hr.gif")
+ AbstractImagePrototype hr();
+
+ @ImageBundle.Resource("indent.gif")
+ AbstractImagePrototype indent();
+
+ @ImageBundle.Resource("insertImage.gif")
+ AbstractImagePrototype insertImage();
+
+ @ImageBundle.Resource("italic.gif")
+ AbstractImagePrototype italic();
+
+ @ImageBundle.Resource("justifyCenter.gif")
+ AbstractImagePrototype justifyCenter();
+
+ @ImageBundle.Resource("justifyLeft.gif")
+ AbstractImagePrototype justifyLeft();
+
+ @ImageBundle.Resource("justifyRight.gif")
+ AbstractImagePrototype justifyRight();
+
+ @ImageBundle.Resource("ol.gif")
+ AbstractImagePrototype ol();
+
+ @ImageBundle.Resource("outdent.gif")
+ AbstractImagePrototype outdent();
+
+ @ImageBundle.Resource("removeFormat.gif")
+ AbstractImagePrototype removeFormat();
+
+ @ImageBundle.Resource("removeLink.gif")
+ AbstractImagePrototype removeLink();
+
+ @ImageBundle.Resource("strikeThrough.gif")
+ AbstractImagePrototype strikeThrough();
+
+ @ImageBundle.Resource("subscript.gif")
+ AbstractImagePrototype subscript();
+
+ @ImageBundle.Resource("superscript.gif")
+ AbstractImagePrototype superscript();
+
+ @ImageBundle.Resource("ul.gif")
+ AbstractImagePrototype ul();
+
+ @ImageBundle.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 FlowPanel outer = new FlowPanel();
+ private final FlowPanel topPanel = new FlowPanel();
+ private final FlowPanel bottomPanel = new FlowPanel();
+ 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 IRichTextToolbar(RichTextArea richText) {
+ this.richText = richText;
+ basic = richText.getBasicFormatter();
+ extended = richText.getExtendedFormatter();
+
+ outer.add(topPanel);
+ outer.add(bottomPanel);
+ topPanel.setWidth("100%");
+ topPanel.setHeight("20px");
+ topPanel.getElement().getStyle().setProperty("overflow", "hidden");
+ 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/vaadin/terminal/gwt/client/ui/richtextarea/backColors.gif b/src/com/vaadin/terminal/gwt/client/ui/richtextarea/backColors.gif
new file mode 100644
index 0000000000..ddfc1cea2c
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/richtextarea/backColors.gif
Binary files differ
diff --git a/src/com/vaadin/terminal/gwt/client/ui/richtextarea/bold.gif b/src/com/vaadin/terminal/gwt/client/ui/richtextarea/bold.gif
new file mode 100644
index 0000000000..249e5afc04
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/richtextarea/bold.gif
Binary files differ
diff --git a/src/com/vaadin/terminal/gwt/client/ui/richtextarea/createLink.gif b/src/com/vaadin/terminal/gwt/client/ui/richtextarea/createLink.gif
new file mode 100644
index 0000000000..3ab9e599f8
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/richtextarea/createLink.gif
Binary files differ
diff --git a/src/com/vaadin/terminal/gwt/client/ui/richtextarea/fontSizes.gif b/src/com/vaadin/terminal/gwt/client/ui/richtextarea/fontSizes.gif
new file mode 100644
index 0000000000..c2f4c8cb21
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/richtextarea/fontSizes.gif
Binary files differ
diff --git a/src/com/vaadin/terminal/gwt/client/ui/richtextarea/fonts.gif b/src/com/vaadin/terminal/gwt/client/ui/richtextarea/fonts.gif
new file mode 100644
index 0000000000..1629cabb78
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/richtextarea/fonts.gif
Binary files differ
diff --git a/src/com/vaadin/terminal/gwt/client/ui/richtextarea/foreColors.gif b/src/com/vaadin/terminal/gwt/client/ui/richtextarea/foreColors.gif
new file mode 100644
index 0000000000..2bb89ef189
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/richtextarea/foreColors.gif
Binary files differ
diff --git a/src/com/vaadin/terminal/gwt/client/ui/richtextarea/gwtLogo.png b/src/com/vaadin/terminal/gwt/client/ui/richtextarea/gwtLogo.png
new file mode 100644
index 0000000000..80728186d8
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/richtextarea/gwtLogo.png
Binary files differ
diff --git a/src/com/vaadin/terminal/gwt/client/ui/richtextarea/hr.gif b/src/com/vaadin/terminal/gwt/client/ui/richtextarea/hr.gif
new file mode 100644
index 0000000000..3fb1607e67
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/richtextarea/hr.gif
Binary files differ
diff --git a/src/com/vaadin/terminal/gwt/client/ui/richtextarea/indent.gif b/src/com/vaadin/terminal/gwt/client/ui/richtextarea/indent.gif
new file mode 100644
index 0000000000..8b837f0fd9
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/richtextarea/indent.gif
Binary files differ
diff --git a/src/com/vaadin/terminal/gwt/client/ui/richtextarea/insertImage.gif b/src/com/vaadin/terminal/gwt/client/ui/richtextarea/insertImage.gif
new file mode 100644
index 0000000000..db61c9a3de
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/richtextarea/insertImage.gif
Binary files differ
diff --git a/src/com/vaadin/terminal/gwt/client/ui/richtextarea/italic.gif b/src/com/vaadin/terminal/gwt/client/ui/richtextarea/italic.gif
new file mode 100644
index 0000000000..2b0a5a0a06
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/richtextarea/italic.gif
Binary files differ
diff --git a/src/com/vaadin/terminal/gwt/client/ui/richtextarea/justifyCenter.gif b/src/com/vaadin/terminal/gwt/client/ui/richtextarea/justifyCenter.gif
new file mode 100644
index 0000000000..7d22640af0
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/richtextarea/justifyCenter.gif
Binary files differ
diff --git a/src/com/vaadin/terminal/gwt/client/ui/richtextarea/justifyLeft.gif b/src/com/vaadin/terminal/gwt/client/ui/richtextarea/justifyLeft.gif
new file mode 100644
index 0000000000..3c0f350122
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/richtextarea/justifyLeft.gif
Binary files differ
diff --git a/src/com/vaadin/terminal/gwt/client/ui/richtextarea/justifyRight.gif b/src/com/vaadin/terminal/gwt/client/ui/richtextarea/justifyRight.gif
new file mode 100644
index 0000000000..99ee25836f
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/richtextarea/justifyRight.gif
Binary files differ
diff --git a/src/com/vaadin/terminal/gwt/client/ui/richtextarea/ol.gif b/src/com/vaadin/terminal/gwt/client/ui/richtextarea/ol.gif
new file mode 100644
index 0000000000..833bb40667
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/richtextarea/ol.gif
Binary files differ
diff --git a/src/com/vaadin/terminal/gwt/client/ui/richtextarea/outdent.gif b/src/com/vaadin/terminal/gwt/client/ui/richtextarea/outdent.gif
new file mode 100644
index 0000000000..be8662411a
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/richtextarea/outdent.gif
Binary files differ
diff --git a/src/com/vaadin/terminal/gwt/client/ui/richtextarea/removeFormat.gif b/src/com/vaadin/terminal/gwt/client/ui/richtextarea/removeFormat.gif
new file mode 100644
index 0000000000..a4339c059e
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/richtextarea/removeFormat.gif
Binary files differ
diff --git a/src/com/vaadin/terminal/gwt/client/ui/richtextarea/removeLink.gif b/src/com/vaadin/terminal/gwt/client/ui/richtextarea/removeLink.gif
new file mode 100644
index 0000000000..522ab4b29e
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/richtextarea/removeLink.gif
Binary files differ
diff --git a/src/com/vaadin/terminal/gwt/client/ui/richtextarea/strikeThrough.gif b/src/com/vaadin/terminal/gwt/client/ui/richtextarea/strikeThrough.gif
new file mode 100644
index 0000000000..6b174c884e
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/richtextarea/strikeThrough.gif
Binary files differ
diff --git a/src/com/vaadin/terminal/gwt/client/ui/richtextarea/subscript.gif b/src/com/vaadin/terminal/gwt/client/ui/richtextarea/subscript.gif
new file mode 100644
index 0000000000..04bba05b8b
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/richtextarea/subscript.gif
Binary files differ
diff --git a/src/com/vaadin/terminal/gwt/client/ui/richtextarea/superscript.gif b/src/com/vaadin/terminal/gwt/client/ui/richtextarea/superscript.gif
new file mode 100644
index 0000000000..ac478eeb7e
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/richtextarea/superscript.gif
Binary files differ
diff --git a/src/com/vaadin/terminal/gwt/client/ui/richtextarea/ul.gif b/src/com/vaadin/terminal/gwt/client/ui/richtextarea/ul.gif
new file mode 100644
index 0000000000..01380dbb83
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/richtextarea/ul.gif
Binary files differ
diff --git a/src/com/vaadin/terminal/gwt/client/ui/richtextarea/underline.gif b/src/com/vaadin/terminal/gwt/client/ui/richtextarea/underline.gif
new file mode 100644
index 0000000000..82bae11f59
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/richtextarea/underline.gif
Binary files differ
diff --git a/src/com/vaadin/terminal/gwt/server/AbstractApplicationServlet.java b/src/com/vaadin/terminal/gwt/server/AbstractApplicationServlet.java
new file mode 100644
index 0000000000..87f966a65b
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/server/AbstractApplicationServlet.java
@@ -0,0 +1,1797 @@
+package com.vaadin.terminal.gwt.server;
+
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.io.Serializable;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.security.GeneralSecurityException;
+import java.util.Collection;
+import java.util.Date;
+import java.util.Enumeration;
+import java.util.Iterator;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Properties;
+
+import javax.servlet.ServletContext;
+import javax.servlet.ServletException;
+import javax.servlet.ServletOutputStream;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+
+import org.xml.sax.SAXException;
+
+import com.itmill.toolkit.external.org.apache.commons.fileupload.servlet.ServletFileUpload;
+import com.vaadin.Application;
+import com.vaadin.Application.SystemMessages;
+import com.vaadin.service.FileTypeResolver;
+import com.vaadin.terminal.DownloadStream;
+import com.vaadin.terminal.ParameterHandler;
+import com.vaadin.terminal.Terminal;
+import com.vaadin.terminal.ThemeResource;
+import com.vaadin.terminal.URIHandler;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.ui.Window;
+
+/**
+ * Abstract implementation of the ApplicationServlet which handles all
+ * communication between the client and the server.
+ *
+ * It is possible to extend this class to provide own functionality but in most
+ * cases this is unnecessary.
+ *
+ *
+ * @author IT Mill Ltd.
+ * @version
+ * @VERSION@
+ * @since 6.0
+ */
+
+@SuppressWarnings("serial")
+public abstract class AbstractApplicationServlet extends HttpServlet {
+ /**
+ * Version number of this release. For example "5.0.0".
+ */
+ public static final String VERSION;
+ /**
+ * Major version number. For example 5 in 5.1.0.
+ */
+ public static final int VERSION_MAJOR;
+
+ /**
+ * Minor version number. For example 1 in 5.1.0.
+ */
+ public static final int VERSION_MINOR;
+
+ /**
+ * Builds number. For example 0-custom_tag in 5.0.0-custom_tag.
+ */
+ public static final String VERSION_BUILD;
+
+ /* Initialize version numbers from string replaced by build-script. */
+ static {
+ if ("@VERSION@".equals("@" + "VERSION" + "@")) {
+ VERSION = "5.9.9-INTERNAL-NONVERSIONED-DEBUG-BUILD";
+ } else {
+ VERSION = "@VERSION@";
+ }
+ final String[] digits = VERSION.split("\\.");
+ VERSION_MAJOR = Integer.parseInt(digits[0]);
+ VERSION_MINOR = Integer.parseInt(digits[1]);
+ VERSION_BUILD = digits[2];
+ }
+
+ /**
+ * If the attribute is present in the request, a html fragment will be
+ * written instead of a whole page.
+ */
+ public static final String REQUEST_FRAGMENT = ApplicationServlet.class
+ .getName()
+ + ".fragment";
+ /**
+ * This request attribute forces widgetset used; e.g for portlets that can
+ * not have different widgetsets.
+ */
+ public static final String REQUEST_WIDGETSET = ApplicationServlet.class
+ .getName()
+ + ".widgetset";
+ /**
+ * This request attribute is used to add styles to the main element. E.g
+ * "height:500px" generates a style="height:500px" to the main element,
+ * useful from some embedding situations (e.g portlet include.)
+ */
+ public static final String REQUEST_APPSTYLE = ApplicationServlet.class
+ .getName()
+ + ".style";
+
+ private Properties applicationProperties;
+
+ private static final String NOT_PRODUCTION_MODE_INFO = "=================================================================\nIT Mill Toolkit is running in DEBUG MODE.\nAdd productionMode=true to web.xml to disable debug features.\nTo show debug window, add ?debug to your application URL.\n=================================================================";
+
+ private boolean productionMode = false;
+
+ private static final String URL_PARAMETER_RESTART_APPLICATION = "restartApplication";
+ private static final String URL_PARAMETER_CLOSE_APPLICATION = "closeApplication";
+ private static final String URL_PARAMETER_REPAINT_ALL = "repaintAll";
+ protected static final String URL_PARAMETER_THEME = "theme";
+
+ private static final String SERVLET_PARAMETER_DEBUG = "Debug";
+ private static final String SERVLET_PARAMETER_PRODUCTION_MODE = "productionMode";
+
+ // Configurable parameter names
+ private static final String PARAMETER_ITMILL_RESOURCES = "Resources";
+
+ private static final int DEFAULT_BUFFER_SIZE = 32 * 1024;
+
+ private static final int MAX_BUFFER_SIZE = 64 * 1024;
+
+ private static final String RESOURCE_URI = "/RES/";
+
+ private static final String AJAX_UIDL_URI = "/UIDL";
+
+ static final String THEME_DIRECTORY_PATH = "ITMILL/themes/";
+
+ private static final int DEFAULT_THEME_CACHETIME = 1000 * 60 * 60 * 24;
+
+ static final String WIDGETSET_DIRECTORY_PATH = "ITMILL/widgetsets/";
+
+ // Name of the default widget set, used if not specified in web.xml
+ private static final String DEFAULT_WIDGETSET = "com.vaadin.terminal.gwt.DefaultWidgetSet";
+
+ // Widget set parameter name
+ private static final String PARAMETER_WIDGETSET = "widgetset";
+
+ private static final String ERROR_NO_WINDOW_FOUND = "Application did not give any window, did you remember to setMainWindow()?";
+
+ private String resourcePath = null;
+
+ /**
+ * Called by the servlet container to indicate to a servlet that the servlet
+ * is being placed into service.
+ *
+ * @param servletConfig
+ * the object containing the servlet's configuration and
+ * initialization parameters
+ * @throws javax.servlet.ServletException
+ * if an exception has occurred that interferes with the
+ * servlet's normal operation.
+ */
+ @Override
+ public void init(javax.servlet.ServletConfig servletConfig)
+ throws javax.servlet.ServletException {
+ super.init(servletConfig);
+
+ // Stores the application parameters into Properties object
+ applicationProperties = new Properties();
+ for (final Enumeration e = servletConfig.getInitParameterNames(); e
+ .hasMoreElements();) {
+ final String name = (String) e.nextElement();
+ applicationProperties.setProperty(name, servletConfig
+ .getInitParameter(name));
+ }
+
+ // Overrides with server.xml parameters
+ final ServletContext context = servletConfig.getServletContext();
+ for (final Enumeration e = context.getInitParameterNames(); e
+ .hasMoreElements();) {
+ final String name = (String) e.nextElement();
+ applicationProperties.setProperty(name, context
+ .getInitParameter(name));
+ }
+
+ checkProductionMode();
+ }
+
+ private void checkProductionMode() {
+ // Check if the application is in production mode.
+ // We are in production mode if Debug=false or productionMode=true
+ if (getApplicationOrSystemProperty(SERVLET_PARAMETER_DEBUG, "true")
+ .equals("false")) {
+ // "Debug=true" is the old way and should no longer be used
+ productionMode = true;
+ } else if (getApplicationOrSystemProperty(
+ SERVLET_PARAMETER_PRODUCTION_MODE, "false").equals("true")) {
+ // "productionMode=true" is the real way to do it
+ productionMode = true;
+ }
+
+ if (!productionMode) {
+ /* Print an information/warning message about running in debug mode */
+ System.err.println(NOT_PRODUCTION_MODE_INFO);
+ }
+
+ }
+
+ /**
+ * Gets an application property value.
+ *
+ * @param parameterName
+ * the Name or the parameter.
+ * @return String value or null if not found
+ */
+ protected String getApplicationProperty(String parameterName) {
+
+ String val = applicationProperties.getProperty(parameterName);
+ if (val != null) {
+ return val;
+ }
+
+ // Try lower case application properties for backward compatibility with
+ // 3.0.2 and earlier
+ val = applicationProperties.getProperty(parameterName.toLowerCase());
+
+ return val;
+ }
+
+ /**
+ * Gets an system property value.
+ *
+ * @param parameterName
+ * the Name or the parameter.
+ * @return String value or null if not found
+ */
+ protected String getSystemProperty(String parameterName) {
+ String val = null;
+
+ String pkgName;
+ final Package pkg = getClass().getPackage();
+ if (pkg != null) {
+ pkgName = pkg.getName();
+ } else {
+ final String className = getClass().getName();
+ pkgName = new String(className.toCharArray(), 0, className
+ .lastIndexOf('.'));
+ }
+ val = System.getProperty(pkgName + "." + parameterName);
+ if (val != null) {
+ return val;
+ }
+
+ // Try lowercased system properties
+ val = System.getProperty(pkgName + "." + parameterName.toLowerCase());
+ return val;
+ }
+
+ /**
+ * Gets an application or system property value.
+ *
+ * @param parameterName
+ * the Name or the parameter.
+ * @param defaultValue
+ * the Default to be used.
+ * @return String value or default if not found
+ */
+ private String getApplicationOrSystemProperty(String parameterName,
+ String defaultValue) {
+
+ String val = null;
+
+ // Try application properties
+ val = getApplicationProperty(parameterName);
+ if (val != null) {
+ return val;
+ }
+
+ // Try system properties
+ val = getSystemProperty(parameterName);
+ if (val != null) {
+ return val;
+ }
+
+ return defaultValue;
+ }
+
+ /**
+ * Returns true if the servlet is running in production mode. Production
+ * mode disables all debug facilities.
+ *
+ * @return true if in production mode, false if in debug mode
+ */
+ public boolean isProductionMode() {
+ return productionMode;
+ }
+
+ /**
+ * Receives standard HTTP requests from the public service method and
+ * dispatches them.
+ *
+ * @param request
+ * the object that contains the request the client made of the
+ * servlet.
+ * @param response
+ * the object that contains the response the servlet returns to
+ * the client.
+ * @throws ServletException
+ * if an input or output error occurs while the servlet is
+ * handling the TRACE request.
+ * @throws IOException
+ * if the request for the TRACE cannot be handled.
+ */
+ @Override
+ protected void service(HttpServletRequest request,
+ HttpServletResponse response) throws ServletException, IOException {
+
+ // check if we should serve static files (widgetsets, themes)
+ if (serveStaticResources(request, response)) {
+ return;
+ }
+
+ Application application = null;
+ RequestType requestType = getRequestType(request);
+
+ try {
+ // Find out which application this request is related to
+ application = findApplicationInstance(request, requestType);
+ if (application == null) {
+ return;
+ }
+
+ /*
+ * Get or create a WebApplicationContext and an ApplicationManager
+ * for the session
+ */
+ WebApplicationContext webApplicationContext = WebApplicationContext
+ .getApplicationContext(request.getSession());
+ CommunicationManager applicationManager = webApplicationContext
+ .getApplicationManager(application, this);
+
+ /* Update browser information from the request */
+ webApplicationContext.getBrowser().updateBrowserProperties(request);
+
+ /*
+ * Transaction starts. Call transaction listeners. Transaction end
+ * is called in the finally block below.
+ */
+ webApplicationContext.startTransaction(application, request);
+
+ // TODO Add screen height and width to the GWT client
+
+ /* Handle the request */
+ if (requestType == RequestType.FILE_UPLOAD) {
+ applicationManager.handleFileUpload(request, response);
+ return;
+ } else if (requestType == RequestType.UIDL) {
+ // Handles AJAX UIDL requests
+ applicationManager.handleUidlRequest(request, response, this);
+ return;
+ }
+
+ // Removes application if it has stopped
+ if (!application.isRunning()) {
+ // FIXME How can this be reached?
+ endApplication(request, response, application);
+ return;
+ }
+
+ // Finds the window within the application
+ Window window = getApplicationWindow(request, application);
+ if (window == null) {
+ throw new ServletException(ERROR_NO_WINDOW_FOUND);
+ }
+
+ // Sets terminal type for the window, if not already set
+ if (window.getTerminal() == null) {
+ window.setTerminal(webApplicationContext.getBrowser());
+ }
+
+ // Handle parameters
+ final Map parameters = request.getParameterMap();
+ if (window != null && parameters != null) {
+ window.handleParameters(parameters);
+ }
+
+ /*
+ * Call the URI handlers and if this turns out to be a download
+ * request, send the file to the client
+ */
+ if (handleURI(applicationManager, window, request, response)) {
+ return;
+ }
+
+ String themeName = getThemeForWindow(request, window);
+
+ // Handles theme resource requests
+ if (handleResourceRequest(request, response, themeName)) {
+ return;
+ }
+
+ // Send initial AJAX page that kickstarts Toolkit application
+ writeAjaxPage(request, response, window, themeName, application);
+
+ } catch (final SessionExpired e) {
+ // Session has expired, notify user
+ handleServiceSessionExpired(request, response);
+ } catch (final GeneralSecurityException e) {
+ handleServiceSecurityException(request, response);
+ } catch (final Throwable e) {
+ handleServiceException(request, response, application, e);
+ } finally {
+ // Notifies transaction end
+ if (application != null) {
+ ((WebApplicationContext) application.getContext())
+ .endTransaction(application, request);
+ }
+
+ // Work-around for GAE session problem. Explicitly touch session so
+ // it is re-serialized.
+ HttpSession session = request.getSession(false);
+ if (session != null) {
+ session.setAttribute("sessionUpdated", new Date().getTime());
+ }
+ }
+ }
+
+ protected ClassLoader getClassLoader() throws ServletException {
+ // Gets custom class loader
+ final String classLoaderName = getApplicationOrSystemProperty(
+ "ClassLoader", null);
+ ClassLoader classLoader;
+ if (classLoaderName == null) {
+ classLoader = getClass().getClassLoader();
+ } else {
+ try {
+ final Class<?> classLoaderClass = getClass().getClassLoader()
+ .loadClass(classLoaderName);
+ final Constructor<?> c = classLoaderClass
+ .getConstructor(new Class[] { ClassLoader.class });
+ classLoader = (ClassLoader) c
+ .newInstance(new Object[] { getClass().getClassLoader() });
+ } catch (final Exception e) {
+ throw new ServletException(
+ "Could not find specified class loader: "
+ + classLoaderName, e);
+ }
+ }
+ return classLoader;
+ }
+
+ /**
+ * Send notification to client's application. Used to notify client of
+ * critical errors and session expiration due to long inactivity. Server has
+ * no knowledge of what application client refers to.
+ *
+ * @param request
+ * the HTTP request instance.
+ * @param response
+ * the HTTP response to write to.
+ * @param caption
+ * for the notification
+ * @param message
+ * for the notification
+ * @param url
+ * url to load after message, null for current page
+ * @throws IOException
+ * if the writing failed due to input/output error.
+ */
+ void criticalNotification(HttpServletRequest request,
+ HttpServletResponse response, String caption, String message,
+ String url) throws IOException {
+
+ // clients JS app is still running, but server application either
+ // no longer exists or it might fail to perform reasonably.
+ // send a notification to client's application and link how
+ // to "restart" application.
+
+ if (caption != null) {
+ caption = "\"" + caption + "\"";
+ }
+ if (message != null) {
+ message = "\"" + message + "\"";
+ }
+ if (url != null) {
+ url = "\"" + url + "\"";
+ }
+
+ // Set the response type
+ response.setContentType("application/json; charset=UTF-8");
+ final ServletOutputStream out = response.getOutputStream();
+ final PrintWriter outWriter = new PrintWriter(new BufferedWriter(
+ new OutputStreamWriter(out, "UTF-8")));
+ outWriter.print("for(;;);[{\"changes\":[], \"meta\" : {"
+ + "\"appError\": {" + "\"caption\":" + caption + ","
+ + "\"message\" : " + message + "," + "\"url\" : " + url
+ + "}}, \"resources\": {}, \"locales\":[]}]");
+ outWriter.flush();
+ outWriter.close();
+ out.flush();
+ }
+
+ /**
+ * Returns the application instance to be used for the request. If an
+ * existing instance is not found a new one is created or null is returned
+ * to indicate that the application is not available.
+ *
+ * @param request
+ * @param requestType
+ * @return
+ * @throws MalformedURLException
+ * @throws SAXException
+ * @throws IllegalAccessException
+ * @throws InstantiationException
+ * @throws ServletException
+ * @throws SessionExpired
+ */
+ private Application findApplicationInstance(HttpServletRequest request,
+ RequestType requestType) throws MalformedURLException,
+ SAXException, IllegalAccessException, InstantiationException,
+ ServletException, SessionExpired {
+
+ final boolean restartApplication = (request
+ .getParameter(URL_PARAMETER_RESTART_APPLICATION) != null);
+ final boolean closeApplication = (request
+ .getParameter(URL_PARAMETER_CLOSE_APPLICATION) != null);
+
+ Application application = getExistingApplication(request);
+ if (application != null) {
+ /*
+ * There is an existing application. We can use this as long as the
+ * user not specifically requested to close or restart it.
+ */
+ if (restartApplication) {
+ closeApplication(application, request.getSession(false));
+ return createApplication(request);
+ } else if (closeApplication) {
+ closeApplication(application, request.getSession(false));
+ return null;
+ } else {
+ return application;
+ }
+ }
+
+ // No existing application was found
+ if (requestType == RequestType.UIDL && isRepaintAll(request)) {
+ /*
+ * UIDL request contains valid repaintAll=1 event, the user probably
+ * wants to initiate a new application through a custom index.html
+ * without using writeAjaxPage.
+ */
+ return createApplication(request);
+
+ } else if (requestType == RequestType.OTHER) {
+ /*
+ * TODO Should *any* request really create a new application
+ * instance if none was found?
+ */
+ return createApplication(request);
+
+ } else {
+ /* The application was not found. Assume the session has expired. */
+ throw new SessionExpired();
+ }
+
+ }
+
+ /**
+ * Gets resource path using different implementations. Required to
+ * supporting different servlet container implementations (application
+ * servers).
+ *
+ * @param servletContext
+ * @param path
+ * the resource path.
+ * @return the resource path.
+ */
+ protected static String getResourcePath(ServletContext servletContext,
+ String path) {
+ String resultPath = null;
+ resultPath = servletContext.getRealPath(path);
+ if (resultPath != null) {
+ return resultPath;
+ } else {
+ try {
+ final URL url = servletContext.getResource(path);
+ resultPath = url.getFile();
+ } catch (final Exception e) {
+ // FIXME: Handle exception
+ e.printStackTrace();
+ }
+ }
+ return resultPath;
+ }
+
+ /**
+ * Handles the requested URI. An application can add handlers to do special
+ * processing, when a certain URI is requested. The handlers are invoked
+ * before any windows URIs are processed and if a DownloadStream is returned
+ * it is sent to the client.
+ *
+ * @param stream
+ * the download stream.
+ *
+ * @param request
+ * the HTTP request instance.
+ * @param response
+ * the HTTP response to write to.
+ * @throws IOException
+ *
+ * @see com.vaadin.terminal.URIHandler
+ */
+ private void handleDownload(DownloadStream stream,
+ HttpServletRequest request, HttpServletResponse response)
+ throws IOException {
+
+ if (stream.getParameter("Location") != null) {
+ response.setStatus(HttpServletResponse.SC_MOVED_TEMPORARILY);
+ response.addHeader("Location", stream.getParameter("Location"));
+ return;
+ }
+
+ // Download from given stream
+ final InputStream data = stream.getStream();
+ if (data != null) {
+
+ // Sets content type
+ response.setContentType(stream.getContentType());
+
+ // Sets cache headers
+ final long cacheTime = stream.getCacheTime();
+ if (cacheTime <= 0) {
+ response.setHeader("Cache-Control", "no-cache");
+ response.setHeader("Pragma", "no-cache");
+ response.setDateHeader("Expires", 0);
+ } else {
+ response.setHeader("Cache-Control", "max-age=" + cacheTime
+ / 1000);
+ response.setDateHeader("Expires", System.currentTimeMillis()
+ + cacheTime);
+ response.setHeader("Pragma", "cache"); // Required to apply
+ // caching in some
+ // Tomcats
+ }
+
+ // Copy download stream parameters directly
+ // to HTTP headers.
+ final Iterator i = stream.getParameterNames();
+ if (i != null) {
+ while (i.hasNext()) {
+ final String param = (String) i.next();
+ response.setHeader(param, stream.getParameter(param));
+ }
+ }
+
+ // suggest local filename from DownloadStream if Content-Disposition
+ // not explicitly set
+ String contentDispositionValue = stream
+ .getParameter("Content-Disposition");
+ if (contentDispositionValue == null) {
+ contentDispositionValue = "filename=\"" + stream.getFileName()
+ + "\"";
+ response.setHeader("Content-Disposition",
+ contentDispositionValue);
+ }
+
+ int bufferSize = stream.getBufferSize();
+ if (bufferSize <= 0 || bufferSize > MAX_BUFFER_SIZE) {
+ bufferSize = DEFAULT_BUFFER_SIZE;
+ }
+ final byte[] buffer = new byte[bufferSize];
+ int bytesRead = 0;
+
+ final OutputStream out = response.getOutputStream();
+
+ while ((bytesRead = data.read(buffer)) > 0) {
+ out.write(buffer, 0, bytesRead);
+ out.flush();
+ }
+ out.close();
+
+ }
+
+ }
+
+ /**
+ * Creates and starts a new application. This is not meant to be overridden.
+ * Override getNewApplication to create the application instance in a custom
+ * way.
+ *
+ * @param request
+ * @return
+ * @throws ServletException
+ * @throws MalformedURLException
+ */
+ private Application createApplication(HttpServletRequest request)
+ throws ServletException, MalformedURLException {
+ Application newApplication = getNewApplication(request);
+
+ // Create application
+ final URL applicationUrl = getApplicationUrl(request);
+
+ // Initial locale comes from the request
+ Locale locale = request.getLocale();
+ HttpSession session = request.getSession();
+
+ // Start the newly created application
+ startApplication(session, newApplication, applicationUrl, locale);
+
+ return newApplication;
+ }
+
+ private void handleServiceException(HttpServletRequest request,
+ HttpServletResponse response, Application application, Throwable e)
+ throws IOException, ServletException {
+ // if this was an UIDL request, response UIDL back to client
+ if (getRequestType(request) == RequestType.UIDL) {
+ Application.SystemMessages ci = getSystemMessages();
+ criticalNotification(request, response, ci
+ .getInternalErrorCaption(), ci.getInternalErrorMessage(),
+ ci.getInternalErrorURL());
+ if (application != null) {
+ application.getErrorHandler()
+ .terminalError(new RequestError(e));
+ } else {
+ throw new ServletException(e);
+ }
+ } else {
+ // Re-throw other exceptions
+ throw new ServletException(e);
+ }
+
+ }
+
+ private String getThemeForWindow(HttpServletRequest request, Window window) {
+ // Finds theme name
+ String themeName = window.getTheme();
+ if (request.getParameter(URL_PARAMETER_THEME) != null) {
+ themeName = request.getParameter(URL_PARAMETER_THEME);
+ }
+
+ if (themeName == null) {
+ themeName = "default";
+ }
+
+ return themeName;
+ }
+
+ /**
+ * Calls URI handlers for the request. If an URI handler returns a
+ * DownloadStream the stream is passed to the client for downloading.
+ *
+ * @param applicationManager
+ * @param window
+ * @param request
+ * @param response
+ * @return true if an DownloadStream was sent to the client, false otherwise
+ * @throws IOException
+ */
+ private boolean handleURI(CommunicationManager applicationManager,
+ Window window, HttpServletRequest request,
+ HttpServletResponse response) throws IOException {
+ // Handles the URI
+ DownloadStream download = applicationManager.handleURI(window, request,
+ response);
+
+ // A download request
+ if (download != null) {
+ // Client downloads an resource
+ handleDownload(download, request, response);
+ return true;
+ }
+
+ return false;
+ }
+
+ private void handleServiceSessionExpired(HttpServletRequest request,
+ HttpServletResponse response) throws IOException, ServletException {
+
+ if (isOnUnloadRequest(request)) {
+ /*
+ * Request was an unload request (e.g. window close event) and the
+ * client expects no response if it fails.
+ */
+ return;
+ }
+
+ try {
+ Application.SystemMessages ci = getSystemMessages();
+ if (getRequestType(request) != RequestType.UIDL) {
+ // 'plain' http req - e.g. browser reload;
+ // just go ahead redirect the browser
+ response.sendRedirect(ci.getSessionExpiredURL());
+ } else {
+ // send uidl redirect
+ criticalNotification(request, response, ci
+ .getSessionExpiredCaption(), ci
+ .getSessionExpiredMessage(), ci.getSessionExpiredURL());
+ /*
+ * Invalidate session (weird to have session if we're saying
+ * that it's expired, and worse: portal integration will fail
+ * since the session is not created by the portal.
+ */
+ request.getSession().invalidate();
+ }
+ } catch (SystemMessageException ee) {
+ throw new ServletException(ee);
+ }
+
+ }
+
+ private void handleServiceSecurityException(HttpServletRequest request,
+ HttpServletResponse response) throws IOException, ServletException {
+ if (isOnUnloadRequest(request)) {
+ /*
+ * Request was an unload request (e.g. window close event) and the
+ * client expects no response if it fails.
+ */
+ return;
+ }
+
+ // TODO handle differently?
+ // Invalid security key, show session expired message for now.
+ handleServiceSessionExpired(request, response);
+ }
+
+ /**
+ * Creates a new application for the given request.
+ *
+ * @param request
+ * the HTTP request.
+ * @return A new Application instance.
+ * @throws ServletException
+ */
+ protected abstract Application getNewApplication(HttpServletRequest request)
+ throws ServletException;
+
+ /**
+ * Starts the application if it is not already running. Ensures the
+ * application is added to the WebApplicationContext.
+ *
+ * @param session
+ * @param application
+ * @param applicationUrl
+ * @param locale
+ * @throws ServletException
+ */
+ private void startApplication(HttpSession session, Application application,
+ URL applicationUrl, Locale locale) throws ServletException {
+ if (application == null) {
+ throw new ServletException(
+ "Application is null and can't be started");
+ }
+
+ if (!application.isRunning()) {
+ final WebApplicationContext context = WebApplicationContext
+ .getApplicationContext(session);
+ // final URL applicationUrl = getApplicationUrl(request);
+ context.addApplication(application);
+ application.setLocale(locale);
+ application.start(applicationUrl, applicationProperties, context);
+ }
+ }
+
+ /**
+ * Check if this is a request for a static resource and, if it is, serve the
+ * resource to the client. Returns true if a file was served and the request
+ * has been handled, false otherwise.
+ *
+ * @param request
+ * @param response
+ * @return
+ * @throws IOException
+ * @throws ServletException
+ */
+ private boolean serveStaticResources(HttpServletRequest request,
+ HttpServletResponse response) throws IOException, ServletException {
+
+ // FIXME What does 10 refer to?
+ String pathInfo = request.getPathInfo();
+ if (pathInfo == null || pathInfo.length() <= 10) {
+ return false;
+ }
+
+ if ((request.getContextPath() != null)
+ && (request.getRequestURI().startsWith("/ITMILL/"))) {
+ serveStaticResourcesInITMILL(request.getRequestURI(), response);
+ return true;
+ } else if (request.getRequestURI().startsWith(
+ request.getContextPath() + "/ITMILL/")) {
+ serveStaticResourcesInITMILL(request.getRequestURI().substring(
+ request.getContextPath().length()), response);
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Serve resources from ITMILL directory.
+ *
+ * @param request
+ * @param response
+ * @throws IOException
+ * @throws ServletException
+ */
+ private void serveStaticResourcesInITMILL(String filename,
+ HttpServletResponse response) throws IOException, ServletException {
+
+ final ServletContext sc = getServletContext();
+ InputStream is = sc.getResourceAsStream(filename);
+ if (is == null) {
+ // try if requested file is found from classloader
+
+ // strip leading "/" otherwise stream from JAR wont work
+ filename = filename.substring(1);
+ is = getClassLoader().getResourceAsStream(filename);
+
+ if (is == null) {
+ // cannot serve requested file
+ System.err
+ .println("Requested resource ["
+ + filename
+ + "] not found from filesystem or through class loader."
+ + " Add widgetset and/or theme JAR to your classpath or add files to WebContent/ITMILL folder.");
+ response.setStatus(404);
+ return;
+ }
+ }
+ final String mimetype = sc.getMimeType(filename);
+ if (mimetype != null) {
+ response.setContentType(mimetype);
+ }
+ final OutputStream os = response.getOutputStream();
+ final byte buffer[] = new byte[20000];
+ int bytes;
+ while ((bytes = is.read(buffer)) >= 0) {
+ os.write(buffer, 0, bytes);
+ }
+ }
+
+ private enum RequestType {
+ FILE_UPLOAD, UIDL, OTHER;
+ }
+
+ private RequestType getRequestType(HttpServletRequest request) {
+ if (isFileUploadRequest(request)) {
+ return RequestType.FILE_UPLOAD;
+ } else if (isUIDLRequest(request)) {
+ return RequestType.UIDL;
+ }
+
+ return RequestType.OTHER;
+ }
+
+ private boolean isUIDLRequest(HttpServletRequest request) {
+ String pathInfo = getRequestPathInfo(request);
+
+ if (pathInfo == null) {
+ return false;
+ }
+
+ String compare = AJAX_UIDL_URI;
+
+ if (pathInfo.startsWith(compare + "/") || pathInfo.endsWith(compare)) {
+ return true;
+ }
+
+ return false;
+ }
+
+ private boolean isFileUploadRequest(HttpServletRequest request) {
+ return ServletFileUpload.isMultipartContent(request);
+ }
+
+ private boolean isOnUnloadRequest(HttpServletRequest request) {
+ return request.getParameter(ApplicationConnection.PARAM_UNLOADBURST) != null;
+ }
+
+ /**
+ * Get system messages from the current application class
+ *
+ * @return
+ */
+ protected SystemMessages getSystemMessages() {
+ try {
+ Class appCls = getApplicationClass();
+ Method m = appCls.getMethod("getSystemMessages", (Class[]) null);
+ return (Application.SystemMessages) m.invoke(null, (Object[]) null);
+ } catch (ClassNotFoundException e) {
+ // This should never happen
+ throw new SystemMessageException(e);
+ } catch (SecurityException e) {
+ throw new SystemMessageException(
+ "Application.getSystemMessage() should be static public", e);
+ } catch (NoSuchMethodException e) {
+ // This is completely ok and should be silently ignored
+ } catch (IllegalArgumentException e) {
+ // This should never happen
+ throw new SystemMessageException(e);
+ } catch (IllegalAccessException e) {
+ throw new SystemMessageException(
+ "Application.getSystemMessage() should be static public", e);
+ } catch (InvocationTargetException e) {
+ // This should never happen
+ throw new SystemMessageException(e);
+ }
+ return Application.getSystemMessages();
+ }
+
+ protected abstract Class getApplicationClass()
+ throws ClassNotFoundException;
+
+ /**
+ * Resolve widgetset URL. Widgetset is not application specific.
+ *
+ * @param request
+ * @return
+ * @throws MalformedURLException
+ */
+ String getWidgetsetLocation(HttpServletRequest request)
+ throws MalformedURLException {
+ URL applicationURL = getApplicationUrl(request);
+
+ String applicationPath = applicationURL.getPath();
+ String pathParts[] = applicationPath.split("\\/");
+
+ // if context is specified add it to widgetsetUrl
+ String ctxPath = request.getContextPath();
+ if (ctxPath.length() == 0
+ && request.getAttribute("javax.servlet.include.context_path") != null) {
+ // include request (e.g portlet), get context path from
+ // attribute
+ ctxPath = (String) request
+ .getAttribute("javax.servlet.include.context_path");
+ }
+
+ String widgetsetPath = "";
+ if (pathParts.length > 1
+ && pathParts[1].equals(ctxPath.replaceAll("\\/", ""))) {
+ widgetsetPath = "/" + pathParts[1];
+ }
+
+ return widgetsetPath;
+
+ }
+
+ /**
+ *
+ * @param request
+ * the HTTP request.
+ * @param response
+ * the HTTP response to write to.
+ * @param out
+ * @param unhandledParameters
+ * @param window
+ * @param terminalType
+ * @param theme
+ * @throws IOException
+ * if the writing failed due to input/output error.
+ * @throws MalformedURLException
+ * if the application is denied access the persistent data store
+ * represented by the given URL.
+ */
+ private void writeAjaxPage(HttpServletRequest request,
+ HttpServletResponse response, Window window, String themeName,
+ Application application) throws IOException, MalformedURLException,
+ ServletException {
+
+ // e.g portlets only want a html fragment
+ boolean fragment = (request.getAttribute(REQUEST_FRAGMENT) != null);
+ if (fragment) {
+ request.setAttribute(Application.class.getName(), application);
+ }
+
+ final BufferedWriter page = new BufferedWriter(new OutputStreamWriter(
+ response.getOutputStream()));
+ String pathInfo = getRequestPathInfo(request);
+ if (pathInfo == null) {
+ pathInfo = "/";
+ }
+
+ String title = ((window.getCaption() == null) ? "IT Mill Toolkit 5"
+ : window.getCaption());
+
+ String widgetset = null;
+ // request widgetset takes precedence (e.g portlet include)
+ Object reqParam = request.getAttribute(REQUEST_WIDGETSET);
+ try {
+ widgetset = (String) reqParam;
+ } catch (Exception e) {
+ // FIXME: Handle exception
+ System.err.println("Warning: request param '" + REQUEST_WIDGETSET
+ + "' could not be used (is not a String)" + e);
+ }
+
+ // TODO: Any reason this could not use
+ // getApplicationOrSystemProperty with DEFAULT_WIDGETSET as default
+ // value ?
+ if (widgetset == null) {
+ widgetset = getApplicationProperty(PARAMETER_WIDGETSET);
+ }
+ if (widgetset == null) {
+ widgetset = DEFAULT_WIDGETSET;
+ }
+
+ /* Fetch relative url to application */
+ // don't use server and port in uri. It may cause problems with some
+ // virtual server configurations which lose the server name
+ String appUrl = getApplicationUrl(request).getPath();
+ if (appUrl.endsWith("/")) {
+ appUrl = appUrl.substring(0, appUrl.length() - 1);
+ }
+
+ final String widgetsetPath = getWidgetsetLocation(request);
+
+ final String staticFilePath = getApplicationOrSystemProperty(
+ PARAMETER_ITMILL_RESOURCES, widgetsetPath);
+
+ // Default theme does not use theme URI
+ String themeUri = null;
+ if (themeName != null) {
+ // Using custom theme
+ themeUri = staticFilePath + "/" + THEME_DIRECTORY_PATH + themeName;
+ }
+
+ if (!fragment) {
+ // Window renders are not cacheable
+ response.setHeader("Cache-Control", "no-cache");
+ response.setHeader("Pragma", "no-cache");
+ response.setDateHeader("Expires", 0);
+ response.setContentType("text/html; charset=UTF-8");
+
+ // write html header
+ page.write("<!DOCTYPE html PUBLIC \"-//W3C//DTD "
+ + "XHTML 1.0 Transitional//EN\" "
+ + "\"http://www.w3.org/TR/xhtml1/"
+ + "DTD/xhtml1-transitional.dtd\">\n");
+
+ page.write("<html xmlns=\"http://www.w3.org/1999/xhtml\""
+ + ">\n<head>\n");
+ page
+ .write("<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\"/>\n");
+ page
+ .write("<meta http-equiv=\"X-UA-Compatible\" content=\"IE=7\" />\n");
+ page.write("<style type=\"text/css\">"
+ + "html, body {height:100%;}</style>");
+
+ // Add favicon links
+ page
+ .write("<link rel=\"shortcut icon\" type=\"image/vnd.microsoft.icon\" href=\""
+ + themeUri + "/favicon.ico\" />");
+ page
+ .write("<link rel=\"icon\" type=\"image/vnd.microsoft.icon\" href=\""
+ + themeUri + "/favicon.ico\" />");
+
+ page.write("<title>" + title + "</title>");
+
+ page
+ .write("\n</head>\n<body scroll=\"auto\" class=\"i-generated-body\">\n");
+ }
+
+ String appId = appUrl;
+ if ("".equals(appUrl)) {
+ appId = "ROOT";
+ }
+ appId = appId.replaceAll("[^a-zA-Z0-9]", "");
+ // Add hashCode to the end, so that it is still (sort of) predictable,
+ // but indicates that it should not be used in CSS and such:
+ int hashCode = appId.hashCode();
+ if (hashCode < 0) {
+ hashCode = -hashCode;
+ }
+ appId = appId + "-" + hashCode;
+
+ // Get system messages
+ Application.SystemMessages systemMessages = null;
+ try {
+ systemMessages = getSystemMessages();
+ } catch (SystemMessageException e) {
+ // failing to get the system messages is always a problem
+ throw new ServletException("CommunicationError!", e);
+ }
+
+ if (isGecko17(request)) {
+ // special start page for gecko 1.7 versions. Firefox 1.0 is not
+ // supported, but the hack is make it possible to use linux and
+ // hosted mode browser for debugging. Note that due this hack,
+ // debugging gwt code in portals with linux will be problematic if
+ // there are multiple toolkit portlets visible at the same time.
+ // TODO remove this when hosted mode on linux gets newer gecko
+
+ page.write("<iframe tabIndex=\"-1\" id=\"__gwt_historyFrame\" "
+ + "style=\"width:0;height:0;border:0;overflow:"
+ + "hidden\" src=\"javascript:false\"></iframe>\n");
+ page.write("<script language='javascript' src='" + staticFilePath
+ + "/" + WIDGETSET_DIRECTORY_PATH + widgetset + "/"
+ + widgetset + ".nocache.js?" + new Date().getTime()
+ + "'></script>\n");
+ page.write("<script type=\"text/javascript\">\n");
+ page.write("//<![CDATA[\n");
+ page.write("if(!itmill || !itmill.toolkitConfigurations) {\n "
+ + "if(!itmill) { var itmill = {}} \n"
+ + "itmill.toolkitConfigurations = {};\n"
+ + "itmill.themesLoaded = {}};\n");
+
+ if (!isProductionMode()) {
+ page.write("itmill.debug = true;\n");
+ }
+
+ page.write("itmill.toolkitConfigurations[\"" + appId + "\"] = {");
+ page.write("appUri:'" + appUrl + "', ");
+ page.write("pathInfo: '" + pathInfo + "', ");
+ if (window != application.getMainWindow()) {
+ page.write("windowName: '" + window.getName() + "', ");
+ }
+ page.write("themeUri:");
+ page.write(themeUri != null ? "'" + themeUri + "'" : "null");
+ page.write(", versionInfo : {toolkitVersion:\"");
+ page.write(VERSION);
+ page.write("\",applicationVersion:\"");
+ page.write(application.getVersion());
+ page.write("\"},");
+ if (systemMessages != null) {
+ // Write the CommunicationError -message to client
+ String caption = systemMessages.getCommunicationErrorCaption();
+ if (caption != null) {
+ caption = "\"" + caption + "\"";
+ }
+ String message = systemMessages.getCommunicationErrorMessage();
+ if (message != null) {
+ message = "\"" + message + "\"";
+ }
+ String url = systemMessages.getCommunicationErrorURL();
+ if (url != null) {
+ url = "\"" + url + "\"";
+ }
+ page.write("\"comErrMsg\": {" + "\"caption\":" + caption + ","
+ + "\"message\" : " + message + "," + "\"url\" : " + url
+ + "}");
+ }
+ page.write("};\n//]]>\n</script>\n");
+
+ if (themeName != null) {
+ // Custom theme's stylesheet, load only once, in different
+ // script
+ // tag to be dominate styles injected by widget
+ // set
+ page.write("<script type=\"text/javascript\">\n");
+ page.write("//<![CDATA[\n");
+ page.write("if(!itmill.themesLoaded['" + themeName + "']) {\n");
+ page
+ .write("var stylesheet = document.createElement('link');\n");
+ page.write("stylesheet.setAttribute('rel', 'stylesheet');\n");
+ page.write("stylesheet.setAttribute('type', 'text/css');\n");
+ page.write("stylesheet.setAttribute('href', '" + themeUri
+ + "/styles.css');\n");
+ page
+ .write("document.getElementsByTagName('head')[0].appendChild(stylesheet);\n");
+ page.write("itmill.themesLoaded['" + themeName
+ + "'] = true;\n}\n");
+ page.write("//]]>\n</script>\n");
+ }
+
+ } else {
+ page.write("<script type=\"text/javascript\">\n");
+ page.write("//<![CDATA[\n");
+ page.write("if(!itmill || !itmill.toolkitConfigurations) {\n "
+ + "if(!itmill) { var itmill = {}} \n"
+ + "itmill.toolkitConfigurations = {};\n"
+ + "itmill.themesLoaded = {};\n");
+ if (!isProductionMode()) {
+ page.write("itmill.debug = true;\n");
+ }
+ page
+ .write("document.write('<iframe tabIndex=\"-1\" id=\"__gwt_historyFrame\" "
+ + "style=\"width:0;height:0;border:0;overflow:"
+ + "hidden\" src=\"javascript:false\"></iframe>');\n");
+ page.write("document.write(\"<script language='javascript' src='"
+ + staticFilePath + "/" + WIDGETSET_DIRECTORY_PATH
+ + widgetset + "/" + widgetset + ".nocache.js?"
+ + new Date().getTime() + "'><\\/script>\");\n}\n");
+
+ page.write("itmill.toolkitConfigurations[\"" + appId + "\"] = {");
+ page.write("appUri:'" + appUrl + "', ");
+ page.write("pathInfo: '" + pathInfo + "', ");
+ if (window != application.getMainWindow()) {
+ page.write("windowName: '" + window.getName() + "', ");
+ }
+ page.write("themeUri:");
+ page.write(themeUri != null ? "'" + themeUri + "'" : "null");
+ page.write(", versionInfo : {toolkitVersion:\"");
+ page.write(VERSION);
+ page.write("\",applicationVersion:\"");
+ page.write(application.getVersion());
+ page.write("\"},");
+ if (systemMessages != null) {
+ // Write the CommunicationError -message to client
+ String caption = systemMessages.getCommunicationErrorCaption();
+ if (caption != null) {
+ caption = "\"" + caption + "\"";
+ }
+ String message = systemMessages.getCommunicationErrorMessage();
+ if (message != null) {
+ message = "\"" + message + "\"";
+ }
+ String url = systemMessages.getCommunicationErrorURL();
+ if (url != null) {
+ url = "\"" + url + "\"";
+ }
+
+ page.write("\"comErrMsg\": {" + "\"caption\":" + caption + ","
+ + "\"message\" : " + message + "," + "\"url\" : " + url
+ + "}");
+ }
+ page.write("};\n//]]>\n</script>\n");
+
+ if (themeName != null) {
+ // Custom theme's stylesheet, load only once, in different
+ // script
+ // tag to be dominate styles injected by widget
+ // set
+ page.write("<script type=\"text/javascript\">\n");
+ page.write("//<![CDATA[\n");
+ page.write("if(!itmill.themesLoaded['" + themeName + "']) {\n");
+ page
+ .write("var stylesheet = document.createElement('link');\n");
+ page.write("stylesheet.setAttribute('rel', 'stylesheet');\n");
+ page.write("stylesheet.setAttribute('type', 'text/css');\n");
+ page.write("stylesheet.setAttribute('href', '" + themeUri
+ + "/styles.css');\n");
+ page
+ .write("document.getElementsByTagName('head')[0].appendChild(stylesheet);\n");
+ page.write("itmill.themesLoaded['" + themeName
+ + "'] = true;\n}\n");
+ page.write("//]]>\n</script>\n");
+ }
+ }
+
+ // Warn if the widgetset has not been loaded after 15 seconds on
+ // inactivity
+ page.write("<script type=\"text/javascript\">\n");
+ page.write("//<![CDATA[\n");
+ page.write("setTimeout('if (typeof " + widgetset.replace('.', '_')
+ + " == \"undefined\") {alert(\"Failed to load the widgetset: "
+ + staticFilePath + "/" + WIDGETSET_DIRECTORY_PATH + widgetset
+ + "/" + widgetset + ".nocache.js\")};',15000);\n"
+ + "//]]>\n</script>\n");
+
+ String style = null;
+ reqParam = request.getAttribute(REQUEST_APPSTYLE);
+ if (reqParam != null) {
+ style = "style=\"" + reqParam + "\"";
+ }
+ /*- Add classnames;
+ * .i-app
+ * .i-app-loading
+ * .i-app-<simpleName for app class>
+ * .i-theme-<themeName, remove non-alphanum>
+ */
+ String appClass = "i-app-";
+ try {
+ appClass += getApplicationClass().getSimpleName();
+ } catch (ClassNotFoundException e) {
+ appClass += "unknown";
+
+ e.printStackTrace();
+ }
+ String themeClass = "";
+ if (themeName != null) {
+ themeClass = "i-theme-" + themeName.replaceAll("[^a-zA-Z0-9]", "");
+ }
+
+ page.write("<div id=\"" + appId + "\" class=\"i-app i-app-loading "
+ + themeClass + " " + appClass + "\" "
+ + (style != null ? style : "") + "></div>\n");
+
+ if (!fragment) {
+ page.write("<noscript>" + getNoScriptMessage() + "</noscript>");
+ page.write("</body>\n</html>\n");
+ }
+ page.close();
+
+ }
+
+ /**
+ * Returns a message printed for browsers without scripting support or if
+ * browsers scripting support is disabled.
+ */
+ protected String getNoScriptMessage() {
+ return "You have to enable javascript in your browser to use an application built with IT Mill Toolkit.";
+ }
+
+ private boolean isGecko17(HttpServletRequest request) {
+ final WebBrowser browser = WebApplicationContext.getApplicationContext(
+ request.getSession()).getBrowser();
+ if (browser != null && browser.getBrowserApplication() != null) {
+ if (browser.getBrowserApplication().indexOf("rv:1.7.") > 0
+ && browser.getBrowserApplication().indexOf("Gecko") > 0) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Handles theme resource file requests. Resources supplied with the themes
+ * are provided by the WebAdapterServlet.
+ *
+ * @param request
+ * the HTTP request.
+ * @param response
+ * the HTTP response.
+ * @return boolean <code>true</code> if the request was handled and further
+ * processing should be suppressed, <code>false</code> otherwise.
+ * @throws ServletException
+ * if an exception has occurred that interferes with the
+ * servlet's normal operation.
+ */
+ private boolean handleResourceRequest(HttpServletRequest request,
+ HttpServletResponse response, String themeName)
+ throws ServletException {
+
+ // If the resource path is unassigned, initialize it
+ if (resourcePath == null) {
+ resourcePath = request.getContextPath() + request.getServletPath()
+ + RESOURCE_URI;
+ // WebSphere Application Server related fix
+ resourcePath = resourcePath.replaceAll("//", "/");
+ }
+
+ String resourceId = request.getPathInfo();
+
+ // Checks if this really is a resource request
+ if (resourceId == null || !resourceId.startsWith(RESOURCE_URI)) {
+ return false;
+ }
+
+ // Checks the resource type
+ resourceId = resourceId.substring(RESOURCE_URI.length());
+ InputStream data = null;
+
+ // Gets theme resources
+ try {
+ data = getServletContext().getResourceAsStream(
+ THEME_DIRECTORY_PATH + themeName + "/" + resourceId);
+ } catch (final Exception e) {
+ // FIXME: Handle exception
+ e.printStackTrace();
+ data = null;
+ }
+
+ // Writes the response
+ try {
+ if (data != null) {
+ response.setContentType(FileTypeResolver
+ .getMIMEType(resourceId));
+
+ // Use default cache time for theme resources
+ response.setHeader("Cache-Control", "max-age="
+ + DEFAULT_THEME_CACHETIME / 1000);
+ response.setDateHeader("Expires", System.currentTimeMillis()
+ + DEFAULT_THEME_CACHETIME);
+ response.setHeader("Pragma", "cache"); // Required to apply
+ // caching in some
+ // Tomcats
+
+ // Writes the data to client
+ final byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
+ int bytesRead = 0;
+ final OutputStream out = response.getOutputStream();
+ while ((bytesRead = data.read(buffer)) > 0) {
+ out.write(buffer, 0, bytesRead);
+ }
+ out.close();
+ data.close();
+ } else {
+ response.sendError(HttpServletResponse.SC_NOT_FOUND);
+ }
+
+ } catch (final java.io.IOException e) {
+ // FIXME: Handle exception
+ System.err.println("Resource transfer failed: "
+ + request.getRequestURI() + ". (" + e.getMessage() + ")");
+ }
+
+ return true;
+ }
+
+ /**
+ * Gets the current application URL from request.
+ *
+ * @param request
+ * the HTTP request.
+ * @throws MalformedURLException
+ * if the application is denied access to the persistent data
+ * store represented by the given URL.
+ */
+ URL getApplicationUrl(HttpServletRequest request)
+ throws MalformedURLException {
+ final URL reqURL = new URL(
+ (request.isSecure() ? "https://" : "http://")
+ + request.getServerName()
+ + ((request.isSecure() && request.getServerPort() == 443)
+ || (!request.isSecure() && request
+ .getServerPort() == 80) ? "" : ":"
+ + request.getServerPort())
+ + request.getRequestURI());
+ String servletPath = "";
+ if (request.getAttribute("javax.servlet.include.servlet_path") != null) {
+ // this is an include request
+ servletPath = request.getAttribute(
+ "javax.servlet.include.context_path").toString()
+ + request
+ .getAttribute("javax.servlet.include.servlet_path");
+
+ } else {
+ servletPath = request.getContextPath() + request.getServletPath();
+ }
+
+ if (servletPath.length() == 0
+ || servletPath.charAt(servletPath.length() - 1) != '/') {
+ servletPath = servletPath + "/";
+ }
+
+ URL u = new URL(reqURL, servletPath);
+ return u;
+ }
+
+ /**
+ * Gets the existing application for given request. Looks for application
+ * instance for given request based on the requested URL.
+ *
+ * @param request
+ * the HTTP request.
+ * @return Application instance, or null if the URL does not map to valid
+ * application.
+ * @throws MalformedURLException
+ * if the application is denied access to the persistent data
+ * store represented by the given URL.
+ * @throws SAXException
+ * @throws IllegalAccessException
+ * @throws InstantiationException
+ */
+ private Application getExistingApplication(HttpServletRequest request)
+ throws MalformedURLException, SAXException, IllegalAccessException,
+ InstantiationException {
+
+ // Ensures that the session is still valid
+ final HttpSession session = request.getSession(true);
+
+ WebApplicationContext context = WebApplicationContext
+ .getApplicationContext(session);
+
+ // Gets application list for the session.
+ final Collection applications = context.getApplications();
+
+ // Search for the application (using the application URI) from the list
+ for (final Iterator i = applications.iterator(); i.hasNext();) {
+ final Application sessionApplication = (Application) i.next();
+ final String sessionApplicationPath = sessionApplication.getURL()
+ .getPath();
+ String requestApplicationPath = getApplicationUrl(request)
+ .getPath();
+
+ if (requestApplicationPath.equals(sessionApplicationPath)) {
+ // Found a running application
+ if (sessionApplication.isRunning()) {
+ return sessionApplication;
+ }
+ // Application has stopped, so remove it before creating a new
+ // application
+ WebApplicationContext.getApplicationContext(session)
+ .removeApplication(sessionApplication);
+ break;
+ }
+ }
+
+ // Existing application not found
+ return null;
+ }
+
+ /**
+ * Ends the application.
+ *
+ * @param request
+ * the HTTP request.
+ * @param response
+ * the HTTP response to write to.
+ * @param application
+ * the application to end.
+ * @throws IOException
+ * if the writing failed due to input/output error.
+ */
+ private void endApplication(HttpServletRequest request,
+ HttpServletResponse response, Application application)
+ throws IOException {
+
+ String logoutUrl = application.getLogoutURL();
+ if (logoutUrl == null) {
+ logoutUrl = application.getURL().toString();
+ }
+
+ final HttpSession session = request.getSession();
+ if (session != null) {
+ WebApplicationContext.getApplicationContext(session)
+ .removeApplication(application);
+ }
+
+ response.sendRedirect(response.encodeRedirectURL(logoutUrl));
+ }
+
+ /**
+ * Gets the existing application or create a new one. Get a window within an
+ * application based on the requested URI.
+ *
+ * @param request
+ * the HTTP Request.
+ * @param application
+ * the Application to query for window.
+ * @return Window matching the given URI or null if not found.
+ * @throws ServletException
+ * if an exception has occurred that interferes with the
+ * servlet's normal operation.
+ */
+ private Window getApplicationWindow(HttpServletRequest request,
+ Application application) throws ServletException {
+
+ Window window = null;
+
+ // Finds the window where the request is handled
+ String path = getRequestPathInfo(request);
+
+ // Main window as the URI is empty
+ if (path == null || path.length() == 0 || path.equals("/")
+ || path.startsWith("/APP/")) {
+ window = application.getMainWindow();
+ } else {
+ String windowName = null;
+ if (path.charAt(0) == '/') {
+ path = path.substring(1);
+ }
+ final int index = path.indexOf('/');
+ if (index < 0) {
+ windowName = path;
+ path = "";
+ } else {
+ windowName = path.substring(0, index);
+ path = path.substring(index + 1);
+ }
+ window = application.getWindow(windowName);
+
+ if (window == null) {
+ // By default, we use main window
+ window = application.getMainWindow();
+ } else if (!window.isVisible()) {
+ // Implicitly painting without actually invoking paint()
+ window.requestRepaintRequests();
+
+ // If the window is invisible send a blank page
+ return null;
+ }
+ }
+
+ return window;
+ }
+
+ String getRequestPathInfo(HttpServletRequest request) {
+ return request.getPathInfo();
+ }
+
+ /**
+ * Gets relative location of a theme resource.
+ *
+ * @param theme
+ * the Theme name.
+ * @param resource
+ * the Theme resource.
+ * @return External URI specifying the resource
+ */
+ public String getResourceLocation(String theme, ThemeResource resource) {
+
+ if (resourcePath == null) {
+ return resource.getResourceId();
+ }
+ return resourcePath + theme + "/" + resource.getResourceId();
+ }
+
+ private boolean isRepaintAll(HttpServletRequest request) {
+ return (request.getParameter(URL_PARAMETER_REPAINT_ALL) != null)
+ && (request.getParameter(URL_PARAMETER_REPAINT_ALL).equals("1"));
+ }
+
+ private void closeApplication(Application application, HttpSession session) {
+ if (application == null) {
+ return;
+ }
+
+ application.close();
+ if (session != null) {
+ WebApplicationContext context = WebApplicationContext
+ .getApplicationContext(session);
+ context.applicationToAjaxAppMgrMap.remove(application);
+ // FIXME: Move to WebApplicationContext
+ context.removeApplication(application);
+ }
+ }
+
+ /**
+ * Implementation of ParameterHandler.ErrorEvent interface.
+ */
+ public class ParameterHandlerErrorImpl implements
+ ParameterHandler.ErrorEvent, Serializable {
+
+ private ParameterHandler owner;
+
+ private Throwable throwable;
+
+ /**
+ * Gets the contained throwable.
+ *
+ * @see com.vaadin.terminal.Terminal.ErrorEvent#getThrowable()
+ */
+ public Throwable getThrowable() {
+ return throwable;
+ }
+
+ /**
+ * Gets the source ParameterHandler.
+ *
+ * @see com.vaadin.terminal.ParameterHandler.ErrorEvent#getParameterHandler()
+ */
+ public ParameterHandler getParameterHandler() {
+ return owner;
+ }
+
+ }
+
+ /**
+ * Implementation of URIHandler.ErrorEvent interface.
+ */
+ public class URIHandlerErrorImpl implements URIHandler.ErrorEvent,
+ Serializable {
+
+ private final URIHandler owner;
+
+ private final Throwable throwable;
+
+ /**
+ *
+ * @param owner
+ * @param throwable
+ */
+ private URIHandlerErrorImpl(URIHandler owner, Throwable throwable) {
+ this.owner = owner;
+ this.throwable = throwable;
+ }
+
+ /**
+ * Gets the contained throwable.
+ *
+ * @see com.vaadin.terminal.Terminal.ErrorEvent#getThrowable()
+ */
+ public Throwable getThrowable() {
+ return throwable;
+ }
+
+ /**
+ * Gets the source URIHandler.
+ *
+ * @see com.vaadin.terminal.URIHandler.ErrorEvent#getURIHandler()
+ */
+ public URIHandler getURIHandler() {
+ return owner;
+ }
+ }
+
+ public class RequestError implements Terminal.ErrorEvent, Serializable {
+
+ private final Throwable throwable;
+
+ public RequestError(Throwable throwable) {
+ this.throwable = throwable;
+ }
+
+ public Throwable getThrowable() {
+ return throwable;
+ }
+
+ }
+}
diff --git a/src/com/vaadin/terminal/gwt/server/ApplicationPortlet.java b/src/com/vaadin/terminal/gwt/server/ApplicationPortlet.java
new file mode 100644
index 0000000000..25b1c387a2
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/server/ApplicationPortlet.java
@@ -0,0 +1,93 @@
+package com.vaadin.terminal.gwt.server;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.Serializable;
+
+import javax.portlet.ActionRequest;
+import javax.portlet.ActionResponse;
+import javax.portlet.Portlet;
+import javax.portlet.PortletConfig;
+import javax.portlet.PortletException;
+import javax.portlet.PortletRequestDispatcher;
+import javax.portlet.PortletSession;
+import javax.portlet.RenderRequest;
+import javax.portlet.RenderResponse;
+
+import com.vaadin.Application;
+
+@SuppressWarnings("serial")
+public class ApplicationPortlet implements Portlet, Serializable {
+ // The application to show
+ protected String app = null;
+ // some applications might require forced height (and, more seldom, width)
+ protected String style = null; // e.g "height:500px;"
+ protected String widgetset = null;
+
+ public void destroy() {
+
+ }
+
+ public void init(PortletConfig config) throws PortletException {
+ app = config.getInitParameter("application");
+ if (app == null) {
+ app = "PortletDemo";
+ }
+ style = config.getInitParameter("style");
+ widgetset = config.getInitParameter("widgetset");
+ }
+
+ public void processAction(ActionRequest request, ActionResponse response)
+ throws PortletException, IOException {
+ PortletApplicationContext.dispatchRequest(this, request, response);
+ }
+
+ public void render(RenderRequest request, RenderResponse response)
+ throws PortletException, IOException {
+
+ // display the IT Mill Toolkit application
+ writeAjaxWindow(request, response);
+ }
+
+ protected void writeAjaxWindow(RenderRequest request,
+ RenderResponse response) throws IOException {
+
+ response.setContentType("text/html");
+ if (app != null) {
+ PortletSession sess = request.getPortletSession();
+ PortletApplicationContext ctx = PortletApplicationContext
+ .getApplicationContext(sess);
+
+ PortletRequestDispatcher dispatcher = sess.getPortletContext()
+ .getRequestDispatcher("/" + app);
+
+ try {
+ request.setAttribute(ApplicationServlet.REQUEST_FRAGMENT,
+ "true");
+ if (widgetset != null) {
+ request.setAttribute(ApplicationServlet.REQUEST_WIDGETSET,
+ widgetset);
+ }
+ if (style != null) {
+ request.setAttribute(ApplicationServlet.REQUEST_APPSTYLE,
+ style);
+ }
+ dispatcher.include(request, response);
+
+ } catch (PortletException e) {
+ PrintWriter out = response.getWriter();
+ out.print("<h1>Servlet include failed!</h1>");
+ out.print("<div>" + e + "</div>");
+ ctx.setPortletApplication(this, null);
+ return;
+ }
+
+ Application app = (Application) request
+ .getAttribute(Application.class.getName());
+ ctx.setPortletApplication(this, app);
+ ctx.firePortletRenderRequest(this, request, response);
+
+ }
+ }
+
+}
diff --git a/src/com/vaadin/terminal/gwt/server/ApplicationRunnerServlet.java b/src/com/vaadin/terminal/gwt/server/ApplicationRunnerServlet.java
new file mode 100644
index 0000000000..774b866a44
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/server/ApplicationRunnerServlet.java
@@ -0,0 +1,173 @@
+package com.vaadin.terminal.gwt.server;
+
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URL;
+
+import javax.servlet.ServletConfig;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import com.vaadin.Application;
+
+@SuppressWarnings("serial")
+public class ApplicationRunnerServlet extends AbstractApplicationServlet {
+
+ /**
+ * The name of the application class currently used. Only valid within one
+ * request.
+ */
+ String applicationClassName = "";
+
+ @Override
+ public void init(ServletConfig servletConfig) throws ServletException {
+ super.init(servletConfig);
+ }
+
+ @Override
+ protected void service(HttpServletRequest request,
+ HttpServletResponse response) throws ServletException, IOException {
+ applicationClassName = getApplicationRunnerApplicationClassName(request);
+ super.service(request, response);
+ applicationClassName = "";
+
+ }
+
+ @Override
+ URL getApplicationUrl(HttpServletRequest request)
+ throws MalformedURLException {
+ URL url = super.getApplicationUrl(request);
+
+ String path = url.toString();
+ path += applicationClassName;
+ path += "/";
+
+ return new URL(path);
+ }
+
+ @Override
+ protected Application getNewApplication(HttpServletRequest request)
+ throws ServletException {
+
+ // Creates a new application instance
+ try {
+ Class<?> applicationClass = getClassLoader().loadClass(
+ applicationClassName);
+ final Application application = (Application) applicationClass
+ .newInstance();
+
+ return application;
+ } catch (final IllegalAccessException e) {
+ throw new ServletException(e);
+ } catch (final InstantiationException e) {
+ throw new ServletException(e);
+ } catch (final ClassNotFoundException e) {
+ throw new ServletException(
+ new InstantiationException(
+ "Failed to load application class: "
+ + applicationClassName));
+ }
+
+ }
+
+ private String getApplicationRunnerApplicationClassName(
+ HttpServletRequest request) {
+ return getApplicationRunnerURIs(request).applicationClassname;
+ }
+
+ private static class URIS {
+ String widgetsetPath;
+ String applicationURI;
+ String context;
+ String runner;
+ String applicationClassname;
+
+ }
+
+ /**
+ * Parses application runner URIs.
+ *
+ * If request URL is e.g.
+ * http://localhost:8080/itmill/run/com.vaadin.demo.Calc then
+ * <ul>
+ * <li>context=itmill</li>
+ * <li>Runner servlet=run</li>
+ * <li>Toolkit application=com.vaadin.demo.Calc</li>
+ * </ul>
+ *
+ * @param request
+ * @return string array containing widgetset URI, application URI and
+ * context, runner, application classname
+ */
+ private static URIS getApplicationRunnerURIs(HttpServletRequest request) {
+ final String[] urlParts = request.getRequestURI().toString().split(
+ "\\/");
+ String context = null;
+ String runner = null;
+ URIS uris = new URIS();
+ String applicationClassname = null;
+ String contextPath = request.getContextPath();
+ if (urlParts[1].equals(contextPath.replaceAll("\\/", ""))) {
+ // class name comes after web context and runner application
+ context = urlParts[1];
+ runner = urlParts[2];
+ if (urlParts.length == 3) {
+ throw new IllegalArgumentException("No application specified");
+ }
+ applicationClassname = urlParts[3];
+
+ uris.widgetsetPath = "/" + context;
+ uris.applicationURI = "/" + context + "/" + runner + "/"
+ + applicationClassname;
+ uris.context = context;
+ uris.runner = runner;
+ uris.applicationClassname = applicationClassname;
+ } else {
+ // no context
+ context = "";
+ runner = urlParts[1];
+ if (urlParts.length == 2) {
+ throw new IllegalArgumentException("No application specified");
+ }
+ applicationClassname = urlParts[2];
+
+ uris.widgetsetPath = "/";
+ uris.applicationURI = "/" + runner + "/" + applicationClassname;
+ uris.context = context;
+ uris.runner = runner;
+ uris.applicationClassname = applicationClassname;
+ }
+ return uris;
+ }
+
+ // @Override
+ @Override
+ protected Class getApplicationClass() throws ClassNotFoundException {
+ // TODO use getClassLoader() ?
+ return getClass().getClassLoader().loadClass(applicationClassName);
+ }
+
+ @Override
+ String getRequestPathInfo(HttpServletRequest request) {
+ String path = request.getPathInfo();
+ if (path == null) {
+ return null;
+ }
+
+ path = path.substring(1 + applicationClassName.length());
+ return path;
+ }
+
+ @Override
+ String getWidgetsetLocation(HttpServletRequest request) {
+ URIS uris = getApplicationRunnerURIs(request);
+ String widgetsetPath = uris.widgetsetPath;
+ if (widgetsetPath.equals("/")) {
+ widgetsetPath = "";
+ }
+
+ return widgetsetPath;
+ }
+
+}
diff --git a/src/com/vaadin/terminal/gwt/server/ApplicationServlet.java b/src/com/vaadin/terminal/gwt/server/ApplicationServlet.java
new file mode 100644
index 0000000000..990c84bab0
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/server/ApplicationServlet.java
@@ -0,0 +1,84 @@
+/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.server;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+
+import com.vaadin.Application;
+
+/**
+ * This servlet connects IT Mill Toolkit Application to Web.
+ *
+ * @author IT Mill Ltd.
+ * @version
+ * @VERSION@
+ * @since 5.0
+ */
+
+@SuppressWarnings("serial")
+public class ApplicationServlet extends AbstractApplicationServlet {
+
+ // Private fields
+ private Class<? extends Application> applicationClass;
+
+ /**
+ * Called by the servlet container to indicate to a servlet that the servlet
+ * is being placed into service.
+ *
+ * @param servletConfig
+ * the object containing the servlet's configuration and
+ * initialization parameters
+ * @throws javax.servlet.ServletException
+ * if an exception has occurred that interferes with the
+ * servlet's normal operation.
+ */
+ @SuppressWarnings("unchecked")
+ @Override
+ public void init(javax.servlet.ServletConfig servletConfig)
+ throws javax.servlet.ServletException {
+ super.init(servletConfig);
+
+ // Loads the application class using the same class loader
+ // as the servlet itself
+
+ // Gets the application class name
+ final String applicationClassName = servletConfig
+ .getInitParameter("application");
+ if (applicationClassName == null) {
+ throw new ServletException(
+ "Application not specified in servlet parameters");
+ }
+
+ try {
+ applicationClass = (Class<? extends Application>) getClassLoader()
+ .loadClass(applicationClassName);
+ } catch (final ClassNotFoundException e) {
+ throw new ServletException("Failed to load application class: "
+ + applicationClassName);
+ }
+ }
+
+ @Override
+ protected Application getNewApplication(HttpServletRequest request)
+ throws ServletException {
+
+ // Creates a new application instance
+ try {
+ final Application application = getApplicationClass().newInstance();
+
+ return application;
+ } catch (final IllegalAccessException e) {
+ throw new ServletException("getNewApplication failed", e);
+ } catch (final InstantiationException e) {
+ throw new ServletException("getNewApplication failed", e);
+ }
+ }
+
+ @Override
+ protected Class<? extends Application> getApplicationClass() {
+ return applicationClass;
+ }
+} \ No newline at end of file
diff --git a/src/com/vaadin/terminal/gwt/server/ChangeVariablesErrorEvent.java b/src/com/vaadin/terminal/gwt/server/ChangeVariablesErrorEvent.java
new file mode 100644
index 0000000000..62d99a5f02
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/server/ChangeVariablesErrorEvent.java
@@ -0,0 +1,35 @@
+package com.vaadin.terminal.gwt.server;
+
+import java.util.Map;
+
+import com.vaadin.ui.Component;
+import com.vaadin.ui.AbstractComponent.ComponentErrorEvent;
+
+@SuppressWarnings("serial")
+public class ChangeVariablesErrorEvent implements ComponentErrorEvent {
+
+ private Throwable throwable;
+ private Component component;
+
+ private Map variableChanges;
+
+ public ChangeVariablesErrorEvent(Component component, Throwable throwable,
+ Map variableChanges) {
+ this.component = component;
+ this.throwable = throwable;
+ this.variableChanges = variableChanges;
+ }
+
+ public Throwable getThrowable() {
+ return throwable;
+ }
+
+ public Component getComponent() {
+ return component;
+ }
+
+ public Map getVariableChanges() {
+ return variableChanges;
+ }
+
+} \ No newline at end of file
diff --git a/src/com/vaadin/terminal/gwt/server/CommunicationManager.java b/src/com/vaadin/terminal/gwt/server/CommunicationManager.java
new file mode 100644
index 0000000000..c43c857c1a
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/server/CommunicationManager.java
@@ -0,0 +1,1497 @@
+/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.server;
+
+import java.io.BufferedWriter;
+import java.io.CharArrayWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.io.Serializable;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.net.URL;
+import java.security.GeneralSecurityException;
+import java.text.DateFormat;
+import java.text.DateFormatSymbols;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.GregorianCalendar;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+
+import javax.servlet.ServletException;
+import javax.servlet.ServletInputStream;
+import javax.servlet.ServletOutputStream;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import com.itmill.toolkit.external.org.apache.commons.fileupload.FileItemIterator;
+import com.itmill.toolkit.external.org.apache.commons.fileupload.FileItemStream;
+import com.itmill.toolkit.external.org.apache.commons.fileupload.FileUploadException;
+import com.itmill.toolkit.external.org.apache.commons.fileupload.servlet.ServletFileUpload;
+import com.vaadin.Application;
+import com.vaadin.Application.SystemMessages;
+import com.vaadin.terminal.DownloadStream;
+import com.vaadin.terminal.PaintException;
+import com.vaadin.terminal.Paintable;
+import com.vaadin.terminal.URIHandler;
+import com.vaadin.terminal.UploadStream;
+import com.vaadin.terminal.VariableOwner;
+import com.vaadin.terminal.Paintable.RepaintRequestEvent;
+import com.vaadin.terminal.Terminal.ErrorEvent;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.server.ComponentSizeValidator.InvalidLayout;
+import com.vaadin.ui.AbstractField;
+import com.vaadin.ui.Component;
+import com.vaadin.ui.Upload;
+import com.vaadin.ui.Window;
+
+/**
+ * Application manager processes changes and paints for single application
+ * instance.
+ *
+ * @author IT Mill Ltd.
+ * @version
+ * @VERSION@
+ * @since 5.0
+ */
+@SuppressWarnings("serial")
+public class CommunicationManager implements Paintable.RepaintRequestListener,
+ Serializable {
+
+ private static String GET_PARAM_REPAINT_ALL = "repaintAll";
+
+ /* Variable records indexes */
+ private static final int VAR_PID = 1;
+ private static final int VAR_NAME = 2;
+ private static final int VAR_TYPE = 3;
+ private static final int VAR_VALUE = 0;
+
+ private static final String VAR_RECORD_SEPARATOR = "\u001e";
+
+ private static final String VAR_FIELD_SEPARATOR = "\u001f";
+
+ public static final String VAR_BURST_SEPARATOR = "\u001d";
+
+ public static final String VAR_ARRAYITEM_SEPARATOR = "\u001c";
+
+ private final HashSet<String> currentlyOpenWindowsInClient = new HashSet<String>();
+
+ private static final int MAX_BUFFER_SIZE = 64 * 1024;
+
+ private static final String GET_PARAM_ANALYZE_LAYOUTS = "analyzeLayouts";
+
+ private final ArrayList<Paintable> dirtyPaintabletSet = new ArrayList<Paintable>();
+
+ private final HashMap<Paintable, String> paintableIdMap = new HashMap<Paintable, String>();
+
+ private final HashMap<String, Paintable> idPaintableMap = new HashMap<String, Paintable>();
+
+ private int idSequence = 0;
+
+ private final AbstractApplicationServlet applicationServlet;
+
+ private final Application application;
+
+ // Note that this is only accessed from synchronized block and
+ // thus should be thread-safe.
+ private String closingWindowName = null;
+
+ private List<String> locales;
+
+ private int pendingLocalesIndex;
+
+ private int timeoutInterval = -1;
+
+ public CommunicationManager(Application application,
+ AbstractApplicationServlet applicationServlet) {
+ this.application = application;
+ requireLocale(application.getLocale().toString());
+ this.applicationServlet = applicationServlet;
+ }
+
+ /**
+ * Handles file upload request submitted via Upload component.
+ *
+ * @param request
+ * @param response
+ * @throws IOException
+ * @throws FileUploadException
+ */
+ public void handleFileUpload(HttpServletRequest request,
+ HttpServletResponse response) throws IOException,
+ FileUploadException {
+ // Create a new file upload handler
+ final ServletFileUpload upload = new ServletFileUpload();
+
+ final UploadProgressListener pl = new UploadProgressListener();
+
+ upload.setProgressListener(pl);
+
+ // Parse the request
+ FileItemIterator iter;
+
+ try {
+ iter = upload.getItemIterator(request);
+ /*
+ * ATM this loop is run only once as we are uploading one file per
+ * request.
+ */
+ while (iter.hasNext()) {
+ final FileItemStream item = iter.next();
+ final String name = item.getFieldName();
+ final String filename = item.getName();
+ final String mimeType = item.getContentType();
+ final InputStream stream = item.openStream();
+ if (item.isFormField()) {
+ // ignored, upload requests contains only files
+ } else {
+ final String pid = name.split("_")[0];
+ final Upload uploadComponent = (Upload) idPaintableMap
+ .get(pid);
+ if (uploadComponent.isReadOnly()) {
+ throw new FileUploadException(
+ "Warning: ignored file upload because upload component is set as read-only");
+ }
+ if (uploadComponent == null) {
+ throw new FileUploadException(
+ "Upload component not found");
+ }
+ synchronized (application) {
+ // put upload component into receiving state
+ uploadComponent.startUpload();
+ }
+ final UploadStream upstream = new UploadStream() {
+
+ public String getContentName() {
+ return filename;
+ }
+
+ public String getContentType() {
+ return mimeType;
+ }
+
+ public InputStream getStream() {
+ return stream;
+ }
+
+ public String getStreamName() {
+ return "stream";
+ }
+
+ };
+
+ // tell UploadProgressListener which component is receiving
+ // file
+ pl.setUpload(uploadComponent);
+
+ uploadComponent.receiveUpload(upstream);
+ }
+ }
+ } catch (final FileUploadException e) {
+ throw e;
+ }
+
+ // Send short response to acknowledge client that request was done
+ response.setContentType("text/html");
+ final OutputStream out = response.getOutputStream();
+ final PrintWriter outWriter = new PrintWriter(new BufferedWriter(
+ new OutputStreamWriter(out, "UTF-8")));
+ outWriter.print("<html><body>download handled</body></html>");
+ outWriter.flush();
+ out.close();
+ }
+
+ /**
+ * Handles UIDL request
+ *
+ * @param request
+ * @param response
+ * @throws IOException
+ * @throws ServletException
+ */
+ public void handleUidlRequest(HttpServletRequest request,
+ HttpServletResponse response,
+ AbstractApplicationServlet applicationServlet) throws IOException,
+ ServletException, InvalidUIDLSecurityKeyException {
+
+ // repaint requested or session has timed out and new one is created
+ boolean repaintAll = (request.getParameter(GET_PARAM_REPAINT_ALL) != null)
+ || request.getSession().isNew();
+ boolean analyzeLayouts = false;
+ if (repaintAll) {
+ // analyzing can be done only with repaintAll
+ analyzeLayouts = (request.getParameter(GET_PARAM_ANALYZE_LAYOUTS) != null);
+ }
+
+ final OutputStream out = response.getOutputStream();
+ final PrintWriter outWriter = new PrintWriter(new BufferedWriter(
+ new OutputStreamWriter(out, "UTF-8")));
+
+ // The rest of the process is synchronized with the application
+ // in order to guarantee that no parallel variable handling is
+ // made
+ synchronized (application) {
+
+ // Finds the window within the application
+ Window window = null;
+ if (application.isRunning()) {
+ window = getApplicationWindow(request, application, null);
+ // Returns if no window found
+ if (window == null) {
+ // This should not happen, no windows exists but
+ // application is still open.
+ System.err
+ .println("Warning, could not get window for application with request URI "
+ + request.getRequestURI());
+ return;
+ }
+ } else {
+ // application has been closed
+ endApplication(request, response, application);
+ return;
+ }
+
+ // Change all variables based on request parameters
+ if (!handleVariables(request, response, application, window)) {
+
+ // var inconsistency; the client is probably out-of-sync
+ SystemMessages ci = null;
+ try {
+ Method m = application.getClass().getMethod(
+ "getSystemMessages", (Class[]) null);
+ ci = (Application.SystemMessages) m.invoke(null,
+ (Object[]) null);
+ } catch (Exception e2) {
+ // FIXME: Handle exception
+ // Not critical, but something is still wrong; print
+ // stacktrace
+ e2.printStackTrace();
+ }
+ if (ci != null) {
+ String msg = ci.getOutOfSyncMessage();
+ String cap = ci.getOutOfSyncCaption();
+ if (msg != null || cap != null) {
+ applicationServlet.criticalNotification(request,
+ response, cap, msg, ci.getOutOfSyncURL());
+ // will reload page after this
+ return;
+ }
+ }
+ // No message to show, let's just repaint all.
+ repaintAll = true;
+
+ }
+
+ paintAfterVariablechanges(request, response, applicationServlet,
+ repaintAll, outWriter, window, analyzeLayouts);
+
+ // Mark this window to be open on client
+ currentlyOpenWindowsInClient.add(window.getName());
+ if (closingWindowName != null) {
+ currentlyOpenWindowsInClient.remove(closingWindowName);
+ closingWindowName = null;
+ }
+ }
+
+ out.flush();
+ out.close();
+ }
+
+ private void paintAfterVariablechanges(HttpServletRequest request,
+ HttpServletResponse response,
+ AbstractApplicationServlet applicationServlet, boolean repaintAll,
+ final PrintWriter outWriter, Window window, boolean analyzeLayouts)
+ throws IOException, ServletException, PaintException {
+
+ // If repaint is requested, clean all ids in this root window
+ if (repaintAll) {
+ for (final Iterator it = idPaintableMap.keySet().iterator(); it
+ .hasNext();) {
+ final Component c = (Component) idPaintableMap.get(it.next());
+ if (isChildOf(window, c)) {
+ it.remove();
+ paintableIdMap.remove(c);
+ }
+ }
+ }
+
+ // Removes application if it has stopped during variable changes
+ if (!application.isRunning()) {
+ endApplication(request, response, application);
+ return;
+ }
+
+ // Sets the response type
+ response.setContentType("application/json; charset=UTF-8");
+ // some dirt to prevent cross site scripting
+ outWriter.print("for(;;);[{");
+
+ outWriter.print("\"changes\":[");
+
+ ArrayList<Paintable> paintables = null;
+
+ // If the browser-window has been closed - we do not need to paint it at
+ // all
+ if (!window.getName().equals(closingWindowName)) {
+
+ List<InvalidLayout> invalidComponentRelativeSizes = null;
+
+ // re-get window - may have been changed
+ Window newWindow = getApplicationWindow(request, application,
+ window);
+ if (newWindow != window) {
+ window = newWindow;
+ repaintAll = true;
+ }
+
+ JsonPaintTarget paintTarget = new JsonPaintTarget(this, outWriter,
+ !repaintAll);
+
+ // Paints components
+ if (repaintAll) {
+ paintables = new ArrayList<Paintable>();
+ paintables.add(window);
+
+ // Reset sent locales
+ locales = null;
+ requireLocale(application.getLocale().toString());
+
+ } else {
+ // remove detached components from paintableIdMap so they
+ // can be GC'ed
+ for (Iterator it = paintableIdMap.keySet().iterator(); it
+ .hasNext();) {
+ Component p = (Component) it.next();
+ if (p.getApplication() == null) {
+ idPaintableMap.remove(paintableIdMap.get(p));
+ it.remove();
+ dirtyPaintabletSet.remove(p);
+ p.removeListener(this);
+ }
+ }
+ paintables = getDirtyVisibleComponents(window);
+ }
+ if (paintables != null) {
+
+ // We need to avoid painting children before parent.
+ // This is ensured by ordering list by depth in component
+ // tree
+ Collections.sort(paintables, new Comparator<Paintable>() {
+ public int compare(Paintable o1, Paintable o2) {
+ Component c1 = (Component) o1;
+ Component c2 = (Component) o2;
+ int d1 = 0;
+ while (c1.getParent() != null) {
+ d1++;
+ c1 = c1.getParent();
+ }
+ int d2 = 0;
+ while (c2.getParent() != null) {
+ d2++;
+ c2 = c2.getParent();
+ }
+ if (d1 < d2) {
+ return -1;
+ }
+ if (d1 > d2) {
+ return 1;
+ }
+ return 0;
+ }
+ });
+
+ for (final Iterator i = paintables.iterator(); i.hasNext();) {
+ final Paintable p = (Paintable) i.next();
+
+ // TODO CLEAN
+ if (p instanceof Window) {
+ final Window w = (Window) p;
+ if (w.getTerminal() == null) {
+ w.setTerminal(application.getMainWindow()
+ .getTerminal());
+ }
+ }
+ /*
+ * This does not seem to happen in tk5, but remember this
+ * case: else if (p instanceof Component) { if (((Component)
+ * p).getParent() == null || ((Component)
+ * p).getApplication() == null) { // Component requested
+ * repaint, but is no // longer attached: skip
+ * paintablePainted(p); continue; } }
+ */
+
+ // TODO we may still get changes that have been
+ // rendered already (changes with only cached flag)
+ if (paintTarget.needsToBePainted(p)) {
+ paintTarget.startTag("change");
+ paintTarget.addAttribute("format", "uidl");
+ final String pid = getPaintableId(p);
+ paintTarget.addAttribute("pid", pid);
+
+ p.paint(paintTarget);
+
+ paintTarget.endTag("change");
+ }
+ paintablePainted(p);
+
+ if (analyzeLayouts) {
+ invalidComponentRelativeSizes = ComponentSizeValidator
+ .validateComponentRelativeSizes(((Window) p)
+ .getLayout(), null, null);
+ }
+ }
+ }
+
+ paintTarget.close();
+ outWriter.print("]"); // close changes
+
+ outWriter.print(", \"meta\" : {");
+ boolean metaOpen = false;
+
+ if (repaintAll) {
+ metaOpen = true;
+ outWriter.write("\"repaintAll\":true");
+ if (analyzeLayouts) {
+ outWriter.write(", \"invalidLayouts\":");
+ outWriter.write("[");
+ if (invalidComponentRelativeSizes != null) {
+ boolean first = true;
+ for (InvalidLayout invalidLayout : invalidComponentRelativeSizes) {
+ if (!first) {
+ outWriter.write(",");
+ } else {
+ first = false;
+ }
+ invalidLayout.reportErrors(outWriter, this,
+ System.err);
+ }
+ }
+ outWriter.write("]");
+ }
+ }
+
+ SystemMessages ci = null;
+ try {
+ Method m = application.getClass().getMethod(
+ "getSystemMessages", (Class[]) null);
+ ci = (Application.SystemMessages) m.invoke(null,
+ (Object[]) null);
+ } catch (NoSuchMethodException e1) {
+ e1.printStackTrace();
+ } catch (IllegalArgumentException e) {
+ e.printStackTrace();
+ } catch (IllegalAccessException e) {
+ e.printStackTrace();
+ } catch (InvocationTargetException e) {
+ e.printStackTrace();
+ }
+
+ // meta instruction for client to enable auto-forward to
+ // sessionExpiredURL after timer expires.
+ if (ci != null && ci.getSessionExpiredMessage() == null
+ && ci.getSessionExpiredCaption() == null
+ && ci.isSessionExpiredNotificationEnabled()) {
+ int newTimeoutInterval = request.getSession()
+ .getMaxInactiveInterval();
+ if (repaintAll || (timeoutInterval != newTimeoutInterval)) {
+ String escapedURL = ci.getSessionExpiredURL() == null ? ""
+ : ci.getSessionExpiredURL().replace("/", "\\/");
+ if (metaOpen) {
+ outWriter.write(",");
+ }
+ outWriter.write("\"timedRedirect\":{\"interval\":"
+ + (newTimeoutInterval + 15) + ",\"url\":\""
+ + escapedURL + "\"}");
+ metaOpen = true;
+ }
+ timeoutInterval = newTimeoutInterval;
+ }
+
+ outWriter.print("}, \"resources\" : {");
+
+ // Precache custom layouts
+ String themeName = window.getTheme();
+ if (request.getParameter("theme") != null) {
+ themeName = request.getParameter("theme");
+ }
+ if (themeName == null) {
+ themeName = "default";
+ }
+
+ // TODO We should only precache the layouts that are not
+ // cached already
+ int resourceIndex = 0;
+ for (final Iterator i = paintTarget.getPreCachedResources()
+ .iterator(); i.hasNext();) {
+ final String resource = (String) i.next();
+ InputStream is = null;
+ try {
+ is = applicationServlet
+ .getServletContext()
+ .getResourceAsStream(
+ "/"
+ + ApplicationServlet.THEME_DIRECTORY_PATH
+ + themeName + "/" + resource);
+ } catch (final Exception e) {
+ // FIXME: Handle exception
+ e.printStackTrace();
+ }
+ if (is != null) {
+
+ outWriter.print((resourceIndex++ > 0 ? ", " : "") + "\""
+ + resource + "\" : ");
+ final StringBuffer layout = new StringBuffer();
+
+ try {
+ final InputStreamReader r = new InputStreamReader(is,
+ "UTF-8");
+ final char[] buffer = new char[20000];
+ int charsRead = 0;
+ while ((charsRead = r.read(buffer)) > 0) {
+ layout.append(buffer, 0, charsRead);
+ }
+ r.close();
+ } catch (final java.io.IOException e) {
+ // FIXME: Handle exception
+ System.err.println("Resource transfer failed: "
+ + request.getRequestURI() + ". ("
+ + e.getMessage() + ")");
+ }
+ outWriter.print("\""
+ + JsonPaintTarget.escapeJSON(layout.toString())
+ + "\"");
+ } else {
+ // FIXME: Handle exception
+ System.err.println("CustomLayout " + "/"
+ + ApplicationServlet.THEME_DIRECTORY_PATH
+ + themeName + "/" + resource + " not found!");
+ }
+ }
+ outWriter.print("}");
+
+ printLocaleDeclarations(outWriter);
+
+ outWriter.print("}]");
+ }
+ outWriter.flush();
+ outWriter.close();
+
+ }
+
+ /**
+ * If this method returns false, something was submitted that we did not
+ * expect; this is probably due to the client being out-of-sync and sending
+ * variable changes for non-existing pids
+ *
+ * @param request
+ * @param application2
+ * @return true if successful, false if there was an inconsistency
+ * @throws IOException
+ */
+ private boolean handleVariables(HttpServletRequest request,
+ HttpServletResponse response, Application application2,
+ Window window) throws IOException, InvalidUIDLSecurityKeyException {
+ boolean success = true;
+
+ if (request.getContentLength() > 0) {
+ String changes = readRequest(request);
+
+ // Manage bursts one by one
+ final String[] bursts = changes.split(VAR_BURST_SEPARATOR);
+
+ // Security: double cookie submission pattern unless disabled by
+ // property
+ if (!"true".equals(application2
+ .getProperty("disable-xsrf-protection"))) {
+ if (bursts.length == 1 && "init".equals(bursts[0])) {
+ // initial request, no variable changes: send key
+ String seckey = (String) request.getSession().getAttribute(
+ ApplicationConnection.UIDL_SECURITY_HEADER);
+ if (seckey == null) {
+ seckey = "" + (int) (Math.random() * 1000000);
+ }
+ /*
+ * Cookie c = new Cookie(
+ * ApplicationConnection.UIDL_SECURITY_COOKIE_NAME, uuid);
+ * response.addCookie(c);
+ */
+ response.setHeader(
+ ApplicationConnection.UIDL_SECURITY_HEADER, seckey);
+ request.getSession().setAttribute(
+ ApplicationConnection.UIDL_SECURITY_HEADER, seckey);
+ return true;
+ } else {
+ // check the key
+ String sessId = (String) request.getSession().getAttribute(
+ ApplicationConnection.UIDL_SECURITY_HEADER);
+ if (sessId == null || !sessId.equals(bursts[0])) {
+ throw new InvalidUIDLSecurityKeyException(
+ "Security key mismatch");
+ }
+ }
+ }
+
+ for (int bi = 1; bi < bursts.length; bi++) {
+
+ // extract variables to two dim string array
+ final String[] tmp = bursts[bi].split(VAR_RECORD_SEPARATOR);
+ final String[][] variableRecords = new String[tmp.length][4];
+ for (int i = 0; i < tmp.length; i++) {
+ variableRecords[i] = tmp[i].split(VAR_FIELD_SEPARATOR);
+ }
+
+ for (int i = 0; i < variableRecords.length; i++) {
+ String[] variable = variableRecords[i];
+ String[] nextVariable = null;
+ if (i + 1 < variableRecords.length) {
+ nextVariable = variableRecords[i + 1];
+ }
+ final VariableOwner owner = (VariableOwner) idPaintableMap
+ .get(variable[VAR_PID]);
+ if (owner != null && owner.isEnabled()) {
+ Map m;
+ if (nextVariable != null
+ && variable[VAR_PID]
+ .equals(nextVariable[VAR_PID])) {
+ // we have more than one value changes in row for
+ // one variable owner, collect em in HashMap
+ m = new HashMap();
+ m.put(variable[VAR_NAME], convertVariableValue(
+ variable[VAR_TYPE].charAt(0),
+ variable[VAR_VALUE]));
+ } else {
+ // use optimized single value map
+ m = new SingleValueMap(variable[VAR_NAME],
+ convertVariableValue(variable[VAR_TYPE]
+ .charAt(0), variable[VAR_VALUE]));
+ }
+
+ // collect following variable changes for this owner
+ while (nextVariable != null
+ && variable[VAR_PID]
+ .equals(nextVariable[VAR_PID])) {
+ i++;
+ variable = nextVariable;
+ if (i + 1 < variableRecords.length) {
+ nextVariable = variableRecords[i + 1];
+ } else {
+ nextVariable = null;
+ }
+ m.put(variable[VAR_NAME], convertVariableValue(
+ variable[VAR_TYPE].charAt(0),
+ variable[VAR_VALUE]));
+ }
+ try {
+ owner.changeVariables(request, m);
+
+ // Special-case of closing browser-level windows:
+ // track browser-windows currently open in client
+ if (owner instanceof Window
+ && ((Window) owner).getParent() == null) {
+ final Boolean close = (Boolean) m.get("close");
+ if (close != null && close.booleanValue()) {
+ closingWindowName = ((Window) owner)
+ .getName();
+ }
+ }
+ } catch (Exception e) {
+ handleChangeVariablesError(application2,
+ (Component) owner, e, m);
+ }
+ } else {
+
+ // Handle special case where window-close is called
+ // after the window has been removed from the
+ // application or the application has closed
+ if ("close".equals(variable[VAR_NAME])
+ && "true".equals(variable[VAR_VALUE])) {
+ // Silently ignore this
+ continue;
+ }
+
+ // Ignore variable change
+ String msg = "Warning: Ignoring variable change for ";
+ if (owner != null) {
+ msg += "disabled component " + owner.getClass();
+ String caption = ((Component) owner).getCaption();
+ if (caption != null) {
+ msg += ", caption=" + caption;
+ }
+ } else {
+ msg += "non-existent component, VAR_PID="
+ + variable[VAR_PID];
+ success = false;
+ }
+ System.err.println(msg);
+ continue;
+ }
+ }
+
+ // In case that there were multiple bursts, we know that this is
+ // a special synchronous case for closing window. Thus we are
+ // not interested in sending any UIDL changes back to client.
+ // Still we must clear component tree between bursts to ensure
+ // that no removed components are updated. The painting after
+ // the last burst is handled normally by the calling method.
+ if (bi < bursts.length - 1) {
+
+ // We will be discarding all changes
+ final PrintWriter outWriter = new PrintWriter(
+ new CharArrayWriter());
+ try {
+ paintAfterVariablechanges(request, response,
+ applicationServlet, true, outWriter, window,
+ false);
+ } catch (ServletException e) {
+ // We will ignore all servlet exceptions
+ }
+ }
+
+ }
+ }
+ return success;
+ }
+
+ /**
+ * Reads the request data from the HttpServletRequest and returns it
+ * converted to an UTF-8 string.
+ *
+ * @param request
+ * @return
+ * @throws IOException
+ */
+ private static String readRequest(HttpServletRequest request)
+ throws IOException {
+
+ int requestLength = request.getContentLength();
+
+ byte[] buffer = new byte[requestLength];
+ ServletInputStream inputStream = request.getInputStream();
+
+ int bytesRemaining = requestLength;
+ while (bytesRemaining > 0) {
+ int bytesToRead = Math.min(bytesRemaining, MAX_BUFFER_SIZE);
+ int bytesRead = inputStream.read(buffer, requestLength
+ - bytesRemaining, bytesToRead);
+ if (bytesRead == -1) {
+ break;
+ }
+
+ bytesRemaining -= bytesRead;
+ }
+
+ String result = new String(buffer, "utf-8");
+
+ return result;
+ }
+
+ public class ErrorHandlerErrorEvent implements ErrorEvent, Serializable {
+ private final Throwable throwable;
+
+ public ErrorHandlerErrorEvent(Throwable throwable) {
+ this.throwable = throwable;
+ }
+
+ public Throwable getThrowable() {
+ return throwable;
+ }
+
+ }
+
+ private void handleChangeVariablesError(Application application,
+ Component owner, Exception e, Map m) {
+ boolean handled = false;
+ ChangeVariablesErrorEvent errorEvent = new ChangeVariablesErrorEvent(
+ owner, e, m);
+
+ if (owner instanceof AbstractField) {
+ try {
+ handled = ((AbstractField) owner).handleError(errorEvent);
+ } catch (Exception handlerException) {
+ /*
+ * If there is an error in the component error handler we pass
+ * the that error to the application error handler and continue
+ * processing the actual error
+ */
+ application.getErrorHandler().terminalError(
+ new ErrorHandlerErrorEvent(handlerException));
+ handled = false;
+ }
+ }
+
+ if (!handled) {
+ application.getErrorHandler().terminalError(errorEvent);
+ }
+
+ }
+
+ private Object convertVariableValue(char variableType, String strValue) {
+ Object val = null;
+ switch (variableType) {
+ case 'a':
+ val = strValue.split(VAR_ARRAYITEM_SEPARATOR);
+ break;
+ case 's':
+ val = strValue;
+ break;
+ case 'i':
+ val = Integer.valueOf(strValue);
+ break;
+ case 'l':
+ val = Long.valueOf(strValue);
+ break;
+ case 'f':
+ val = Float.valueOf(strValue);
+ break;
+ case 'd':
+ val = Double.valueOf(strValue);
+ break;
+ case 'b':
+ val = Boolean.valueOf(strValue);
+ break;
+ case 'p':
+ val = idPaintableMap.get(strValue);
+ break;
+ }
+
+ return val;
+ }
+
+ private void printLocaleDeclarations(PrintWriter outWriter) {
+ /*
+ * ----------------------------- Sending Locale sensitive date
+ * -----------------------------
+ */
+
+ // Send locale informations to client
+ outWriter.print(", \"locales\":[");
+ for (; pendingLocalesIndex < locales.size(); pendingLocalesIndex++) {
+
+ final Locale l = generateLocale(locales.get(pendingLocalesIndex));
+ // Locale name
+ outWriter.print("{\"name\":\"" + l.toString() + "\",");
+
+ /*
+ * Month names (both short and full)
+ */
+ final DateFormatSymbols dfs = new DateFormatSymbols(l);
+ final String[] short_months = dfs.getShortMonths();
+ final String[] months = dfs.getMonths();
+ outWriter.print("\"smn\":[\""
+ + // ShortMonthNames
+ short_months[0] + "\",\"" + short_months[1] + "\",\""
+ + short_months[2] + "\",\"" + short_months[3] + "\",\""
+ + short_months[4] + "\",\"" + short_months[5] + "\",\""
+ + short_months[6] + "\",\"" + short_months[7] + "\",\""
+ + short_months[8] + "\",\"" + short_months[9] + "\",\""
+ + short_months[10] + "\",\"" + short_months[11] + "\""
+ + "],");
+ outWriter.print("\"mn\":[\""
+ + // MonthNames
+ months[0] + "\",\"" + months[1] + "\",\"" + months[2]
+ + "\",\"" + months[3] + "\",\"" + months[4] + "\",\""
+ + months[5] + "\",\"" + months[6] + "\",\"" + months[7]
+ + "\",\"" + months[8] + "\",\"" + months[9] + "\",\""
+ + months[10] + "\",\"" + months[11] + "\"" + "],");
+
+ /*
+ * Weekday names (both short and full)
+ */
+ final String[] short_days = dfs.getShortWeekdays();
+ final String[] days = dfs.getWeekdays();
+ outWriter.print("\"sdn\":[\""
+ + // ShortDayNames
+ short_days[1] + "\",\"" + short_days[2] + "\",\""
+ + short_days[3] + "\",\"" + short_days[4] + "\",\""
+ + short_days[5] + "\",\"" + short_days[6] + "\",\""
+ + short_days[7] + "\"" + "],");
+ outWriter.print("\"dn\":[\""
+ + // DayNames
+ days[1] + "\",\"" + days[2] + "\",\"" + days[3] + "\",\""
+ + days[4] + "\",\"" + days[5] + "\",\"" + days[6] + "\",\""
+ + days[7] + "\"" + "],");
+
+ /*
+ * First day of week (0 = sunday, 1 = monday)
+ */
+ final Calendar cal = new GregorianCalendar(l);
+ outWriter.print("\"fdow\":" + (cal.getFirstDayOfWeek() - 1) + ",");
+
+ /*
+ * Date formatting (MM/DD/YYYY etc.)
+ */
+
+ DateFormat dateFormat = DateFormat.getDateTimeInstance(
+ DateFormat.SHORT, DateFormat.SHORT, l);
+ if (!(dateFormat instanceof SimpleDateFormat)) {
+ System.err
+ .println("Unable to get default date pattern for locale "
+ + l.toString());
+ dateFormat = new SimpleDateFormat();
+ }
+ final String df = ((SimpleDateFormat) dateFormat).toPattern();
+
+ int timeStart = df.indexOf("H");
+ if (timeStart < 0) {
+ timeStart = df.indexOf("h");
+ }
+ final int ampm_first = df.indexOf("a");
+ // E.g. in Korean locale AM/PM is before h:mm
+ // TODO should take that into consideration on client-side as well,
+ // now always h:mm a
+ if (ampm_first > 0 && ampm_first < timeStart) {
+ timeStart = ampm_first;
+ }
+ final String dateformat = df.substring(0, timeStart - 1);
+
+ outWriter.print("\"df\":\"" + dateformat.trim() + "\",");
+
+ /*
+ * Time formatting (24 or 12 hour clock and AM/PM suffixes)
+ */
+ final String timeformat = df.substring(timeStart, df.length());
+ /*
+ * Doesn't return second or milliseconds.
+ *
+ * We use timeformat to determine 12/24-hour clock
+ */
+ final boolean twelve_hour_clock = timeformat.indexOf("a") > -1;
+ // TODO there are other possibilities as well, like 'h' in french
+ // (ignore them, too complicated)
+ final String hour_min_delimiter = timeformat.indexOf(".") > -1 ? "."
+ : ":";
+ // outWriter.print("\"tf\":\"" + timeformat + "\",");
+ outWriter.print("\"thc\":" + twelve_hour_clock + ",");
+ outWriter.print("\"hmd\":\"" + hour_min_delimiter + "\"");
+ if (twelve_hour_clock) {
+ final String[] ampm = dfs.getAmPmStrings();
+ outWriter.print(",\"ampm\":[\"" + ampm[0] + "\",\"" + ampm[1]
+ + "\"]");
+ }
+ outWriter.print("}");
+ if (pendingLocalesIndex < locales.size() - 1) {
+ outWriter.print(",");
+ }
+ }
+ outWriter.print("]"); // Close locales
+ }
+
+ /**
+ * Gets the existing application or create a new one. Get a window within an
+ * application based on the requested URI.
+ *
+ * @param request
+ * the HTTP Request.
+ * @param application
+ * the Application to query for window.
+ * @param assumedWindow
+ * if the window has been already resolved once, this parameter
+ * must contain the window.
+ * @return Window mathing the given URI or null if not found.
+ * @throws ServletException
+ * if an exception has occurred that interferes with the
+ * servlet's normal operation.
+ */
+ private Window getApplicationWindow(HttpServletRequest request,
+ Application application, Window assumedWindow)
+ throws ServletException {
+
+ Window window = null;
+
+ // If the client knows which window to use, use it if possible
+ String windowClientRequestedName = request.getParameter("windowName");
+ if (assumedWindow != null
+ && application.getWindows().contains(assumedWindow)) {
+ windowClientRequestedName = assumedWindow.getName();
+ }
+ if (windowClientRequestedName != null) {
+ window = application.getWindow(windowClientRequestedName);
+ if (window != null) {
+ return window;
+ }
+ }
+
+ // If client does not know what window it wants
+ if (window == null) {
+
+ // Get the path from URL
+ String path = applicationServlet.getRequestPathInfo(request);
+ path = path.substring("/UIDL".length());
+
+ // If the path is specified, create name from it
+ if (path != null && path.length() > 0 && !path.equals("/")) {
+ String windowUrlName = null;
+ if (path.charAt(0) == '/') {
+ path = path.substring(1);
+ }
+ final int index = path.indexOf('/');
+ if (index < 0) {
+ windowUrlName = path;
+ path = "";
+ } else {
+ windowUrlName = path.substring(0, index);
+ path = path.substring(index + 1);
+ }
+
+ window = application.getWindow(windowUrlName);
+ }
+ }
+
+ // By default, use mainwindow
+ if (window == null) {
+ window = application.getMainWindow();
+ }
+
+ // If the requested window is already open, resolve conflict
+ if (currentlyOpenWindowsInClient.contains(window.getName())) {
+ String newWindowName = window.getName();
+ while (currentlyOpenWindowsInClient.contains(newWindowName)) {
+ newWindowName = window.getName() + "_"
+ + ((int) (Math.random() * 100000000));
+ }
+
+ window = application.getWindow(newWindowName);
+
+ // If everything else fails, use main window even in case of
+ // conflicts
+ if (window == null) {
+ window = application.getMainWindow();
+ }
+ }
+
+ return window;
+ }
+
+ /**
+ * Ends the Application.
+ *
+ * @param request
+ * the HTTP request instance.
+ * @param response
+ * the HTTP response to write to.
+ * @param application
+ * the Application to end.
+ * @throws IOException
+ * if the writing failed due to input/output error.
+ */
+ private void endApplication(HttpServletRequest request,
+ HttpServletResponse response, Application application)
+ throws IOException {
+
+ String logoutUrl = application.getLogoutURL();
+ if (logoutUrl == null) {
+ logoutUrl = application.getURL().toString();
+ }
+ // clients JS app is still running, send a special json file to tell
+ // client that application has quit and where to point browser now
+ // Set the response type
+ response.setContentType("application/json; charset=UTF-8");
+ final ServletOutputStream out = response.getOutputStream();
+ final PrintWriter outWriter = new PrintWriter(new BufferedWriter(
+ new OutputStreamWriter(out, "UTF-8")));
+ outWriter.print("for(;;);[{");
+ outWriter.print("\"redirect\":{");
+ outWriter.write("\"url\":\"" + logoutUrl + "\"}}]");
+ outWriter.flush();
+ outWriter.close();
+ out.flush();
+ }
+
+ /**
+ * Gets the Paintable Id. If Paintable has debug id set it will be used
+ * prefixed with "PID_S". Otherwise a sequenced ID is created.
+ *
+ * @param paintable
+ * @return the paintable Id.
+ */
+ public String getPaintableId(Paintable paintable) {
+
+ String id = paintableIdMap.get(paintable);
+ if (id == null) {
+ // use testing identifier as id if set
+ id = paintable.getDebugId();
+ if (id == null) {
+ id = "PID" + Integer.toString(idSequence++);
+ } else {
+ id = "PID_S" + id;
+ }
+ Paintable old = idPaintableMap.put(id, paintable);
+ if (old != null && old != paintable) {
+ /*
+ * Two paintables have the same id. We still make sure the old
+ * one is a component which is still attached to the
+ * application. This is just a precaution and should not be
+ * absolutely necessary.
+ */
+
+ if (old instanceof Component
+ && ((Component) old).getApplication() != null) {
+ throw new IllegalStateException("Two paintables ("
+ + paintable.getClass().getSimpleName() + ","
+ + old.getClass().getSimpleName()
+ + ") have been assigned the same id: "
+ + paintable.getDebugId());
+ }
+ }
+ paintableIdMap.put(paintable, id);
+ }
+
+ return id;
+ }
+
+ public boolean hasPaintableId(Paintable paintable) {
+ return paintableIdMap.containsKey(paintable);
+ }
+
+ /**
+ * Returns dirty components which are in given window. Components in an
+ * invisible subtrees are omitted.
+ *
+ * @param w
+ * root window for which dirty components is to be fetched
+ * @return
+ */
+ private ArrayList<Paintable> getDirtyVisibleComponents(Window w) {
+ final ArrayList<Paintable> resultset = new ArrayList<Paintable>(
+ dirtyPaintabletSet);
+
+ // The following algorithm removes any components that would be painted
+ // as a direct descendant of other components from the dirty components
+ // list. The result is that each component should be painted exactly
+ // once and any unmodified components will be painted as "cached=true".
+
+ for (final Iterator i = dirtyPaintabletSet.iterator(); i.hasNext();) {
+ final Paintable p = (Paintable) i.next();
+ if (p instanceof Component) {
+ final Component component = (Component) p;
+ if (component.getApplication() == null) {
+ // component is detached after requestRepaint is called
+ resultset.remove(p);
+ i.remove();
+ } else {
+ Window componentsRoot = component.getWindow();
+ if (componentsRoot.getParent() != null) {
+ // this is a subwindow
+ componentsRoot = (Window) componentsRoot.getParent();
+ }
+ if (componentsRoot != w) {
+ resultset.remove(p);
+ } else if (component.getParent() != null
+ && !component.getParent().isVisible()) {
+ /*
+ * Do not return components in an invisible subtree.
+ *
+ * Components that are invisible in visible subree, must
+ * be rendered (to let client know that they need to be
+ * hidden).
+ */
+ resultset.remove(p);
+ }
+ }
+ }
+ }
+
+ return resultset;
+ }
+
+ /**
+ * @see com.vaadin.terminal.Paintable.RepaintRequestListener#repaintRequested(com.vaadin.terminal.Paintable.RepaintRequestEvent)
+ */
+ public void repaintRequested(RepaintRequestEvent event) {
+ final Paintable p = event.getPaintable();
+ if (!dirtyPaintabletSet.contains(p)) {
+ dirtyPaintabletSet.add(p);
+ }
+ }
+
+ /**
+ *
+ * @param p
+ */
+ private void paintablePainted(Paintable p) {
+ dirtyPaintabletSet.remove(p);
+ p.requestRepaintRequests();
+ }
+
+ private final class SingleValueMap implements Map<Object, Object>,
+ Serializable {
+
+ private final String name;
+
+ private final Object value;
+
+ private SingleValueMap(String name, Object value) {
+ this.name = name;
+ this.value = value;
+ }
+
+ public void clear() {
+ throw new UnsupportedOperationException();
+ }
+
+ public boolean containsKey(Object key) {
+ if (name == null) {
+ return key == null;
+ }
+ return name.equals(key);
+ }
+
+ public boolean containsValue(Object v) {
+ if (value == null) {
+ return v == null;
+ }
+ return value.equals(v);
+ }
+
+ public Set entrySet() {
+ final Set s = new HashSet();
+ s.add(new Map.Entry() {
+
+ public Object getKey() {
+ return name;
+ }
+
+ public Object getValue() {
+ return value;
+ }
+
+ public Object setValue(Object value) {
+ throw new UnsupportedOperationException();
+ }
+ });
+ return s;
+ }
+
+ public Object get(Object key) {
+ if (!name.equals(key)) {
+ return null;
+ }
+ return value;
+ }
+
+ public boolean isEmpty() {
+ return false;
+ }
+
+ public Set keySet() {
+ final Set s = new HashSet();
+ s.add(name);
+ return s;
+ }
+
+ public Object put(Object key, Object value) {
+ throw new UnsupportedOperationException();
+ }
+
+ public void putAll(Map t) {
+ throw new UnsupportedOperationException();
+ }
+
+ public Object remove(Object key) {
+ throw new UnsupportedOperationException();
+ }
+
+ public int size() {
+ return 1;
+ }
+
+ public Collection values() {
+ final LinkedList s = new LinkedList();
+ s.add(value);
+ return s;
+
+ }
+ }
+
+ /**
+ * Implementation of URIHandler.ErrorEvent interface.
+ */
+ public class URIHandlerErrorImpl implements URIHandler.ErrorEvent,
+ Serializable {
+
+ private final URIHandler owner;
+
+ private final Throwable throwable;
+
+ /**
+ *
+ * @param owner
+ * @param throwable
+ */
+ private URIHandlerErrorImpl(URIHandler owner, Throwable throwable) {
+ this.owner = owner;
+ this.throwable = throwable;
+ }
+
+ /**
+ * @see com.vaadin.terminal.Terminal.ErrorEvent#getThrowable()
+ */
+ public Throwable getThrowable() {
+ return throwable;
+ }
+
+ /**
+ * @see com.vaadin.terminal.URIHandler.ErrorEvent#getURIHandler()
+ */
+ public URIHandler getURIHandler() {
+ return owner;
+ }
+ }
+
+ public void requireLocale(String value) {
+ if (locales == null) {
+ locales = new ArrayList<String>();
+ locales.add(application.getLocale().toString());
+ pendingLocalesIndex = 0;
+ }
+ if (!locales.contains(value)) {
+ locales.add(value);
+ }
+ }
+
+ private Locale generateLocale(String value) {
+ final String[] temp = value.split("_");
+ if (temp.length == 1) {
+ return new Locale(temp[0]);
+ } else if (temp.length == 2) {
+ return new Locale(temp[0], temp[1]);
+ } else {
+ return new Locale(temp[0], temp[1], temp[2]);
+ }
+ }
+
+ /*
+ * Upload progress listener notifies upload component once when Jakarta
+ * FileUpload can determine content length. Used to detect files total size,
+ * uploads progress can be tracked inside upload.
+ */
+ private class UploadProgressListener implements ProgressListener,
+ Serializable {
+
+ Upload uploadComponent;
+
+ boolean updated = false;
+
+ public void setUpload(Upload u) {
+ uploadComponent = u;
+ }
+
+ public void update(long bytesRead, long contentLength, int items) {
+ if (!updated && uploadComponent != null) {
+ uploadComponent.setUploadSize(contentLength);
+ updated = true;
+ }
+ }
+ }
+
+ /**
+ * Helper method to test if a component contains another
+ *
+ * @param parent
+ * @param child
+ */
+ private static boolean isChildOf(Component parent, Component child) {
+ Component p = child.getParent();
+ while (p != null) {
+ if (parent == p) {
+ return true;
+ }
+ p = p.getParent();
+ }
+ return false;
+ }
+
+ private class InvalidUIDLSecurityKeyException extends
+ GeneralSecurityException {
+
+ InvalidUIDLSecurityKeyException(String message) {
+ super(message);
+ }
+
+ }
+
+ /**
+ * Handles the requested URI. An application can add handlers to do special
+ * processing, when a certain URI is requested. The handlers are invoked
+ * before any windows URIs are processed and if a DownloadStream is returned
+ * it is sent to the client.
+ *
+ * @param application
+ * the Application owning the URI.
+ * @param request
+ * the HTTP request instance.
+ * @param response
+ * the HTTP response to write to.
+ * @return boolean <code>true</code> if the request was handled and further
+ * processing should be suppressed, <code>false</code> otherwise.
+ * @see com.vaadin.terminal.URIHandler
+ */
+ DownloadStream handleURI(Window window, HttpServletRequest request,
+ HttpServletResponse response) {
+
+ String uri = applicationServlet.getRequestPathInfo(request);
+
+ // If no URI is available
+ if (uri == null) {
+ uri = "";
+ } else {
+ // Removes the leading /
+ while (uri.startsWith("/") && uri.length() > 0) {
+ uri = uri.substring(1);
+ }
+ }
+
+ // Handles the uri
+ try {
+ URL context = application.getURL();
+ if (window == application.getMainWindow()) {
+ DownloadStream stream = null;
+ /*
+ * Application.handleURI run first. Handles possible
+ * ApplicationResources.
+ */
+ stream = application.handleURI(context, uri);
+ if (stream == null) {
+ stream = window.handleURI(context, uri);
+ }
+ return stream;
+ } else {
+ // Resolve the prefix end inded
+ final int index = uri.indexOf('/');
+ if (index > 0) {
+ String prefix = uri.substring(0, index);
+ URL windowContext;
+ windowContext = new URL(context, prefix + "/");
+ final String windowUri = (uri.length() > prefix.length() + 1) ? uri
+ .substring(prefix.length() + 1)
+ : "";
+ return window.handleURI(windowContext, windowUri);
+ } else {
+ return null;
+ }
+ }
+
+ } catch (final Throwable t) {
+ application.getErrorHandler().terminalError(
+ new URIHandlerErrorImpl(application, t));
+ return null;
+ }
+ }
+}
diff --git a/src/com/vaadin/terminal/gwt/server/ComponentSizeValidator.java b/src/com/vaadin/terminal/gwt/server/ComponentSizeValidator.java
new file mode 100644
index 0000000000..045b9c180d
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/server/ComponentSizeValidator.java
@@ -0,0 +1,678 @@
+package com.vaadin.terminal.gwt.server;
+
+import java.io.PrintStream;
+import java.io.PrintWriter;
+import java.io.Serializable;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Stack;
+import java.util.Vector;
+
+import com.vaadin.terminal.Sizeable;
+import com.vaadin.ui.AbstractOrderedLayout;
+import com.vaadin.ui.Component;
+import com.vaadin.ui.ComponentContainer;
+import com.vaadin.ui.CustomComponent;
+import com.vaadin.ui.Form;
+import com.vaadin.ui.GridLayout;
+import com.vaadin.ui.Layout;
+import com.vaadin.ui.OrderedLayout;
+import com.vaadin.ui.Panel;
+import com.vaadin.ui.SplitPanel;
+import com.vaadin.ui.TabSheet;
+import com.vaadin.ui.VerticalLayout;
+import com.vaadin.ui.Window;
+import com.vaadin.ui.GridLayout.Area;
+
+@SuppressWarnings("serial")
+public class ComponentSizeValidator implements Serializable {
+
+ private final static int LAYERS_SHOWN = 4;
+
+ /**
+ * Recursively checks given component and its subtree for invalid layout
+ * setups. Prints errors to std err stream.
+ *
+ * @param component
+ * component to check
+ * @return set of first level errors found
+ */
+ public static List<InvalidLayout> validateComponentRelativeSizes(
+ Component component, List<InvalidLayout> errors,
+ InvalidLayout parent) {
+
+ boolean invalidHeight = !checkHeights(component);
+ boolean invalidWidth = !checkWidths(component);
+
+ if (invalidHeight || invalidWidth) {
+ InvalidLayout error = new InvalidLayout(component, invalidHeight,
+ invalidWidth);
+ if (parent != null) {
+ parent.addError(error);
+ } else {
+ if (errors == null) {
+ errors = new LinkedList<InvalidLayout>();
+ }
+ errors.add(error);
+ }
+ parent = error;
+ }
+
+ if (component instanceof Panel) {
+ Panel panel = (Panel) component;
+ errors = validateComponentRelativeSizes(panel.getLayout(), errors,
+ parent);
+ } else if (component instanceof ComponentContainer) {
+ ComponentContainer lo = (ComponentContainer) component;
+ Iterator it = lo.getComponentIterator();
+ while (it.hasNext()) {
+ errors = validateComponentRelativeSizes((Component) it.next(),
+ errors, parent);
+ }
+ } else if (component instanceof Form) {
+ Form form = (Form) component;
+ if (form.getLayout() != null) {
+ errors = validateComponentRelativeSizes(form.getLayout(),
+ errors, parent);
+ }
+ if (form.getFooter() != null) {
+ errors = validateComponentRelativeSizes(form.getFooter(),
+ errors, parent);
+ }
+ }
+
+ return errors;
+ }
+
+ private static void printServerError(String msg,
+ Stack<ComponentInfo> attributes, boolean widthError,
+ PrintStream errorStream) {
+ StringBuffer err = new StringBuffer();
+ err.append("IT Mill Toolkit DEBUG\n");
+
+ StringBuilder indent = new StringBuilder("");
+ ComponentInfo ci;
+ if (attributes != null) {
+ while (attributes.size() > LAYERS_SHOWN) {
+ attributes.pop();
+ }
+ while (!attributes.empty()) {
+ ci = attributes.pop();
+ showComponent(ci.component, ci.info, err, indent, widthError);
+ }
+ }
+
+ err.append("Layout problem detected: ");
+ err.append(msg);
+ err.append("\n");
+ err
+ .append("Relative sizes were replaced by undefined sizes, components may not render as expected.\n");
+ errorStream.println(err);
+
+ }
+
+ public static boolean checkHeights(Component component) {
+ try {
+ if (!hasRelativeHeight(component)) {
+ return true;
+ }
+ if (component instanceof Window) {
+ return true;
+ }
+ if (component.getParent() == null) {
+ return true;
+ }
+
+ return parentCanDefineHeight(component);
+ } catch (Exception e) {
+ e.printStackTrace();
+ return true;
+ }
+ }
+
+ public static boolean checkWidths(Component component) {
+ try {
+ if (!hasRelativeWidth(component)) {
+ return true;
+ }
+ if (component instanceof Window) {
+ return true;
+ }
+ if (component.getParent() == null) {
+ return true;
+ }
+
+ return parentCanDefineWidth(component);
+ } catch (Exception e) {
+ e.printStackTrace();
+ return true;
+ }
+ }
+
+ public static class InvalidLayout implements Serializable {
+
+ private Component component;
+
+ private boolean invalidHeight;
+ private boolean invalidWidth;
+
+ private Vector<InvalidLayout> subErrors = new Vector<InvalidLayout>();
+
+ public InvalidLayout(Component component, boolean height, boolean width) {
+ this.component = component;
+ invalidHeight = height;
+ invalidWidth = width;
+ }
+
+ public void addError(InvalidLayout error) {
+ subErrors.add(error);
+ }
+
+ @SuppressWarnings("deprecation")
+ public void reportErrors(PrintWriter clientJSON,
+ CommunicationManager communicationManager,
+ PrintStream serverErrorStream) {
+ clientJSON.write("{");
+
+ Component parent = component.getParent();
+ String paintableId = communicationManager.getPaintableId(component);
+
+ clientJSON.print("id:\"" + paintableId + "\"");
+
+ if (invalidHeight) {
+ Stack<ComponentInfo> attributes = null;
+ String msg = "";
+ // set proper error messages
+ if (parent instanceof AbstractOrderedLayout) {
+ AbstractOrderedLayout ol = (AbstractOrderedLayout) parent;
+ boolean vertical = false;
+
+ if (ol instanceof OrderedLayout) {
+ if (((OrderedLayout) ol).getOrientation() == OrderedLayout.ORIENTATION_VERTICAL) {
+ vertical = true;
+ }
+ } else if (ol instanceof VerticalLayout) {
+ vertical = true;
+ }
+
+ if (vertical) {
+ msg = "Component with relative height inside a VerticalLayout with no height defined.";
+ attributes = getHeightAttributes(component);
+ } else {
+ msg = "At least one of a HorizontalLayout's components must have non relative height if the height of the layout is not defined";
+ attributes = getHeightAttributes(component);
+ }
+ } else if (parent instanceof GridLayout) {
+ msg = "At least one of the GridLayout's components in each row should have non relative height if the height of the layout is not defined.";
+ attributes = getHeightAttributes(component);
+ } else {
+ // default error for non sized parent issue
+ msg = "A component with relative height needs a parent with defined height.";
+ attributes = getHeightAttributes(component);
+ }
+ printServerError(msg, attributes, false, serverErrorStream);
+ clientJSON.print(",\"heightMsg\":\"" + msg + "\"");
+ }
+ if (invalidWidth) {
+ Stack<ComponentInfo> attributes = null;
+ String msg = "";
+ if (parent instanceof AbstractOrderedLayout) {
+ AbstractOrderedLayout ol = (AbstractOrderedLayout) parent;
+ boolean horizontal = true;
+
+ if (ol instanceof OrderedLayout) {
+ if (((OrderedLayout) ol).getOrientation() == OrderedLayout.ORIENTATION_VERTICAL) {
+ horizontal = false;
+ }
+ } else if (ol instanceof VerticalLayout) {
+ horizontal = false;
+ }
+
+ if (horizontal) {
+ msg = "Component with relative width inside a HorizontalLayout with no width defined";
+ attributes = getWidthAttributes(component);
+ } else {
+ msg = "At least one of a VerticalLayout's components must have non relative width if the width of the layout is not defined";
+ attributes = getWidthAttributes(component);
+ }
+ } else if (parent instanceof GridLayout) {
+ msg = "At least one of the GridLayout's components in each column should have non relative width if the width of the layout is not defined.";
+ attributes = getWidthAttributes(component);
+ } else {
+ // default error for non sized parent issue
+ msg = "A component with relative width needs a parent with defined width.";
+ attributes = getWidthAttributes(component);
+ }
+ clientJSON.print(",\"widthMsg\":\"" + msg + "\"");
+ printServerError(msg, attributes, true, serverErrorStream);
+ }
+ if (subErrors.size() > 0) {
+ serverErrorStream.println("Sub errors >>");
+ clientJSON.write(", \"subErrors\" : [");
+ boolean first = true;
+ for (InvalidLayout subError : subErrors) {
+ if (!first) {
+ clientJSON.print(",");
+ } else {
+ first = false;
+ }
+ subError.reportErrors(clientJSON, communicationManager,
+ serverErrorStream);
+ }
+ clientJSON.write("]");
+ serverErrorStream.println("<< Sub erros");
+ }
+ clientJSON.write("}");
+ }
+ }
+
+ private static class ComponentInfo implements Serializable {
+ Component component;
+ String info;
+
+ public ComponentInfo(Component component, String info) {
+ this.component = component;
+ this.info = info;
+ }
+
+ }
+
+ private static Stack<ComponentInfo> getHeightAttributes(Component component) {
+ Stack<ComponentInfo> attributes = new Stack<ComponentInfo>();
+ attributes
+ .add(new ComponentInfo(component, getHeightString(component)));
+ Component parent = component.getParent();
+ attributes.add(new ComponentInfo(parent, getHeightString(parent)));
+
+ while ((parent = parent.getParent()) != null) {
+ attributes.add(new ComponentInfo(parent, getHeightString(parent)));
+ }
+
+ return attributes;
+ }
+
+ private static Stack<ComponentInfo> getWidthAttributes(Component component) {
+ Stack<ComponentInfo> attributes = new Stack<ComponentInfo>();
+ attributes.add(new ComponentInfo(component, getWidthString(component)));
+ Component parent = component.getParent();
+ attributes.add(new ComponentInfo(parent, getWidthString(parent)));
+
+ while ((parent = parent.getParent()) != null) {
+ attributes.add(new ComponentInfo(parent, getWidthString(parent)));
+ }
+
+ return attributes;
+ }
+
+ private static String getWidthString(Component component) {
+ String width = "width: ";
+ if (hasRelativeWidth(component)) {
+ width += "RELATIVE, " + component.getWidth() + " %";
+ } else if (component instanceof Window && component.getParent() == null) {
+ width += "MAIN WINDOW";
+ } else if (component.getWidth() >= 0) {
+ width += "ABSOLUTE, " + component.getWidth() + " "
+ + Sizeable.UNIT_SYMBOLS[component.getWidthUnits()];
+ } else {
+ width += "UNDEFINED";
+ }
+
+ return width;
+ }
+
+ private static String getHeightString(Component component) {
+ String height = "height: ";
+ if (hasRelativeHeight(component)) {
+ height += "RELATIVE, " + component.getHeight() + " %";
+ } else if (component instanceof Window && component.getParent() == null) {
+ height += "MAIN WINDOW";
+ } else if (component.getHeight() > 0) {
+ height += "ABSOLUTE, " + component.getHeight() + " "
+ + Sizeable.UNIT_SYMBOLS[component.getHeightUnits()];
+ } else {
+ height += "UNDEFINED";
+ }
+
+ return height;
+ }
+
+ private static void showComponent(Component component, String attribute,
+ StringBuffer err, StringBuilder indent, boolean widthError) {
+
+ FileLocation createLoc = creationLocations.get(component);
+
+ FileLocation sizeLoc;
+ if (widthError) {
+ sizeLoc = widthLocations.get(component);
+ } else {
+ sizeLoc = heightLocations.get(component);
+ }
+
+ err.append(indent);
+ indent.append(" ");
+ err.append("- ");
+
+ err.append(component.getClass().getSimpleName());
+ err.append("/").append(Integer.toHexString(component.hashCode()));
+
+ if (component.getCaption() != null) {
+ err.append(" \"");
+ err.append(component.getCaption());
+ err.append("\"");
+ }
+
+ if (component.getDebugId() != null) {
+ err.append(" debugId: ");
+ err.append(component.getDebugId());
+ }
+
+ if (createLoc != null) {
+ err.append(", created at (" + createLoc.file + ":"
+ + createLoc.lineNumber + ")");
+
+ }
+
+ if (attribute != null) {
+ err.append(" (");
+ err.append(attribute);
+ if (sizeLoc != null) {
+ err.append(", set at (" + sizeLoc.file + ":"
+ + sizeLoc.lineNumber + ")");
+ }
+
+ err.append(")");
+ }
+ err.append("\n");
+
+ }
+
+ private static boolean hasNonRelativeHeightComponent(
+ AbstractOrderedLayout ol) {
+ Iterator it = ol.getComponentIterator();
+ while (it.hasNext()) {
+ if (!hasRelativeHeight((Component) it.next())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @SuppressWarnings("deprecation")
+ public static boolean parentCanDefineHeight(Component component) {
+ Component parent = component.getParent();
+ if (parent == null) {
+ // main window, valid situation
+ return true;
+ }
+ if (parent.getHeight() < 0) {
+ // Undefined height
+ if (parent instanceof Window) {
+ Window w = (Window) parent;
+ if (w.getParent() == null) {
+ // main window is considered to have size
+ return true;
+ }
+ }
+
+ if (parent instanceof AbstractOrderedLayout) {
+ boolean horizontal = true;
+ if (parent instanceof OrderedLayout) {
+ horizontal = ((OrderedLayout) parent).getOrientation() == OrderedLayout.ORIENTATION_HORIZONTAL;
+ } else if (parent instanceof VerticalLayout) {
+ horizontal = false;
+ }
+ if (horizontal
+ && hasNonRelativeHeightComponent((AbstractOrderedLayout) parent)) {
+ return true;
+ } else {
+ return false;
+ }
+
+ } else if (parent instanceof GridLayout) {
+ GridLayout gl = (GridLayout) parent;
+ Area componentArea = gl.getComponentArea(component);
+ boolean rowHasHeight = false;
+ for (int row = componentArea.getRow1(); !rowHasHeight
+ && row <= componentArea.getRow2(); row++) {
+ for (int column = 0; !rowHasHeight
+ && column < gl.getColumns(); column++) {
+ Component c = gl.getComponent(column, row);
+ if (c != null) {
+ rowHasHeight = !hasRelativeHeight(c);
+ }
+ }
+ }
+ if (!rowHasHeight) {
+ return false;
+ } else {
+ // Other components define row height
+ return true;
+ }
+ }
+
+ if (parent instanceof Panel || parent instanceof SplitPanel
+ || parent instanceof TabSheet
+ || parent instanceof CustomComponent) {
+ // height undefined, we know how how component works and no
+ // exceptions
+ // TODO horiz SplitPanel ??
+ return false;
+ } else {
+ // We cannot generally know if undefined component can serve
+ // space for children (like CustomLayout or component built by
+ // third party) so we assume they can
+ return true;
+ }
+
+ } else if (hasRelativeHeight(parent)) {
+ // Relative height
+ if (parent.getParent() != null) {
+ return parentCanDefineHeight(parent);
+ } else {
+ return true;
+ }
+ } else {
+ // Absolute height
+ return true;
+ }
+ }
+
+ private static boolean hasRelativeHeight(Component component) {
+ return (component.getHeightUnits() == Sizeable.UNITS_PERCENTAGE && component
+ .getHeight() > 0);
+ }
+
+ private static boolean hasNonRelativeWidthComponent(AbstractOrderedLayout ol) {
+ Iterator it = ol.getComponentIterator();
+ while (it.hasNext()) {
+ if (!hasRelativeWidth((Component) it.next())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static boolean hasRelativeWidth(Component paintable) {
+ return paintable.getWidth() > 0
+ && paintable.getWidthUnits() == Sizeable.UNITS_PERCENTAGE;
+ }
+
+ @SuppressWarnings("deprecation")
+ public static boolean parentCanDefineWidth(Component component) {
+ Component parent = component.getParent();
+ if (parent == null) {
+ // main window, valid situation
+ return true;
+ }
+ if (parent instanceof Window) {
+ Window w = (Window) parent;
+ if (w.getParent() == null) {
+ // main window is considered to have size
+ return true;
+ }
+
+ }
+
+ if (parent.getWidth() < 0) {
+ // Undefined width
+
+ if (parent instanceof AbstractOrderedLayout) {
+ AbstractOrderedLayout ol = (AbstractOrderedLayout) parent;
+ boolean horizontal = true;
+ if (ol instanceof OrderedLayout) {
+ if (((OrderedLayout) ol).getOrientation() == OrderedLayout.ORIENTATION_VERTICAL) {
+ horizontal = false;
+ }
+ } else if (ol instanceof VerticalLayout) {
+ horizontal = false;
+ }
+
+ if (!horizontal && hasNonRelativeWidthComponent(ol)) {
+ // valid situation, other components defined width
+ return true;
+ } else {
+ return false;
+ }
+ } else if (parent instanceof GridLayout) {
+ GridLayout gl = (GridLayout) parent;
+ Area componentArea = gl.getComponentArea(component);
+ boolean columnHasWidth = false;
+ for (int col = componentArea.getColumn1(); !columnHasWidth
+ && col <= componentArea.getColumn2(); col++) {
+ for (int row = 0; !columnHasWidth && row < gl.getRows(); row++) {
+ Component c = gl.getComponent(col, row);
+ if (c != null) {
+ columnHasWidth = !hasRelativeWidth(c);
+ }
+ }
+ }
+ if (!columnHasWidth) {
+ return false;
+ } else {
+ // Other components define column width
+ return true;
+ }
+ } else if (parent instanceof Form) {
+ /*
+ * If some other part of the form is not relative it determines
+ * the component width
+ */
+ return hasNonRelativeWidthComponent((Form) parent);
+ } else if (parent instanceof SplitPanel
+ || parent instanceof TabSheet
+ || parent instanceof CustomComponent) {
+ // FIXME Could we use com.vaadin package name here and
+ // fail for all component containers?
+ // FIXME Actually this should be moved to containers so it can
+ // be implemented for custom containers
+ // TODO vertical splitpanel with another non relative component?
+ return false;
+ } else if (parent instanceof Window) {
+ // Sub window can define width based on caption
+ if (parent.getCaption() != null
+ && !parent.getCaption().equals("")) {
+ return true;
+ } else {
+ return false;
+ }
+ } else if (parent instanceof Panel) {
+ // TODO Panel should be able to define width based on caption
+ return false;
+ } else {
+ return true;
+ }
+ } else if (hasRelativeWidth(parent)) {
+ // Relative width
+ if (parent.getParent() == null) {
+ return true;
+ }
+
+ return parentCanDefineWidth(parent);
+ } else {
+ return true;
+ }
+
+ }
+
+ private static boolean hasNonRelativeWidthComponent(Form form) {
+ Layout layout = form.getLayout();
+ Layout footer = form.getFooter();
+
+ if (layout != null && !hasRelativeWidth(layout)) {
+ return true;
+ }
+ if (footer != null && !hasRelativeWidth(footer)) {
+ return true;
+ }
+
+ return false;
+ }
+
+ private static Map<Object, FileLocation> creationLocations = new HashMap<Object, FileLocation>();
+ private static Map<Object, FileLocation> widthLocations = new HashMap<Object, FileLocation>();
+ private static Map<Object, FileLocation> heightLocations = new HashMap<Object, FileLocation>();
+
+ public static class FileLocation implements Serializable {
+ public String method;
+ public String file;
+ public String className;
+ public String classNameSimple;
+ public int lineNumber;
+
+ public FileLocation(StackTraceElement traceElement) {
+ file = traceElement.getFileName();
+ className = traceElement.getClassName();
+ classNameSimple = className
+ .substring(className.lastIndexOf('.') + 1);
+ lineNumber = traceElement.getLineNumber();
+ method = traceElement.getMethodName();
+ }
+ }
+
+ public static void setCreationLocation(Object object) {
+ setLocation(creationLocations, object);
+ }
+
+ public static void setWidthLocation(Object object) {
+ setLocation(widthLocations, object);
+ }
+
+ public static void setHeightLocation(Object object) {
+ setLocation(heightLocations, object);
+ }
+
+ private static void setLocation(Map<Object, FileLocation> map, Object object) {
+ StackTraceElement[] traceLines = Thread.currentThread().getStackTrace();
+ for (StackTraceElement traceElement : traceLines) {
+ Class cls;
+ try {
+ String className = traceElement.getClassName();
+ if (className.startsWith("java.")
+ || className.startsWith("sun.")) {
+ continue;
+ }
+
+ cls = Class.forName(className);
+ if (cls == ComponentSizeValidator.class || cls == Thread.class) {
+ continue;
+ }
+
+ if (Component.class.isAssignableFrom(cls)
+ && !CustomComponent.class.isAssignableFrom(cls)) {
+ continue;
+ }
+ FileLocation cl = new FileLocation(traceElement);
+ map.put(object, cl);
+ return;
+ } catch (Exception e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+
+ }
+ }
+
+}
diff --git a/src/com/vaadin/terminal/gwt/server/HttpUploadStream.java b/src/com/vaadin/terminal/gwt/server/HttpUploadStream.java
new file mode 100644
index 0000000000..c9e3c77cad
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/server/HttpUploadStream.java
@@ -0,0 +1,91 @@
+/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.server;
+
+import java.io.InputStream;
+
+/**
+ * AjaxAdapter implementation of the UploadStream interface.
+ *
+ * @author IT Mill Ltd.
+ * @version
+ * @VERSION@
+ * @since 5.0
+ */
+@SuppressWarnings("serial")
+public class HttpUploadStream implements
+ com.vaadin.terminal.UploadStream {
+
+ /**
+ * Holds value of property variableName.
+ */
+ private final String streamName;
+
+ private final String contentName;
+
+ private final String contentType;
+
+ /**
+ * Holds value of property variableValue.
+ */
+ private final InputStream stream;
+
+ /**
+ * Creates a new instance of UploadStreamImpl.
+ *
+ * @param name
+ * the name of the stream.
+ * @param stream
+ * the input stream.
+ * @param contentName
+ * the name of the content.
+ * @param contentType
+ * the type of the content.
+ */
+ public HttpUploadStream(String name, InputStream stream,
+ String contentName, String contentType) {
+ streamName = name;
+ this.stream = stream;
+ this.contentName = contentName;
+ this.contentType = contentType;
+ }
+
+ /**
+ * Gets the name of the stream.
+ *
+ * @return the name of the stream.
+ */
+ public String getStreamName() {
+ return streamName;
+ }
+
+ /**
+ * Gets the input stream.
+ *
+ * @return the Input stream.
+ */
+ public InputStream getStream() {
+ return stream;
+ }
+
+ /**
+ * Gets the input stream content type.
+ *
+ * @return the content type of the input stream.
+ */
+ public String getContentType() {
+ return contentType;
+ }
+
+ /**
+ * Gets the stream content name. Stream content name usually differs from
+ * the actual stream name. It is used to identify the content of the stream.
+ *
+ * @return the Name of the stream content.
+ */
+ public String getContentName() {
+ return contentName;
+ }
+}
diff --git a/src/com/vaadin/terminal/gwt/server/JsonPaintTarget.java b/src/com/vaadin/terminal/gwt/server/JsonPaintTarget.java
new file mode 100644
index 0000000000..b81e65e429
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/server/JsonPaintTarget.java
@@ -0,0 +1,1108 @@
+/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.server;
+
+import java.io.PrintWriter;
+import java.io.Serializable;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Set;
+import java.util.Stack;
+import java.util.Vector;
+
+import com.vaadin.Application;
+import com.vaadin.terminal.ApplicationResource;
+import com.vaadin.terminal.ExternalResource;
+import com.vaadin.terminal.PaintException;
+import com.vaadin.terminal.PaintTarget;
+import com.vaadin.terminal.Paintable;
+import com.vaadin.terminal.Resource;
+import com.vaadin.terminal.ThemeResource;
+import com.vaadin.terminal.VariableOwner;
+import com.vaadin.ui.Component;
+
+/**
+ * User Interface Description Language Target.
+ *
+ * @author IT Mill Ltd.
+ * @version
+ * @VERSION@
+ * @since 5.0
+ */
+@SuppressWarnings("serial")
+public class JsonPaintTarget implements PaintTarget {
+
+ /* Document type declarations */
+
+ private final static String UIDL_ARG_NAME = "name";
+
+ private final Stack<String> mOpenTags;
+
+ private final Stack<JsonTag> openJsonTags;
+
+ private final PrintWriter uidlBuffer;
+
+ private boolean closed = false;
+
+ private final CommunicationManager manager;
+
+ private int changes = 0;
+
+ Set preCachedResources = new HashSet();
+
+ private boolean customLayoutArgumentsOpen = false;
+
+ private JsonTag tag;
+
+ private int errorsOpen;
+
+ private boolean cacheEnabled = false;
+
+ private Collection<Paintable> paintedComponents = new HashSet<Paintable>();
+
+ private Collection<Paintable> identifiersCreatedDueRefPaint;
+
+ /**
+ * Creates a new XMLPrintWriter, without automatic line flushing.
+ *
+ * @param variableMap
+ * @param manager
+ * @param outWriter
+ * A character-output stream.
+ * @throws PaintException
+ * if the paint operation failed.
+ */
+ public JsonPaintTarget(CommunicationManager manager, PrintWriter outWriter,
+ boolean cachingRequired) throws PaintException {
+
+ this.manager = manager;
+
+ // Sets the target for UIDL writing
+ uidlBuffer = outWriter;
+
+ // Initialize tag-writing
+ mOpenTags = new Stack<String>();
+ openJsonTags = new Stack<JsonTag>();
+ cacheEnabled = cachingRequired;
+ }
+
+ public void startTag(String tagName) throws PaintException {
+ startTag(tagName, false);
+ }
+
+ /**
+ * Prints the element start tag.
+ *
+ * <pre>
+ * Todo:
+ * Checking of input values
+ *
+ * </pre>
+ *
+ * @param tagName
+ * the name of the start tag.
+ * @throws PaintException
+ * if the paint operation failed.
+ *
+ */
+ public void startTag(String tagName, boolean isChildNode)
+ throws PaintException {
+ // In case of null data output nothing:
+ if (tagName == null) {
+ throw new NullPointerException();
+ }
+
+ // Ensures that the target is open
+ if (closed) {
+ throw new PaintException(
+ "Attempted to write to a closed PaintTarget.");
+ }
+
+ if (tag != null) {
+ openJsonTags.push(tag);
+ }
+ // Checks tagName and attributes here
+ mOpenTags.push(tagName);
+
+ tag = new JsonTag(tagName);
+
+ customLayoutArgumentsOpen = "customlayout".equals(tagName);
+
+ if ("error".equals(tagName)) {
+ errorsOpen++;
+ }
+ }
+
+ /**
+ * Prints the element end tag.
+ *
+ * If the parent tag is closed before every child tag is closed an
+ * PaintException is raised.
+ *
+ * @param tag
+ * the name of the end tag.
+ * @throws Paintexception
+ * if the paint operation failed.
+ */
+ public void endTag(String tagName) throws PaintException {
+ // In case of null data output nothing:
+ if (tagName == null) {
+ throw new NullPointerException();
+ }
+
+ // Ensure that the target is open
+ if (closed) {
+ throw new PaintException(
+ "Attempted to write to a closed PaintTarget.");
+ }
+
+ if (openJsonTags.size() > 0) {
+ final JsonTag parent = openJsonTags.pop();
+
+ String lastTag = "";
+
+ lastTag = mOpenTags.pop();
+ if (!tagName.equalsIgnoreCase(lastTag)) {
+ throw new PaintException("Invalid UIDL: wrong ending tag: '"
+ + tagName + "' expected: '" + lastTag + "'.");
+ }
+
+ // simple hack which writes error uidl structure into attribute
+ if ("error".equals(lastTag)) {
+ if (errorsOpen == 1) {
+ parent.addAttribute("\"error\":[\"error\",{}"
+ + tag.getData() + "]");
+ } else {
+ // sub error
+ parent.addData(tag.getJSON());
+ }
+ errorsOpen--;
+ } else {
+ parent.addData(tag.getJSON());
+ }
+
+ tag = parent;
+ } else {
+ changes++;
+ uidlBuffer.print(((changes > 1) ? "," : "") + tag.getJSON());
+ tag = null;
+ }
+ }
+
+ /**
+ * Substitutes the XML sensitive characters with predefined XML entities.
+ *
+ * @param xml
+ * the String to be substituted.
+ * @return A new string instance where all occurrences of XML sensitive
+ * characters are substituted with entities.
+ */
+ static public String escapeXML(String xml) {
+ if (xml == null || xml.length() <= 0) {
+ return "";
+ }
+ return escapeXML(new StringBuffer(xml)).toString();
+ }
+
+ /**
+ * Substitutes the XML sensitive characters with predefined XML entities.
+ *
+ * @param xml
+ * the String to be substituted.
+ * @return A new StringBuffer instance where all occurrences of XML
+ * sensitive characters are substituted with entities.
+ *
+ */
+ static public StringBuffer escapeXML(StringBuffer xml) {
+ if (xml == null || xml.length() <= 0) {
+ return new StringBuffer("");
+ }
+
+ final StringBuffer result = new StringBuffer(xml.length() * 2);
+
+ for (int i = 0; i < xml.length(); i++) {
+ final char c = xml.charAt(i);
+ final String s = toXmlChar(c);
+ if (s != null) {
+ result.append(s);
+ } else {
+ result.append(c);
+ }
+ }
+ return result;
+ }
+
+ static public String escapeJSON(String s) {
+ if (s == null) {
+ return "";
+ }
+ final StringBuffer sb = new StringBuffer();
+ for (int i = 0; i < s.length(); i++) {
+ final char ch = s.charAt(i);
+ switch (ch) {
+ case '"':
+ sb.append("\\\"");
+ break;
+ case '\\':
+ sb.append("\\\\");
+ break;
+ case '\b':
+ sb.append("\\b");
+ break;
+ case '\f':
+ sb.append("\\f");
+ break;
+ case '\n':
+ sb.append("\\n");
+ break;
+ case '\r':
+ sb.append("\\r");
+ break;
+ case '\t':
+ sb.append("\\t");
+ break;
+ case '/':
+ sb.append("\\/");
+ break;
+ default:
+ if (ch >= '\u0000' && ch <= '\u001F') {
+ final String ss = Integer.toHexString(ch);
+ sb.append("\\u");
+ for (int k = 0; k < 4 - ss.length(); k++) {
+ sb.append('0');
+ }
+ sb.append(ss.toUpperCase());
+ } else {
+ sb.append(ch);
+ }
+ }
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Substitutes a XML sensitive character with predefined XML entity.
+ *
+ * @param c
+ * the Character to be replaced with an entity.
+ * @return String of the entity or null if character is not to be replaced
+ * with an entity.
+ */
+ private static String toXmlChar(char c) {
+ switch (c) {
+ case '&':
+ return "&amp;"; // & => &amp;
+ case '>':
+ return "&gt;"; // > => &gt;
+ case '<':
+ return "&lt;"; // < => &lt;
+ case '"':
+ return "&quot;"; // " => &quot;
+ case '\'':
+ return "&apos;"; // ' => &apos;
+ default:
+ return null;
+ }
+ }
+
+ /**
+ * Prints XML-escaped text.
+ *
+ * @param str
+ * @throws PaintException
+ * if the paint operation failed.
+ *
+ */
+ public void addText(String str) throws PaintException {
+ tag.addData("\"" + escapeJSON(str) + "\"");
+ }
+
+ /**
+ * Adds a boolean attribute to component. Atributes must be added before any
+ * content is written.
+ *
+ * @param name
+ * the Attribute name.
+ * @param value
+ * the Attribute value.
+ * @throws PaintException
+ * if the paint operation failed.
+ */
+ public void addAttribute(String name, boolean value) throws PaintException {
+ tag.addAttribute("\"" + name + "\":" + (value ? "true" : "false"));
+ }
+
+ /**
+ * Adds a resource attribute to component. Attributes must be added before
+ * any content is written.
+ *
+ * @param name
+ * the Attribute name.
+ * @param value
+ * the Attribute value.
+ *
+ * @throws PaintException
+ * if the paint operation failed.
+ */
+ public void addAttribute(String name, Resource value) throws PaintException {
+
+ if (value instanceof ExternalResource) {
+ addAttribute(name, ((ExternalResource) value).getURL());
+
+ } else if (value instanceof ApplicationResource) {
+ final ApplicationResource r = (ApplicationResource) value;
+ final Application a = r.getApplication();
+ if (a == null) {
+ throw new PaintException(
+ "Application not specified for resorce "
+ + value.getClass().getName());
+ }
+ String uri;
+ if (a.getURL() != null) {
+ uri = a.getURL().getPath();
+ } else {
+ uri = "";
+ }
+ if (uri.length() > 0 && uri.charAt(uri.length() - 1) != '/') {
+ uri += "/";
+ }
+ uri += a.getRelativeLocation(r);
+ addAttribute(name, uri);
+
+ } else if (value instanceof ThemeResource) {
+ final String uri = "theme://"
+ + ((ThemeResource) value).getResourceId();
+ addAttribute(name, uri);
+ } else {
+ throw new PaintException("Ajax adapter does not "
+ + "support resources of type: "
+ + value.getClass().getName());
+ }
+
+ }
+
+ /**
+ * Adds a integer attribute to component. Atributes must be added before any
+ * content is written.
+ *
+ * @param name
+ * the Attribute name.
+ * @param value
+ * the Attribute value.
+ *
+ * @throws PaintException
+ * if the paint operation failed.
+ */
+ public void addAttribute(String name, int value) throws PaintException {
+ tag.addAttribute("\"" + name + "\":" + String.valueOf(value));
+ }
+
+ /**
+ * Adds a long attribute to component. Atributes must be added before any
+ * content is written.
+ *
+ * @param name
+ * the Attribute name.
+ * @param value
+ * the Attribute value.
+ *
+ * @throws PaintException
+ * if the paint operation failed.
+ */
+ public void addAttribute(String name, long value) throws PaintException {
+ tag.addAttribute("\"" + name + "\":" + String.valueOf(value));
+ }
+
+ /**
+ * Adds a float attribute to component. Atributes must be added before any
+ * content is written.
+ *
+ * @param name
+ * the Attribute name.
+ * @param value
+ * the Attribute value.
+ *
+ * @throws PaintException
+ * if the paint operation failed.
+ */
+ public void addAttribute(String name, float value) throws PaintException {
+ tag.addAttribute("\"" + name + "\":" + String.valueOf(value));
+ }
+
+ /**
+ * Adds a double attribute to component. Atributes must be added before any
+ * content is written.
+ *
+ * @param name
+ * the Attribute name.
+ * @param value
+ * the Attribute value.
+ *
+ * @throws PaintException
+ * if the paint operation failed.
+ */
+ public void addAttribute(String name, double value) throws PaintException {
+ tag.addAttribute("\"" + name + "\":" + String.valueOf(value));
+ }
+
+ /**
+ * Adds a string attribute to component. Atributes must be added before any
+ * content is written.
+ *
+ * @param name
+ * the String attribute name.
+ * @param value
+ * the String attribute value.
+ *
+ * @throws PaintException
+ * if the paint operation failed.
+ */
+ public void addAttribute(String name, String value) throws PaintException {
+ // In case of null data output nothing:
+ if ((value == null) || (name == null)) {
+ throw new NullPointerException(
+ "Parameters must be non-null strings");
+ }
+
+ tag.addAttribute("\"" + name + "\": \"" + escapeJSON(value) + "\"");
+
+ if (customLayoutArgumentsOpen && "template".equals(name)) {
+ getPreCachedResources().add("layouts/" + value + ".html");
+ }
+
+ if (name.equals("locale")) {
+ manager.requireLocale(value);
+ }
+
+ }
+
+ public void addAttribute(String name, Object[] values) {
+ // In case of null data output nothing:
+ if ((values == null) || (name == null)) {
+ throw new NullPointerException(
+ "Parameters must be non-null strings");
+ }
+ final StringBuffer buf = new StringBuffer();
+ buf.append("\"" + name + "\":[");
+ for (int i = 0; i < values.length; i++) {
+ if (i > 0) {
+ buf.append(",");
+ }
+ buf.append("\"");
+ buf.append(escapeJSON(values[i].toString()));
+ buf.append("\"");
+ }
+ buf.append("]");
+ tag.addAttribute(buf.toString());
+ }
+
+ /**
+ * Adds a string type variable.
+ *
+ * @param owner
+ * the Listener for variable changes.
+ * @param name
+ * the Variable name.
+ * @param value
+ * the Variable initial value.
+ *
+ * @throws PaintException
+ * if the paint operation failed.
+ */
+ public void addVariable(VariableOwner owner, String name, String value)
+ throws PaintException {
+ tag.addVariable(new StringVariable(owner, name, escapeJSON(value)));
+ }
+
+ /**
+ * Adds a int type variable.
+ *
+ * @param owner
+ * the Listener for variable changes.
+ * @param name
+ * the Variable name.
+ * @param value
+ * the Variable initial value.
+ *
+ * @throws PaintException
+ * if the paint operation failed.
+ */
+ public void addVariable(VariableOwner owner, String name, int value)
+ throws PaintException {
+ tag.addVariable(new IntVariable(owner, name, value));
+ }
+
+ /**
+ * Adds a long type variable.
+ *
+ * @param owner
+ * the Listener for variable changes.
+ * @param name
+ * the Variable name.
+ * @param value
+ * the Variable initial value.
+ *
+ * @throws PaintException
+ * if the paint operation failed.
+ */
+ public void addVariable(VariableOwner owner, String name, long value)
+ throws PaintException {
+ tag.addVariable(new LongVariable(owner, name, value));
+ }
+
+ /**
+ * Adds a float type variable.
+ *
+ * @param owner
+ * the Listener for variable changes.
+ * @param name
+ * the Variable name.
+ * @param value
+ * the Variable initial value.
+ *
+ * @throws PaintException
+ * if the paint operation failed.
+ */
+ public void addVariable(VariableOwner owner, String name, float value)
+ throws PaintException {
+ tag.addVariable(new FloatVariable(owner, name, value));
+ }
+
+ /**
+ * Adds a double type variable.
+ *
+ * @param owner
+ * the Listener for variable changes.
+ * @param name
+ * the Variable name.
+ * @param value
+ * the Variable initial value.
+ *
+ * @throws PaintException
+ * if the paint operation failed.
+ */
+ public void addVariable(VariableOwner owner, String name, double value)
+ throws PaintException {
+ tag.addVariable(new DoubleVariable(owner, name, value));
+ }
+
+ /**
+ * Adds a boolean type variable.
+ *
+ * @param owner
+ * the Listener for variable changes.
+ * @param name
+ * the Variable name.
+ * @param value
+ * the Variable initial value.
+ *
+ * @throws PaintException
+ * if the paint operation failed.
+ */
+ public void addVariable(VariableOwner owner, String name, boolean value)
+ throws PaintException {
+ tag.addVariable(new BooleanVariable(owner, name, value));
+ }
+
+ /**
+ * Adds a string array type variable.
+ *
+ * @param owner
+ * the Listener for variable changes.
+ * @param name
+ * the Variable name.
+ * @param value
+ * the Variable initial value.
+ *
+ * @throws PaintException
+ * if the paint operation failed.
+ */
+ public void addVariable(VariableOwner owner, String name, String[] value)
+ throws PaintException {
+ tag.addVariable(new ArrayVariable(owner, name, value));
+ }
+
+ /**
+ * Adds a upload stream type variable.
+ *
+ * TODO not converted for JSON
+ *
+ * @param owner
+ * the Listener for variable changes.
+ * @param name
+ * the Variable name.
+ *
+ * @throws PaintException
+ * if the paint operation failed.
+ */
+ public void addUploadStreamVariable(VariableOwner owner, String name)
+ throws PaintException {
+ startTag("uploadstream");
+ addAttribute(UIDL_ARG_NAME, name);
+ endTag("uploadstream");
+ }
+
+ /**
+ * Prints the single text section.
+ *
+ * Prints full text section. The section data is escaped
+ *
+ * @param sectionTagName
+ * the name of the tag.
+ * @param sectionData
+ * the section data to be printed.
+ * @throws PaintException
+ * if the paint operation failed.
+ */
+ public void addSection(String sectionTagName, String sectionData)
+ throws PaintException {
+ tag.addData("{\"" + sectionTagName + "\":\"" + escapeJSON(sectionData)
+ + "\"}");
+ }
+
+ /**
+ * Adds XML directly to UIDL.
+ *
+ * @param xml
+ * the Xml to be added.
+ * @throws PaintException
+ * if the paint operation failed.
+ */
+ public void addUIDL(String xml) throws PaintException {
+
+ // Ensure that the target is open
+ if (closed) {
+ throw new PaintException(
+ "Attempted to write to a closed PaintTarget.");
+ }
+
+ // Make sure that the open start tag is closed before
+ // anything is written.
+
+ // Escape and write what was given
+ if (xml != null) {
+ tag.addData("\"" + escapeJSON(xml) + "\"");
+ }
+
+ }
+
+ /**
+ * Adds XML section with namespace.
+ *
+ * @param sectionTagName
+ * the name of the tag.
+ * @param sectionData
+ * the section data.
+ * @param namespace
+ * the namespace to be added.
+ * @throws PaintException
+ * if the paint operation failed.
+ *
+ * @see com.vaadin.terminal.PaintTarget#addXMLSection(String,
+ * String, String)
+ */
+ public void addXMLSection(String sectionTagName, String sectionData,
+ String namespace) throws PaintException {
+
+ // Ensure that the target is open
+ if (closed) {
+ throw new PaintException(
+ "Attempted to write to a closed PaintTarget.");
+ }
+
+ startTag(sectionTagName);
+ if (namespace != null) {
+ addAttribute("xmlns", namespace);
+ }
+ customLayoutArgumentsOpen = false;
+
+ if (sectionData != null) {
+ tag.addData("\"" + escapeJSON(sectionData) + "\"");
+ }
+ endTag(sectionTagName);
+ }
+
+ /**
+ * Gets the UIDL already printed to stream. Paint target must be closed
+ * before the <code>getUIDL</code> can be called.
+ *
+ * @return the UIDL.
+ */
+ public String getUIDL() {
+ if (closed) {
+ return uidlBuffer.toString();
+ }
+ throw new IllegalStateException(
+ "Tried to read UIDL from open PaintTarget");
+ }
+
+ /**
+ * Closes the paint target. Paint target must be closed before the
+ * <code>getUIDL</code> can be called. Subsequent attempts to write to paint
+ * target. If the target was already closed, call to this function is
+ * ignored. will generate an exception.
+ *
+ * @throws PaintException
+ * if the paint operation failed.
+ */
+ public void close() throws PaintException {
+ if (tag != null) {
+ uidlBuffer.write(tag.getJSON());
+ }
+ flush();
+ closed = true;
+ }
+
+ /**
+ * Method flush.
+ */
+ private void flush() {
+ uidlBuffer.flush();
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.terminal.PaintTarget#startTag(com.vaadin.terminal
+ * .Paintable, java.lang.String)
+ */
+ public boolean startTag(Paintable paintable, String tagName)
+ throws PaintException {
+ startTag(tagName, true);
+ final boolean isPreviouslyPainted = manager.hasPaintableId(paintable)
+ && (identifiersCreatedDueRefPaint == null || !identifiersCreatedDueRefPaint
+ .contains(paintable));
+ final String id = manager.getPaintableId(paintable);
+ paintable.addListener(manager);
+ addAttribute("id", id);
+ paintedComponents.add(paintable);
+ return cacheEnabled && isPreviouslyPainted;
+ }
+
+ public void paintReference(Paintable paintable, String referenceName)
+ throws PaintException {
+ if (!manager.hasPaintableId(paintable)) {
+ if (identifiersCreatedDueRefPaint == null) {
+ identifiersCreatedDueRefPaint = new HashSet<Paintable>();
+ }
+ identifiersCreatedDueRefPaint.add(paintable);
+ }
+ final String id = manager.getPaintableId(paintable);
+ addAttribute(referenceName, id);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.terminal.PaintTarget#addCharacterData(java.lang.String
+ * )
+ */
+ public void addCharacterData(String text) throws PaintException {
+ if (text != null) {
+ tag.addData(text);
+ }
+ }
+
+ /**
+ * This is basically a container for UI components variables, that will be
+ * added at the end of JSON object.
+ *
+ * @author mattitahvonen
+ *
+ */
+ class JsonTag implements Serializable {
+ boolean firstField = false;
+
+ Vector variables = new Vector();
+
+ Vector children = new Vector();
+
+ Vector attr = new Vector();
+
+ StringBuffer data = new StringBuffer();
+
+ public boolean childrenArrayOpen = false;
+
+ private boolean childNode = false;
+
+ private boolean tagClosed = false;
+
+ public JsonTag(String tagName) {
+ data.append("[\"" + tagName + "\"");
+ }
+
+ private void closeTag() {
+ if (!tagClosed) {
+ data.append(attributesAsJsonObject());
+ data.append(getData());
+ // Writes the end (closing) tag
+ data.append("]");
+ tagClosed = true;
+ }
+ }
+
+ public String getJSON() {
+ if (!tagClosed) {
+ closeTag();
+ }
+ return data.toString();
+ }
+
+ public void openChildrenArray() {
+ if (!childrenArrayOpen) {
+ // append("c : [");
+ childrenArrayOpen = true;
+ // firstField = true;
+ }
+ }
+
+ public void closeChildrenArray() {
+ // append("]");
+ // firstField = false;
+ }
+
+ public void setChildNode(boolean b) {
+ childNode = b;
+ }
+
+ public boolean isChildNode() {
+ return childNode;
+ }
+
+ public String startField() {
+ if (firstField) {
+ firstField = false;
+ return "";
+ } else {
+ return ",";
+ }
+ }
+
+ /**
+ *
+ * @param s
+ * json string, object or array
+ */
+ public void addData(String s) {
+ children.add(s);
+ }
+
+ public String getData() {
+ final StringBuffer buf = new StringBuffer();
+ final Iterator it = children.iterator();
+ while (it.hasNext()) {
+ buf.append(startField());
+ buf.append(it.next());
+ }
+ return buf.toString();
+ }
+
+ public void addAttribute(String jsonNode) {
+ attr.add(jsonNode);
+ }
+
+ private String attributesAsJsonObject() {
+ final StringBuffer buf = new StringBuffer();
+ buf.append(startField());
+ buf.append("{");
+ for (final Iterator iter = attr.iterator(); iter.hasNext();) {
+ final String element = (String) iter.next();
+ buf.append(element);
+ if (iter.hasNext()) {
+ buf.append(",");
+ }
+ }
+ buf.append(tag.variablesAsJsonObject());
+ buf.append("}");
+ return buf.toString();
+ }
+
+ public void addVariable(Variable v) {
+ variables.add(v);
+ }
+
+ private String variablesAsJsonObject() {
+ if (variables.size() == 0) {
+ return "";
+ }
+ final StringBuffer buf = new StringBuffer();
+ buf.append(startField());
+ buf.append("\"v\":{");
+ final Iterator iter = variables.iterator();
+ while (iter.hasNext()) {
+ final Variable element = (Variable) iter.next();
+ buf.append(element.getJsonPresentation());
+ if (iter.hasNext()) {
+ buf.append(",");
+ }
+ }
+ buf.append("}");
+ return buf.toString();
+ }
+
+ class TagCounter {
+ int count;
+
+ public TagCounter() {
+ count = 0;
+ }
+
+ public void increment() {
+ count++;
+ }
+
+ public String postfix(String s) {
+ if (count > 0) {
+ return s + count;
+ }
+ return s;
+ }
+ }
+ }
+
+ abstract class Variable implements Serializable {
+
+ String name;
+
+ public abstract String getJsonPresentation();
+ }
+
+ class BooleanVariable extends Variable implements Serializable {
+ boolean value;
+
+ public BooleanVariable(VariableOwner owner, String name, boolean v) {
+ value = v;
+ this.name = name;
+ }
+
+ @Override
+ public String getJsonPresentation() {
+ return "\"" + name + "\":" + (value == true ? "true" : "false");
+ }
+
+ }
+
+ class StringVariable extends Variable implements Serializable {
+ String value;
+
+ public StringVariable(VariableOwner owner, String name, String v) {
+ value = v;
+ this.name = name;
+ }
+
+ @Override
+ public String getJsonPresentation() {
+ return "\"" + name + "\":\"" + value + "\"";
+ }
+
+ }
+
+ class IntVariable extends Variable implements Serializable {
+ int value;
+
+ public IntVariable(VariableOwner owner, String name, int v) {
+ value = v;
+ this.name = name;
+ }
+
+ @Override
+ public String getJsonPresentation() {
+ return "\"" + name + "\":" + value;
+ }
+ }
+
+ class LongVariable extends Variable implements Serializable {
+ long value;
+
+ public LongVariable(VariableOwner owner, String name, long v) {
+ value = v;
+ this.name = name;
+ }
+
+ @Override
+ public String getJsonPresentation() {
+ return "\"" + name + "\":" + value;
+ }
+ }
+
+ class FloatVariable extends Variable implements Serializable {
+ float value;
+
+ public FloatVariable(VariableOwner owner, String name, float v) {
+ value = v;
+ this.name = name;
+ }
+
+ @Override
+ public String getJsonPresentation() {
+ return "\"" + name + "\":" + value;
+ }
+ }
+
+ class DoubleVariable extends Variable implements Serializable {
+ double value;
+
+ public DoubleVariable(VariableOwner owner, String name, double v) {
+ value = v;
+ this.name = name;
+ }
+
+ @Override
+ public String getJsonPresentation() {
+ return "\"" + name + "\":" + value;
+ }
+ }
+
+ class ArrayVariable extends Variable implements Serializable {
+ String[] value;
+
+ public ArrayVariable(VariableOwner owner, String name, String[] v) {
+ value = v;
+ this.name = name;
+ }
+
+ @Override
+ public String getJsonPresentation() {
+ String pres = "\"" + name + "\":[";
+ for (int i = 0; i < value.length;) {
+ pres += "\"" + value[i] + "\"";
+ i++;
+ if (i < value.length) {
+ pres += ",";
+ }
+ }
+ pres += "]";
+ return pres;
+ }
+ }
+
+ public Set getPreCachedResources() {
+ return preCachedResources;
+ }
+
+ public void setPreCachedResources(Set preCachedResources) {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Method to check if paintable is already painted into this target.
+ *
+ * @param p
+ * @return true if is not yet painted into this target and is connected to
+ * app
+ */
+ public boolean needsToBePainted(Paintable p) {
+ if (paintedComponents.contains(p)) {
+ return false;
+ } else if (((Component) p).getApplication() == null) {
+ return false;
+ } else {
+ return true;
+ }
+ }
+}
diff --git a/src/com/vaadin/terminal/gwt/server/PortletApplicationContext.java b/src/com/vaadin/terminal/gwt/server/PortletApplicationContext.java
new file mode 100644
index 0000000000..87fe4104a8
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/server/PortletApplicationContext.java
@@ -0,0 +1,290 @@
+/**
+ *
+ */
+package com.vaadin.terminal.gwt.server;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.PrintWriter;
+import java.io.Serializable;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+
+import javax.portlet.ActionRequest;
+import javax.portlet.ActionResponse;
+import javax.portlet.Portlet;
+import javax.portlet.PortletSession;
+import javax.portlet.PortletURL;
+import javax.portlet.RenderRequest;
+import javax.portlet.RenderResponse;
+import javax.servlet.http.HttpSession;
+
+import com.vaadin.Application;
+
+/**
+ * @author marc
+ *
+ */
+@SuppressWarnings("serial")
+public class PortletApplicationContext extends WebApplicationContext implements
+ Serializable {
+
+ protected PortletSession portletSession;
+
+ protected Map portletListeners = new HashMap();
+
+ protected Map portletToApplication = new HashMap();
+
+ PortletApplicationContext() {
+
+ }
+
+ static public PortletApplicationContext getApplicationContext(
+ PortletSession session) {
+ WebApplicationContext cx = (WebApplicationContext) session
+ .getAttribute(WebApplicationContext.class.getName(),
+ PortletSession.APPLICATION_SCOPE);
+ if (cx == null) {
+ cx = new PortletApplicationContext();
+ }
+ if (!(cx instanceof PortletApplicationContext)) {
+ // TODO Should we even try this? And should we leave original as-is?
+ PortletApplicationContext pcx = new PortletApplicationContext();
+ pcx.applications.addAll(cx.applications);
+ cx.applications.clear();
+ pcx.browser = cx.browser;
+ cx.browser = null;
+ pcx.listeners = cx.listeners;
+ cx.listeners = null;
+ pcx.session = cx.session;
+ cx = pcx;
+ }
+ if (((PortletApplicationContext) cx).portletSession == null) {
+ ((PortletApplicationContext) cx).portletSession = session;
+ }
+ session.setAttribute(WebApplicationContext.class.getName(), cx,
+ PortletSession.APPLICATION_SCOPE);
+ return (PortletApplicationContext) cx;
+ }
+
+ static public WebApplicationContext getApplicationContext(
+ HttpSession session) {
+ WebApplicationContext cx = (WebApplicationContext) session
+ .getAttribute(WebApplicationContext.class.getName());
+ if (cx == null) {
+ cx = new PortletApplicationContext();
+ }
+ if (cx.session == null) {
+ cx.session = session;
+ }
+ session.setAttribute(WebApplicationContext.class.getName(), cx);
+ return cx;
+ }
+
+ @Override
+ protected void removeApplication(Application application) {
+ portletListeners.remove(application);
+ for (Iterator it = portletToApplication.keySet().iterator(); it
+ .hasNext();) {
+ Object key = it.next();
+ if (key == application) {
+ portletToApplication.remove(key);
+ }
+ }
+ super.removeApplication(application);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (portletSession == null) {
+ return super.equals(obj);
+ }
+ return portletSession.equals(obj);
+ }
+
+ @Override
+ public int hashCode() {
+ if (portletSession == null) {
+ return super.hashCode();
+ }
+ return portletSession.hashCode();
+ }
+
+ public void setPortletApplication(Portlet portlet, Application app) {
+ portletToApplication.put(portlet, app);
+ }
+
+ public Application getPortletApplication(Portlet portlet) {
+ return (Application) portletToApplication.get(portlet);
+ }
+
+ public PortletSession getPortletSession() {
+ return portletSession;
+ }
+
+ public void addPortletListener(Application app, PortletListener listener) {
+ Set l = (Set) portletListeners.get(app);
+ if (l == null) {
+ l = new LinkedHashSet();
+ portletListeners.put(app, l);
+ }
+ l.add(listener);
+ }
+
+ public void removePortletListener(Application app, PortletListener listener) {
+ Set l = (Set) portletListeners.get(app);
+ if (l != null) {
+ l.remove(listener);
+ }
+ }
+
+ public static void dispatchRequest(Portlet portlet, RenderRequest request,
+ RenderResponse response) {
+ PortletApplicationContext ctx = getApplicationContext(request
+ .getPortletSession());
+ if (ctx != null) {
+ ctx.firePortletRenderRequest(portlet, request, response);
+ }
+ }
+
+ public static void dispatchRequest(Portlet portlet, ActionRequest request,
+ ActionResponse response) {
+ PortletApplicationContext ctx = getApplicationContext(request
+ .getPortletSession());
+ if (ctx != null) {
+ ctx.firePortletActionRequest(portlet, request, response);
+ }
+ }
+
+ public void firePortletRenderRequest(Portlet portlet,
+ RenderRequest request, RenderResponse response) {
+ Application app = getPortletApplication(portlet);
+ Set listeners = (Set) portletListeners.get(app);
+ if (listeners != null) {
+ for (Iterator it = listeners.iterator(); it.hasNext();) {
+ PortletListener l = (PortletListener) it.next();
+ l.handleRenderRequest(request, new RestrictedRenderResponse(
+ response));
+ }
+ }
+ }
+
+ public void firePortletActionRequest(Portlet portlet,
+ ActionRequest request, ActionResponse response) {
+ Application app = getPortletApplication(portlet);
+ Set listeners = (Set) portletListeners.get(app);
+ if (listeners != null) {
+ for (Iterator it = listeners.iterator(); it.hasNext();) {
+ PortletListener l = (PortletListener) it.next();
+ l.handleActionRequest(request, response);
+ }
+ }
+ }
+
+ public interface PortletListener extends Serializable {
+ public void handleRenderRequest(RenderRequest request,
+ RenderResponse response);
+
+ public void handleActionRequest(ActionRequest request,
+ ActionResponse response);
+ }
+
+ private class RestrictedRenderResponse implements RenderResponse,
+ Serializable {
+
+ private RenderResponse response;
+
+ private RestrictedRenderResponse(RenderResponse response) {
+ this.response = response;
+ }
+
+ public void addProperty(String key, String value) {
+ response.addProperty(key, value);
+ }
+
+ public PortletURL createActionURL() {
+ return response.createActionURL();
+ }
+
+ public PortletURL createRenderURL() {
+ return response.createRenderURL();
+ }
+
+ public String encodeURL(String path) {
+ return response.encodeURL(path);
+ }
+
+ public void flushBuffer() throws IOException {
+ // NOP
+ // TODO throw?
+ }
+
+ public int getBufferSize() {
+ return response.getBufferSize();
+ }
+
+ public String getCharacterEncoding() {
+ return response.getCharacterEncoding();
+ }
+
+ public String getContentType() {
+ return response.getContentType();
+ }
+
+ public Locale getLocale() {
+ return response.getLocale();
+ }
+
+ public String getNamespace() {
+ return response.getNamespace();
+ }
+
+ public OutputStream getPortletOutputStream() throws IOException {
+ // write forbidden
+ return null;
+ }
+
+ public PrintWriter getWriter() throws IOException {
+ // write forbidden
+ return null;
+ }
+
+ public boolean isCommitted() {
+ return response.isCommitted();
+ }
+
+ public void reset() {
+ // NOP
+ // TODO throw?
+ }
+
+ public void resetBuffer() {
+ // NOP
+ // TODO throw?
+ }
+
+ public void setBufferSize(int size) {
+ // NOP
+ // TODO throw?
+ }
+
+ public void setContentType(String type) {
+ // NOP
+ // TODO throw?
+ }
+
+ public void setProperty(String key, String value) {
+ response.setProperty(key, value);
+ }
+
+ public void setTitle(String title) {
+ response.setTitle(title);
+ }
+
+ }
+
+}
diff --git a/src/com/vaadin/terminal/gwt/server/SessionExpired.java b/src/com/vaadin/terminal/gwt/server/SessionExpired.java
new file mode 100644
index 0000000000..c2cf309837
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/server/SessionExpired.java
@@ -0,0 +1,6 @@
+package com.vaadin.terminal.gwt.server;
+
+@SuppressWarnings("serial")
+public class SessionExpired extends Exception {
+
+}
diff --git a/src/com/vaadin/terminal/gwt/server/SystemMessageException.java b/src/com/vaadin/terminal/gwt/server/SystemMessageException.java
new file mode 100644
index 0000000000..67731d5627
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/server/SystemMessageException.java
@@ -0,0 +1,54 @@
+package com.vaadin.terminal.gwt.server;
+
+@SuppressWarnings("serial")
+public class SystemMessageException extends RuntimeException {
+
+ /**
+ * Cause of the method exception
+ */
+ private Throwable cause;
+
+ /**
+ * Constructs a new <code>SystemMessageException</code> with the specified
+ * detail message.
+ *
+ * @param msg
+ * the detail message.
+ */
+ public SystemMessageException(String msg) {
+ super(msg);
+ }
+
+ /**
+ * Constructs a new <code>SystemMessageException</code> with the specified
+ * detail message and cause.
+ *
+ * @param msg
+ * the detail message.
+ * @param cause
+ * the cause of the exception.
+ */
+ public SystemMessageException(String msg, Throwable cause) {
+ super(msg, cause);
+ }
+
+ /**
+ * Constructs a new <code>SystemMessageException</code> from another
+ * exception.
+ *
+ * @param cause
+ * the cause of the exception.
+ */
+ public SystemMessageException(Throwable cause) {
+ this.cause = cause;
+ }
+
+ /**
+ * @see java.lang.Throwable#getCause()
+ */
+ @Override
+ public Throwable getCause() {
+ return cause;
+ }
+
+} \ No newline at end of file
diff --git a/src/com/vaadin/terminal/gwt/server/WebApplicationContext.java b/src/com/vaadin/terminal/gwt/server/WebApplicationContext.java
new file mode 100644
index 0000000000..0cc83f21f5
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/server/WebApplicationContext.java
@@ -0,0 +1,286 @@
+/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.server;
+
+import java.io.File;
+import java.io.PrintWriter;
+import java.io.Serializable;
+import java.io.StringWriter;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpSession;
+import javax.servlet.http.HttpSessionBindingEvent;
+import javax.servlet.http.HttpSessionBindingListener;
+
+import com.vaadin.Application;
+import com.vaadin.service.ApplicationContext;
+
+/**
+ * Web application context for the IT Mill Toolkit applications.
+ *
+ * @author IT Mill Ltd.
+ * @version
+ * @VERSION@
+ * @since 3.1
+ */
+@SuppressWarnings("serial")
+public class WebApplicationContext implements ApplicationContext,
+ HttpSessionBindingListener, Serializable {
+
+ protected List<TransactionListener> listeners;
+
+ protected transient HttpSession session;
+
+ protected final HashSet<Application> applications = new HashSet<Application>();
+
+ protected WebBrowser browser = new WebBrowser();
+
+ protected HashMap<Application, CommunicationManager> applicationToAjaxAppMgrMap = new HashMap<Application, CommunicationManager>();
+
+ /**
+ * Creates a new Web Application Context.
+ *
+ */
+ WebApplicationContext() {
+
+ }
+
+ /**
+ * Gets the application context base directory.
+ *
+ * @see com.vaadin.service.ApplicationContext#getBaseDirectory()
+ */
+ public File getBaseDirectory() {
+ final String realPath = ApplicationServlet.getResourcePath(session
+ .getServletContext(), "/");
+ if (realPath == null) {
+ return null;
+ }
+ return new File(realPath);
+ }
+
+ /**
+ * Gets the http-session application is running in.
+ *
+ * @return HttpSession this application context resides in.
+ */
+ public HttpSession getHttpSession() {
+ return session;
+ }
+
+ /**
+ * Gets the applications in this context.
+ *
+ * @see com.vaadin.service.ApplicationContext#getApplications()
+ */
+ public Collection getApplications() {
+ return Collections.unmodifiableCollection(applications);
+ }
+
+ /**
+ * Gets the application context for HttpSession.
+ *
+ * @param session
+ * the HTTP session.
+ * @return the application context for HttpSession.
+ */
+ static public WebApplicationContext getApplicationContext(
+ HttpSession session) {
+ WebApplicationContext cx = (WebApplicationContext) session
+ .getAttribute(WebApplicationContext.class.getName());
+ if (cx == null) {
+ cx = new WebApplicationContext();
+ session.setAttribute(WebApplicationContext.class.getName(), cx);
+ }
+ if (cx.session == null) {
+ cx.session = session;
+ }
+ return cx;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (session == null) {
+ return false;
+ }
+
+ return session.equals(obj);
+ }
+
+ @Override
+ public int hashCode() {
+ if (session == null) {
+ return -1;
+ }
+ return session.hashCode();
+ }
+
+ /**
+ * Adds the transaction listener to this context.
+ *
+ * @see com.vaadin.service.ApplicationContext#addTransactionListener(com.vaadin.service.ApplicationContext.TransactionListener)
+ */
+ public void addTransactionListener(TransactionListener listener) {
+ if (listeners == null) {
+ listeners = new LinkedList<TransactionListener>();
+ }
+ listeners.add(listener);
+ }
+
+ /**
+ * Removes the transaction listener from this context.
+ *
+ * @see com.vaadin.service.ApplicationContext#removeTransactionListener(com.vaadin.service.ApplicationContext.TransactionListener)
+ */
+ public void removeTransactionListener(TransactionListener listener) {
+ if (listeners != null) {
+ listeners.remove(listener);
+ }
+
+ }
+
+ /**
+ * Notifies the transaction start.
+ *
+ * @param application
+ * @param request
+ * the HTTP request.
+ */
+ protected void startTransaction(Application application,
+ HttpServletRequest request) {
+ if (listeners == null) {
+ return;
+ }
+ for (final Iterator i = listeners.iterator(); i.hasNext();) {
+ ((ApplicationContext.TransactionListener) i.next())
+ .transactionStart(application, request);
+ }
+ }
+
+ /**
+ * Notifies the transaction end.
+ *
+ * @param application
+ * @param request
+ * the HTTP request.
+ */
+ protected void endTransaction(Application application,
+ HttpServletRequest request) {
+ if (listeners == null) {
+ return;
+ }
+
+ LinkedList<Exception> exceptions = null;
+ for (final Iterator i = listeners.iterator(); i.hasNext();) {
+ try {
+ ((ApplicationContext.TransactionListener) i.next())
+ .transactionEnd(application, request);
+ } catch (final RuntimeException t) {
+ if (exceptions == null) {
+ exceptions = new LinkedList<Exception>();
+ }
+ exceptions.add(t);
+ }
+ }
+
+ // If any runtime exceptions occurred, throw a combined exception
+ if (exceptions != null) {
+ final StringBuffer msg = new StringBuffer();
+ for (final Iterator i = exceptions.iterator(); i.hasNext();) {
+ final RuntimeException e = (RuntimeException) i.next();
+ if (msg.length() == 0) {
+ msg.append("\n\n--------------------------\n\n");
+ }
+ msg.append(e.getMessage() + "\n");
+ final StringWriter trace = new StringWriter();
+ e.printStackTrace(new PrintWriter(trace, true));
+ msg.append(trace.toString());
+ }
+ throw new RuntimeException(msg.toString());
+ }
+ }
+
+ protected void removeApplication(Application application) {
+ applications.remove(application);
+ }
+
+ protected void addApplication(Application application) {
+ applications.add(application);
+ }
+
+ /**
+ * @see javax.servlet.http.HttpSessionBindingListener#valueBound(HttpSessionBindingEvent)
+ */
+ public void valueBound(HttpSessionBindingEvent arg0) {
+ // We are not interested in bindings
+ }
+
+ /**
+ * @see javax.servlet.http.HttpSessionBindingListener#valueUnbound(HttpSessionBindingEvent)
+ */
+ public void valueUnbound(HttpSessionBindingEvent event) {
+ // If we are going to be unbound from the session, the session must be
+ // closing
+ try {
+ while (!applications.isEmpty()) {
+ final Application app = applications.iterator().next();
+ app.close();
+ applicationToAjaxAppMgrMap.remove(app);
+ removeApplication(app);
+ }
+ } catch (Exception e) {
+ // This should never happen but is possible with rare
+ // configurations (e.g. robustness tests). If you have one
+ // thread doing HTTP socket write and another thread trying to
+ // remove same application here. Possible if you got e.g. session
+ // lifetime 1 min but socket write may take longer than 1 min.
+ // FIXME: Handle exception
+ System.err.println("Could not remove application, leaking memory.");
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * Get the web browser associated with this application context.
+ *
+ * Because application context is related to the http session and server
+ * maintains one session per browser-instance, each context has exactly one
+ * web browser associated with it.
+ *
+ * @return
+ */
+ public WebBrowser getBrowser() {
+ return browser;
+ }
+
+ /**
+ * Gets communication manager for an application.
+ *
+ * If this application has not been running before, a new manager is
+ * created.
+ *
+ * @param application
+ * @return CommunicationManager
+ */
+ protected CommunicationManager getApplicationManager(
+ Application application, AbstractApplicationServlet servlet) {
+ CommunicationManager mgr = applicationToAjaxAppMgrMap.get(application);
+
+ if (mgr == null) {
+ // Creates new manager
+ mgr = new CommunicationManager(application, servlet);
+ applicationToAjaxAppMgrMap.put(application, mgr);
+ }
+ return mgr;
+ }
+
+}
diff --git a/src/com/vaadin/terminal/gwt/server/WebBrowser.java b/src/com/vaadin/terminal/gwt/server/WebBrowser.java
new file mode 100644
index 0000000000..2ec726fc6a
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/server/WebBrowser.java
@@ -0,0 +1,98 @@
+/*
+@ITMillApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.server;
+
+import java.util.Locale;
+
+import javax.servlet.http.HttpServletRequest;
+
+import com.vaadin.terminal.Terminal;
+
+@SuppressWarnings("serial")
+public class WebBrowser implements Terminal {
+
+ private int screenHeight = 0;
+ private int screenWidth = 0;
+ private String browserApplication = null;
+ private Locale locale;
+ private String address;
+ private boolean secureConnection;
+
+ /**
+ * There is no default-theme for this terminal type.
+ *
+ * @return Allways returns null.
+ */
+ public String getDefaultTheme() {
+ return null;
+ }
+
+ /**
+ * Get the height of the users display in pixels.
+ *
+ */
+ public int getScreenHeight() {
+ return screenHeight;
+ }
+
+ /**
+ * Get the width of the users display in pixels.
+ *
+ */
+ public int getScreenWidth() {
+ return screenWidth;
+ }
+
+ /**
+ * Get the browser user-agent string.
+ *
+ * @return
+ */
+ public String getBrowserApplication() {
+ return browserApplication;
+ }
+
+ void updateBrowserProperties(HttpServletRequest request) {
+ locale = request.getLocale();
+ address = request.getRemoteAddr();
+ secureConnection = request.isSecure();
+
+ final String agent = request.getHeader("user-agent");
+ if (agent != null) {
+ browserApplication = agent;
+ }
+
+ final String sw = request.getParameter("screenWidth");
+ final String sh = request.getParameter("screenHeight");
+ if (sw != null && sh != null) {
+ try {
+ screenHeight = Integer.parseInt(sh);
+ screenWidth = Integer.parseInt(sw);
+ } catch (final NumberFormatException e) {
+ screenHeight = screenWidth = 0;
+ }
+ }
+ }
+
+ /**
+ * Get the IP-address of the web browser.
+ *
+ * @return IP-address in 1.12.123.123 -format
+ */
+ public String getAddress() {
+ return address;
+ }
+
+ /** Get the default locate of the browser. */
+ public Locale getLocale() {
+ return locale;
+ }
+
+ /** Is the connection made using HTTPS? */
+ public boolean isSecureConnection() {
+ return secureConnection;
+ }
+
+}